summaryrefslogtreecommitdiffstats
path: root/src/test
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 20:49:52 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-10 20:49:52 +0000
commit55944e5e40b1be2afc4855d8d2baf4b73d1876b5 (patch)
tree33f869f55a1b149e9b7c2b7e201867ca5dd52992 /src/test
parentInitial commit. (diff)
downloadsystemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.tar.xz
systemd-55944e5e40b1be2afc4855d8d2baf4b73d1876b5.zip
Adding upstream version 255.4.upstream/255.4
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rwxr-xr-xsrc/test/generate-sym-test.py114
-rw-r--r--src/test/meson.build597
-rw-r--r--src/test/nss-test-util.c42
-rw-r--r--src/test/nss-test-util.h8
-rw-r--r--src/test/test-acl-util.c130
-rw-r--r--src/test/test-af-list.c30
-rw-r--r--src/test/test-alloc-util.c233
-rw-r--r--src/test/test-architecture.c53
-rw-r--r--src/test/test-argv-util.c132
-rw-r--r--src/test/test-arphrd-util.c26
-rw-r--r--src/test/test-ask-password-api.c21
-rw-r--r--src/test/test-async.c100
-rw-r--r--src/test/test-barrier.c441
-rw-r--r--src/test/test-bitfield.c230
-rw-r--r--src/test/test-bitmap.c116
-rw-r--r--src/test/test-blockdev-util.c41
-rw-r--r--src/test/test-boot-timestamps.c89
-rw-r--r--src/test/test-bootspec.c211
-rw-r--r--src/test/test-bpf-devices.c307
-rw-r--r--src/test/test-bpf-firewall.c217
-rw-r--r--src/test/test-bpf-foreign-programs.c330
-rw-r--r--src/test/test-bpf-lsm.c102
-rw-r--r--src/test/test-btrfs-physical-offset.c37
-rw-r--r--src/test/test-btrfs.c200
-rw-r--r--src/test/test-bus-util.c47
-rw-r--r--src/test/test-calendarspec.c264
-rw-r--r--src/test/test-cap-list.c177
-rw-r--r--src/test/test-capability.c332
-rw-r--r--src/test/test-cgroup-cpu.c34
-rw-r--r--src/test/test-cgroup-mask.c184
-rw-r--r--src/test/test-cgroup-setup.c73
-rw-r--r--src/test/test-cgroup-unit-default.c138
-rw-r--r--src/test/test-cgroup-util.c466
-rw-r--r--src/test/test-cgroup.c132
-rw-r--r--src/test/test-chase-manual.c116
-rw-r--r--src/test/test-chase.c756
-rw-r--r--src/test/test-chown-rec.c162
-rw-r--r--src/test/test-clock.c74
-rw-r--r--src/test/test-compare-operator.c43
-rw-r--r--src/test/test-compress-benchmark.c176
-rw-r--r--src/test/test-compress.c373
-rw-r--r--src/test/test-condition.c1496
-rw-r--r--src/test/test-conf-files.c218
-rw-r--r--src/test/test-conf-parser.c393
-rw-r--r--src/test/test-copy.c532
-rw-r--r--src/test/test-core-unit.c120
-rw-r--r--src/test/test-coredump-util.c161
-rw-r--r--src/test/test-cpu-set-util.c280
-rw-r--r--src/test/test-creds.c121
-rw-r--r--src/test/test-cryptolib.c38
-rw-r--r--src/test/test-daemon.c60
-rw-r--r--src/test/test-data-fd-util.c148
-rw-r--r--src/test/test-date.c112
-rw-r--r--src/test/test-dev-setup.c66
-rw-r--r--src/test/test-device-nodes.c38
-rw-r--r--src/test/test-devnum-util.c124
-rw-r--r--src/test/test-dlopen-so.c76
-rw-r--r--src/test/test-dlopen.c19
-rw-r--r--src/test/test-dns-domain.c753
-rw-r--r--src/test/test-ellipsize.c159
-rw-r--r--src/test/test-emergency-action.c51
-rw-r--r--src/test/test-engine.c300
-rw-r--r--src/test/test-env-file.c191
-rw-r--r--src/test/test-env-util.c563
-rw-r--r--src/test/test-errno-list.c32
-rw-r--r--src/test/test-errno-util.c112
-rw-r--r--src/test/test-escape.c242
-rw-r--r--src/test/test-ether-addr-util.c162
-rw-r--r--src/test/test-exec-util.c456
-rw-r--r--src/test/test-execute.c1550
-rw-r--r--src/test/test-execve.c40
-rw-r--r--src/test/test-exit-status.c38
-rw-r--r--src/test/test-extract-word.c763
-rw-r--r--src/test/test-fd-util.c765
-rw-r--r--src/test/test-fdset.c212
-rw-r--r--src/test/test-fiemap.c64
-rw-r--r--src/test/test-fileio.c1151
-rw-r--r--src/test/test-firewall-util.c123
-rw-r--r--src/test/test-format-table.c635
-rw-r--r--src/test/test-format-util.c61
-rw-r--r--src/test/test-fs-util.c796
-rw-r--r--src/test/test-fstab-util.c192
-rw-r--r--src/test/test-glob-util.c137
-rw-r--r--src/test/test-gpt.c111
-rw-r--r--src/test/test-gunicode.c27
-rw-r--r--src/test/test-hash-funcs.c77
-rw-r--r--src/test/test-hashmap-ordered.awk12
-rw-r--r--src/test/test-hashmap-plain.c1008
-rw-r--r--src/test/test-hashmap.c171
-rw-r--r--src/test/test-hexdecoct.c548
-rw-r--r--src/test/test-hmac.c68
-rw-r--r--src/test/test-hostname-setup.c64
-rw-r--r--src/test/test-hostname-util.c116
-rw-r--r--src/test/test-id128.c339
-rw-r--r--src/test/test-image-policy.c132
-rw-r--r--src/test/test-import-util.c64
-rw-r--r--src/test/test-in-addr-prefix-util.c121
-rw-r--r--src/test/test-in-addr-util.c408
-rw-r--r--src/test/test-install-file.c64
-rw-r--r--src/test/test-install-root.c1299
-rw-r--r--src/test/test-install.c270
-rw-r--r--src/test/test-io-util.c49
-rw-r--r--src/test/test-ip-protocol-list.c71
-rw-r--r--src/test/test-ipcrm.c29
-rw-r--r--src/test/test-job-type.c81
-rw-r--r--src/test/test-journal-importer.c71
-rw-r--r--src/test/test-json.c816
-rw-r--r--src/test/test-kbd-util.c27
-rw-r--r--src/test/test-libcrypt-util.c126
-rw-r--r--src/test/test-libmount.c110
-rw-r--r--src/test/test-limits-util.c89
-rw-r--r--src/test/test-list.c286
-rw-r--r--src/test/test-load-fragment.c1105
-rw-r--r--src/test/test-local-addresses.c73
-rw-r--r--src/test/test-locale-util.c132
-rw-r--r--src/test/test-lock-util.c65
-rw-r--r--src/test/test-log.c226
-rw-r--r--src/test/test-logarithm.c95
-rw-r--r--src/test/test-loop-block.c350
-rw-r--r--src/test/test-loopback.c46
-rw-r--r--src/test/test-macro.c1040
-rw-r--r--src/test/test-manager.c19
-rw-r--r--src/test/test-math-util.c110
-rw-r--r--src/test/test-memfd-util.c30
-rw-r--r--src/test/test-memory-util.c125
-rw-r--r--src/test/test-mempool.c92
-rw-r--r--src/test/test-mempress.c309
-rw-r--r--src/test/test-memstream-util.c60
-rw-r--r--src/test/test-mkdir.c141
-rw-r--r--src/test/test-modhex.c51
-rw-r--r--src/test/test-mount-util.c509
-rw-r--r--src/test/test-mountpoint-util.c434
-rw-r--r--src/test/test-namespace.c199
-rw-r--r--src/test/test-net-naming-scheme.c31
-rw-r--r--src/test/test-netlink-manual.c126
-rw-r--r--src/test/test-nft-set.c78
-rw-r--r--src/test/test-ns.c125
-rw-r--r--src/test/test-nscd-flush.c20
-rw-r--r--src/test/test-nss-hosts.c495
-rw-r--r--src/test/test-nss-users.c256
-rw-r--r--src/test/test-nulstr-util.c184
-rw-r--r--src/test/test-open-file.c185
-rw-r--r--src/test/test-openssl.c483
-rw-r--r--src/test/test-ordered-set.c112
-rw-r--r--src/test/test-os-util.c135
-rw-r--r--src/test/test-parse-argument.c53
-rw-r--r--src/test/test-parse-helpers.c95
-rw-r--r--src/test/test-parse-util.c979
-rw-r--r--src/test/test-path-lookup.c126
-rw-r--r--src/test/test-path-util.c1308
-rw-r--r--src/test/test-path.c418
-rw-r--r--src/test/test-percent-util.c199
-rw-r--r--src/test/test-pretty-print.c79
-rw-r--r--src/test/test-prioq.c123
-rw-r--r--src/test/test-proc-cmdline.c354
-rw-r--r--src/test/test-process-util.c954
-rw-r--r--src/test/test-procfs-util.c76
-rw-r--r--src/test/test-psi-util.c78
-rw-r--r--src/test/test-qrcode-util.c23
-rw-r--r--src/test/test-random-util.c79
-rw-r--r--src/test/test-ratelimit.c43
-rw-r--r--src/test/test-raw-clone.c41
-rw-r--r--src/test/test-recurse-dir.c177
-rw-r--r--src/test/test-replace-var.c32
-rw-r--r--src/test/test-rlimit-util.c139
-rw-r--r--src/test/test-rm-rf.c114
-rw-r--r--src/test/test-sbat.c26
-rw-r--r--src/test/test-sched-prio.c81
-rw-r--r--src/test/test-sd-hwdb.c85
-rw-r--r--src/test/test-sd-path.c59
-rw-r--r--src/test/test-seccomp.c1234
-rw-r--r--src/test/test-secure-bits.c97
-rw-r--r--src/test/test-selinux.c104
-rw-r--r--src/test/test-serialize.c265
-rw-r--r--src/test/test-set-disable-mempool.c58
-rw-r--r--src/test/test-set.c403
-rw-r--r--src/test/test-sha256.c50
-rw-r--r--src/test/test-sigbus.c62
-rw-r--r--src/test/test-signal-util.c175
-rw-r--r--src/test/test-siphash24.c108
-rw-r--r--src/test/test-sizeof.c130
-rw-r--r--src/test/test-sleep-config.c79
-rw-r--r--src/test/test-socket-bind.c149
-rw-r--r--src/test/test-socket-netlink.c372
-rw-r--r--src/test/test-socket-util.c593
-rw-r--r--src/test/test-specifier.c188
-rw-r--r--src/test/test-stat-util.c188
-rw-r--r--src/test/test-static-destruct.c67
-rw-r--r--src/test/test-strbuf.c73
-rw-r--r--src/test/test-string-util.c1327
-rw-r--r--src/test/test-strip-tab-ansi.c72
-rw-r--r--src/test/test-strv.c1009
-rw-r--r--src/test/test-strxcpyx.c175
-rw-r--r--src/test/test-sysctl-util.c75
-rw-r--r--src/test/test-tables.c131
-rw-r--r--src/test/test-terminal-util.c168
-rw-r--r--src/test/test-time-util.c1195
-rw-r--r--src/test/test-tmpfile-util.c306
-rw-r--r--src/test/test-tpm2.c1324
-rw-r--r--src/test/test-udev-util.c63
-rw-r--r--src/test/test-uid-alloc-range.c93
-rw-r--r--src/test/test-uid-range.c175
-rw-r--r--src/test/test-umask-util.c57
-rw-r--r--src/test/test-unaligned.c168
-rw-r--r--src/test/test-unit-file.c110
-rw-r--r--src/test/test-unit-name.c1009
-rw-r--r--src/test/test-unit-serialize.c63
-rw-r--r--src/test/test-user-util.c484
-rw-r--r--src/test/test-utf8.c235
-rw-r--r--src/test/test-utmp.c58
-rw-r--r--src/test/test-varlink-idl.c385
-rw-r--r--src/test/test-varlink.c376
-rw-r--r--src/test/test-verbs.c59
-rw-r--r--src/test/test-watch-pid.c102
-rw-r--r--src/test/test-watchdog.c39
-rw-r--r--src/test/test-web-util.c21
-rw-r--r--src/test/test-xattr-util.c129
-rw-r--r--src/test/test-xml.c68
218 files changed, 54611 insertions, 0 deletions
diff --git a/src/test/generate-sym-test.py b/src/test/generate-sym-test.py
new file mode 100755
index 0000000..028d108
--- /dev/null
+++ b/src/test/generate-sym-test.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+import os
+import re
+import sys
+
+def process_sym_file(file):
+ for line in file:
+ m = re.search(r'^ +([a-zA-Z0-9_]+);', line)
+ if m:
+ if m[1] == 'sd_bus_object_vtable_format':
+ print(' {{"{0}", &{0}}},'.format(m[1]))
+ else:
+ print(' {{"{0}", {0}}},'.format(m[1]))
+
+def process_source_file(file):
+ for line in file:
+ # Functions
+ m = re.search(r'^_public_\s+(\S+\s+)+\**(\w+)\s*\(', line)
+ if m:
+ print(' {{ "{0}", {0} }},'.format(m[2]))
+ # Variables
+ m = re.search(r'^_public_\s+(\S+\s+)+\**(\w+)\s*=', line)
+ if m:
+ print(' {{ "{0}", &{0} }},'.format(m[2]))
+ # Functions defined through a macro
+ m = re.search(r'^DEFINE_PUBLIC_TRIVIAL_REF_FUNC\([^,]+,\s*(\w+)\s*\)', line)
+ if m:
+ print(' {{ "{0}_ref", {0}_ref }},'.format(m[1]))
+ m = re.search(r'^DEFINE_PUBLIC_TRIVIAL_UNREF_FUNC\([^,]+,\s*(\w+)\s*,', line)
+ if m:
+ print(' {{ "{0}_unref", {0}_unref }},'.format(m[1]))
+ m = re.search(r"^DEFINE_PUBLIC_TRIVIAL_REF_UNREF_FUNC\([^,]+,\s*(\w+)\s*,", line)
+ if m:
+ print(' {{ "{0}_ref", {0}_ref }},'.format(m[1]))
+ print(' {{ "{0}_unref", {0}_unref }},'.format(m[1]))
+
+print('''/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+''')
+
+for header in sys.argv[3:]:
+ print('#include "{}"'.format(header.split('/')[-1]))
+
+print('''
+/* We want to check deprecated symbols too, without complaining */
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+''')
+
+print('''
+struct symbol {
+ const char *name;
+ const void *symbol;
+};
+static struct symbol symbols_from_sym[] = {''')
+
+with open(sys.argv[1], "r") as f:
+ process_sym_file(f)
+
+print(''' {}
+}, symbols_from_source[] = {''')
+
+for dirpath, _, filenames in sorted(os.walk(sys.argv[2])):
+ for filename in sorted(filenames):
+ if not filename.endswith(".c") and not filename.endswith(".h"):
+ continue
+ with open(os.path.join(dirpath, filename), "r") as f:
+ process_source_file(f)
+
+print(''' {}
+};
+
+static int sort_callback(const void *a, const void *b) {
+ const struct symbol *x = a, *y = b;
+ return strcmp(x->name, y->name);
+}
+
+int main(void) {
+ size_t i, j;
+
+ qsort(symbols_from_sym, sizeof(symbols_from_sym)/sizeof(symbols_from_sym[0])-1, sizeof(symbols_from_sym[0]), sort_callback);
+ qsort(symbols_from_source, sizeof(symbols_from_source)/sizeof(symbols_from_source[0])-1, sizeof(symbols_from_source[0]), sort_callback);
+
+ puts("From symbol file:");
+ for (i = 0; symbols_from_sym[i].name; i++)
+ printf("%p: %s\\n", symbols_from_sym[i].symbol, symbols_from_sym[i].name);
+
+ puts("\\nFrom source files:");
+ for (j = 0; symbols_from_source[j].name; j++)
+ printf("%p: %s\\n", symbols_from_source[j].symbol, symbols_from_source[j].name);
+
+ puts("");
+ printf("Found %zu symbols from symbol file.\\n", i);
+ printf("Found %zu symbols from source files.\\n", j);
+
+ for (i = 0; symbols_from_sym[i].name; i++) {
+ struct symbol*n = bsearch(symbols_from_sym+i, symbols_from_source, sizeof(symbols_from_source)/sizeof(symbols_from_source[0])-1, sizeof(symbols_from_source[0]), sort_callback);
+ if (!n)
+ printf("Found in symbol file, but not in sources: %s\\n", symbols_from_sym[i].name);
+ }
+
+ for (j = 0; symbols_from_source[j].name; j++) {
+ struct symbol*n = bsearch(symbols_from_source+j, symbols_from_source, sizeof(symbols_from_sym)/sizeof(symbols_from_sym[0])-1, sizeof(symbols_from_sym[0]), sort_callback);
+ if (!n)
+ printf("Found in sources, but not in symbol file: %s\\n", symbols_from_source[i].name);
+ }
+
+ return i == j ? EXIT_SUCCESS : EXIT_FAILURE;
+}''')
diff --git a/src/test/meson.build b/src/test/meson.build
new file mode 100644
index 0000000..cce90d7
--- /dev/null
+++ b/src/test/meson.build
@@ -0,0 +1,597 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+awkscript = 'test-hashmap-ordered.awk'
+test_hashmap_ordered_c = custom_target(
+ 'test-hashmap-ordered.c',
+ input : [awkscript, 'test-hashmap-plain.c'],
+ output : 'test-hashmap-ordered.c',
+ command : [awk, '-f', '@INPUT0@', '@INPUT1@'],
+ capture : true,
+ build_by_default : want_tests != 'false')
+
+path = run_command(sh, '-c', 'echo "$PATH"', check: true).stdout().strip()
+test_env = environment()
+test_env.set('SYSTEMD_LANGUAGE_FALLBACK_MAP', language_fallback_map)
+test_env.set('PATH', project_build_root + ':' + path)
+test_env.set('PROJECT_BUILD_ROOT', project_build_root)
+test_env.set('SYSTEMD_SLOW_TESTS', slow_tests ? '1' : '0')
+
+if efi_addon != ''
+ test_env.set('EFI_ADDON', efi_addon)
+endif
+
+############################################################
+
+generate_sym_test_py = find_program('generate-sym-test.py')
+
+test_libsystemd_sym_c = custom_target(
+ 'test-libsystemd-sym.c',
+ input : [libsystemd_sym_path] + systemd_headers + libsystemd_sources,
+ output : 'test-libsystemd-sym.c',
+ command : [generate_sym_test_py, libsystemd_sym_path, libsystemd_dir_path] + systemd_headers,
+ capture : true,
+ build_by_default : want_tests != 'false')
+
+test_libudev_sym_c = custom_target(
+ 'test-libudev-sym.c',
+ input : [libudev_sym_path, libudev_h_path] + libudev_sources,
+ output : 'test-libudev-sym.c',
+ command : [generate_sym_test_py, libudev_sym_path, libudev_dir_path, libudev_h_path],
+ capture : true,
+ build_by_default : want_tests != 'false')
+
+############################################################
+
+simple_tests += files(
+ 'test-alloc-util.c',
+ 'test-architecture.c',
+ 'test-argv-util.c',
+ 'test-barrier.c',
+ 'test-bitfield.c',
+ 'test-bitmap.c',
+ 'test-blockdev-util.c',
+ 'test-bootspec.c',
+ 'test-bus-util.c',
+ 'test-calendarspec.c',
+ 'test-cgroup-setup.c',
+ 'test-cgroup-util.c',
+ 'test-cgroup.c',
+ 'test-chase.c',
+ 'test-clock.c',
+ 'test-compare-operator.c',
+ 'test-condition.c',
+ 'test-conf-files.c',
+ 'test-conf-parser.c',
+ 'test-copy.c',
+ 'test-coredump-util.c',
+ 'test-cpu-set-util.c',
+ 'test-creds.c',
+ 'test-daemon.c',
+ 'test-data-fd-util.c',
+ 'test-date.c',
+ 'test-dev-setup.c',
+ 'test-device-nodes.c',
+ 'test-devnum-util.c',
+ 'test-dns-domain.c',
+ 'test-ellipsize.c',
+ 'test-env-file.c',
+ 'test-env-util.c',
+ 'test-errno-util.c',
+ 'test-escape.c',
+ 'test-ether-addr-util.c',
+ 'test-exec-util.c',
+ 'test-execve.c',
+ 'test-exit-status.c',
+ 'test-extract-word.c',
+ 'test-fdset.c',
+ 'test-fiemap.c',
+ 'test-fileio.c',
+ 'test-firewall-util.c',
+ 'test-format-table.c',
+ 'test-format-util.c',
+ 'test-fs-util.c',
+ 'test-fstab-util.c',
+ 'test-glob-util.c',
+ 'test-gpt.c',
+ 'test-gunicode.c',
+ 'test-hash-funcs.c',
+ 'test-hexdecoct.c',
+ 'test-hmac.c',
+ 'test-hostname-setup.c',
+ 'test-hostname-util.c',
+ 'test-id128.c',
+ 'test-image-policy.c',
+ 'test-import-util.c',
+ 'test-in-addr-prefix-util.c',
+ 'test-in-addr-util.c',
+ 'test-install-file.c',
+ 'test-install-root.c',
+ 'test-io-util.c',
+ 'test-journal-importer.c',
+ 'test-kbd-util.c',
+ 'test-limits-util.c',
+ 'test-list.c',
+ 'test-local-addresses.c',
+ 'test-locale-util.c',
+ 'test-lock-util.c',
+ 'test-log.c',
+ 'test-logarithm.c',
+ 'test-macro.c',
+ 'test-memfd-util.c',
+ 'test-memory-util.c',
+ 'test-mempool.c',
+ 'test-memstream-util.c',
+ 'test-mkdir.c',
+ 'test-modhex.c',
+ 'test-mountpoint-util.c',
+ 'test-net-naming-scheme.c',
+ 'test-nulstr-util.c',
+ 'test-open-file.c',
+ 'test-ordered-set.c',
+ 'test-os-util.c',
+ 'test-parse-argument.c',
+ 'test-parse-helpers.c',
+ 'test-path-lookup.c',
+ 'test-path-util.c',
+ 'test-percent-util.c',
+ 'test-pretty-print.c',
+ 'test-prioq.c',
+ 'test-proc-cmdline.c',
+ 'test-procfs-util.c',
+ 'test-psi-util.c',
+ 'test-ratelimit.c',
+ 'test-raw-clone.c',
+ 'test-recurse-dir.c',
+ 'test-replace-var.c',
+ 'test-rlimit-util.c',
+ 'test-rm-rf.c',
+ 'test-sd-hwdb.c',
+ 'test-sd-path.c',
+ 'test-secure-bits.c',
+ 'test-selinux.c',
+ 'test-serialize.c',
+ 'test-set.c',
+ 'test-sha256.c',
+ 'test-sigbus.c',
+ 'test-signal-util.c',
+ 'test-siphash24.c',
+ 'test-sleep-config.c',
+ 'test-socket-netlink.c',
+ 'test-socket-util.c',
+ 'test-specifier.c',
+ 'test-stat-util.c',
+ 'test-static-destruct.c',
+ 'test-strbuf.c',
+ 'test-string-util.c',
+ 'test-strip-tab-ansi.c',
+ 'test-strv.c',
+ 'test-strxcpyx.c',
+ 'test-sysctl-util.c',
+ 'test-terminal-util.c',
+ 'test-tmpfile-util.c',
+ 'test-udev-util.c',
+ 'test-uid-alloc-range.c',
+ 'test-uid-range.c',
+ 'test-umask-util.c',
+ 'test-unaligned.c',
+ 'test-unit-file.c',
+ 'test-user-util.c',
+ 'test-utf8.c',
+ 'test-verbs.c',
+ 'test-web-util.c',
+ 'test-xattr-util.c',
+ 'test-xml.c',
+)
+
+############################################################
+
+common_test_dependencies = [
+ libblkid,
+ libmount,
+ librt,
+ libseccomp,
+ libselinux,
+ threads,
+]
+
+executables += [
+ test_template + {
+ 'sources' : files('test-acl-util.c'),
+ 'conditions' : ['HAVE_ACL'],
+ },
+ test_template + {
+ 'sources' : files('test-af-list.c') +
+ generated_gperf_headers,
+ },
+ test_template + {
+ 'sources' : files('test-arphrd-util.c') +
+ generated_gperf_headers,
+ },
+ test_template + {
+ 'sources' : files('test-ask-password-api.c'),
+ 'type' : 'manual',
+ },
+ test_template + {
+ 'sources' : files('test-async.c'),
+ 'timeout' : 120,
+ },
+ test_template + {
+ 'sources' : files('test-boot-timestamps.c'),
+ 'conditions' : ['ENABLE_EFI'],
+ },
+ test_template + {
+ 'sources' : files('test-btrfs.c'),
+ 'type' : 'manual',
+ },
+ test_template + {
+ 'sources' : files('test-btrfs-physical-offset.c'),
+ 'type' : 'manual',
+ },
+ test_template + {
+ 'sources' : files('test-cap-list.c') +
+ generated_gperf_headers,
+ 'dependencies' : libcap,
+ },
+ test_template + {
+ 'sources' : files('test-capability.c'),
+ 'dependencies' : libcap,
+ },
+ test_template + {
+ 'sources' : files('test-chase-manual.c'),
+ 'type' : 'manual',
+ },
+ test_template + {
+ 'sources' : files('test-compress-benchmark.c'),
+ 'link_with' : [
+ libbasic_compress,
+ libshared,
+ ],
+ 'timeout' : 90,
+ },
+ test_template + {
+ 'sources' : files('test-compress.c'),
+ 'link_with' : [
+ libbasic_compress,
+ libshared,
+ ],
+ },
+ test_template + {
+ 'sources' : files('test-cryptolib.c'),
+ 'dependencies' : lib_openssl_or_gcrypt,
+ 'conditions' : ['HAVE_OPENSSL_OR_GCRYPT'],
+ },
+ test_template + {
+ 'sources' : files('test-dlopen-so.c'),
+ 'dependencies' : libp11kit_cflags
+ },
+ test_template + {
+ # only static linking apart from libdl, to make sure that the
+ # module is linked to all libraries that it uses.
+ 'sources' : files('test-dlopen.c'),
+ 'link_with' : libbasic,
+ 'dependencies' : libdl,
+ 'install' : false,
+ 'type' : 'manual',
+ },
+ test_template + {
+ 'sources' : files('test-errno-list.c') +
+ generated_gperf_headers,
+ },
+ test_template + {
+ 'sources' : files('test-fd-util.c'),
+ 'dependencies' : libseccomp,
+ },
+ test_template + {
+ 'sources' : files(
+ 'test-hashmap.c',
+ 'test-hashmap-plain.c',
+ ) + [
+ test_hashmap_ordered_c,
+ ],
+ 'timeout' : 180,
+ },
+ test_template + {
+ 'sources' : files('test-ip-protocol-list.c') +
+ shared_generated_gperf_headers,
+ },
+ test_template + {
+ 'sources' : files('test-ipcrm.c'),
+ 'type' : 'unsafe',
+ },
+ test_template + {
+ 'sources' : files('test-json.c'),
+ 'dependencies' : libm,
+ },
+ test_template + {
+ 'sources' : files('test-libcrypt-util.c'),
+ 'dependencies' : libcrypt,
+ 'timeout' : 120,
+ },
+ test_template + {
+ 'sources' : files('test-libmount.c'),
+ 'dependencies' : [
+ libmount,
+ threads,
+ ],
+ },
+ test_template + {
+ 'sources' : files('test-loopback.c'),
+ 'dependencies' : common_test_dependencies,
+ },
+ test_template + {
+ 'sources' : files('test-math-util.c'),
+ 'dependencies' : libm,
+ },
+ test_template + {
+ 'sources' : files('test-mempress.c'),
+ 'dependencies' : threads,
+ },
+ test_template + {
+ 'sources' : files('test-mount-util.c'),
+ 'dependencies' : libmount,
+ },
+ test_template + {
+ 'sources' : files('test-netlink-manual.c'),
+ 'dependencies' : libkmod,
+ 'conditions' : ['HAVE_KMOD'],
+ 'type' : 'manual',
+ },
+ test_template + {
+ 'sources' : files('test-nft-set.c'),
+ 'type' : 'manual',
+ },
+ test_template + {
+ 'sources' : files('test-nscd-flush.c'),
+ 'conditions' : ['ENABLE_NSCD'],
+ 'type' : 'manual',
+ },
+ test_template + {
+ 'sources' : files(
+ 'test-nss-hosts.c',
+ 'nss-test-util.c',
+ ),
+ 'dependencies' : libdl,
+ 'conditions' : ['ENABLE_NSS'],
+ 'timeout' : 120,
+ },
+ test_template + {
+ 'sources' : files(
+ 'test-nss-users.c',
+ 'nss-test-util.c',
+ ),
+ 'dependencies' : libdl,
+ 'conditions' : ['ENABLE_NSS'],
+ },
+ test_template + {
+ 'sources' : files('test-openssl.c'),
+ 'dependencies' : libopenssl,
+ 'conditions' : ['HAVE_OPENSSL'],
+ },
+ test_template + {
+ 'sources' : files('test-parse-util.c'),
+ 'dependencies' : libm,
+ },
+ test_template + {
+ 'sources' : files('test-process-util.c'),
+ 'dependencies' : threads,
+ },
+ test_template + {
+ 'sources' : files('test-qrcode-util.c'),
+ 'dependencies' : libdl,
+ },
+ test_template + {
+ 'sources' : files('test-random-util.c'),
+ 'dependencies' : libm,
+ 'timeout' : 120,
+ },
+ test_template + {
+ 'sources' : files('test-sbat.c'),
+ 'conditions' : ['ENABLE_BOOTLOADER'],
+ 'c_args' : '-I@0@'.format(efi_config_h_dir),
+ },
+ test_template + {
+ 'sources' : files('test-seccomp.c'),
+ 'dependencies' : libseccomp,
+ 'conditions' : ['HAVE_SECCOMP'],
+ },
+ test_template + {
+ 'sources' : files('test-set-disable-mempool.c'),
+ 'dependencies' : threads,
+ },
+ test_template + {
+ 'sources' : files('test-sizeof.c'),
+ 'link_with' : libbasic,
+ },
+ test_template + {
+ 'sources' : files('test-time-util.c'),
+ 'timeout' : 120,
+ },
+ test_template + {
+ 'sources' : files('test-tpm2.c'),
+ 'dependencies' : libopenssl,
+ 'timeout' : 120,
+ },
+ test_template + {
+ 'sources' : files('test-utmp.c'),
+ 'conditions' : ['ENABLE_UTMP'],
+ },
+ test_template + {
+ 'sources' : files('test-varlink.c'),
+ 'dependencies' : threads,
+ },
+ test_template + {
+ 'sources' : files('test-varlink-idl.c'),
+ 'dependencies' : threads,
+ },
+ test_template + {
+ 'sources' : files('test-watchdog.c'),
+ 'type' : 'unsafe',
+ },
+
+ # Symbol tests
+ test_template + {
+ 'name' : 'test-libsystemd-sym',
+ 'sources' : test_libsystemd_sym_c,
+ 'link_with' : libsystemd,
+ 'suite' : 'libsystemd',
+ },
+ test_template + {
+ 'name' : 'test-libsystemd-static-sym',
+ 'sources' : test_libsystemd_sym_c,
+ 'link_with' : install_libsystemd_static,
+ 'build_by_default' : want_tests != 'false' and static_libsystemd != 'false',
+ 'install' : install_tests and static_libsystemd != 'false',
+ 'suite' : 'libsystemd',
+ },
+ test_template + {
+ 'name' : 'test-libudev-sym',
+ 'sources' : test_libudev_sym_c,
+ 'include_directories' : libudev_includes,
+ 'c_args' : ['-Wno-deprecated-declarations'] + test_cflags,
+ 'link_with' : libudev,
+ 'suite' : 'libudev',
+ },
+ test_template + {
+ 'name' : 'test-libudev-static-sym',
+ 'sources' : test_libudev_sym_c,
+ 'include_directories' : libudev_includes,
+ 'c_args' : ['-Wno-deprecated-declarations'] + test_cflags,
+ 'link_with' : install_libudev_static,
+ 'build_by_default' : want_tests != 'false' and static_libudev != 'false',
+ 'install' : install_tests and static_libudev != 'false',
+ 'suite' : 'libudev',
+ },
+
+ # Tests that link to libcore, i.e. tests for pid1 code.
+ core_test_template + {
+ 'sources' : files('test-bpf-devices.c'),
+ 'dependencies' : common_test_dependencies,
+ },
+ core_test_template + {
+ 'sources' : files('test-bpf-firewall.c'),
+ 'dependencies' : common_test_dependencies,
+ },
+ core_test_template + {
+ 'sources' : files('test-bpf-foreign-programs.c'),
+ },
+ core_test_template + {
+ 'sources' : files('test-bpf-lsm.c'),
+ 'dependencies' : common_test_dependencies,
+ },
+ core_test_template + {
+ 'sources' : files('test-cgroup-cpu.c'),
+ },
+ core_test_template + {
+ 'sources' : files('test-cgroup-mask.c'),
+ 'dependencies' : common_test_dependencies,
+ },
+ core_test_template + {
+ 'sources' : files('test-cgroup-unit-default.c'),
+ },
+ core_test_template + {
+ 'sources' : files('test-chown-rec.c'),
+ },
+ core_test_template + {
+ 'sources' : files('test-core-unit.c'),
+ 'dependencies' : common_test_dependencies,
+ },
+ core_test_template + {
+ 'sources' : files('test-emergency-action.c'),
+ },
+ core_test_template + {
+ 'sources' : files('test-engine.c'),
+ 'dependencies' : common_test_dependencies,
+ },
+ core_test_template + {
+ 'sources' : files('test-execute.c'),
+ 'dependencies' : common_test_dependencies,
+ 'timeout' : 360,
+ },
+ core_test_template + {
+ 'sources' : files('test-install.c'),
+ 'type' : 'manual',
+ },
+ core_test_template + {
+ 'sources' : files('test-job-type.c'),
+ 'dependencies' : common_test_dependencies,
+ },
+ core_test_template + {
+ 'sources' : files('test-load-fragment.c'),
+ 'dependencies' : common_test_dependencies,
+ },
+ core_test_template + {
+ 'sources' : files('test-loop-block.c'),
+ 'dependencies' : [threads, libblkid],
+ 'parallel' : false,
+ },
+ core_test_template + {
+ 'sources' : files('test-manager.c'),
+ },
+ core_test_template + {
+ 'sources' : files('test-namespace.c'),
+ 'dependencies' : [
+ libblkid,
+ threads,
+ ],
+ },
+ core_test_template + {
+ 'sources' : files('test-ns.c'),
+ 'dependencies' : common_test_dependencies,
+ 'type' : 'manual',
+ },
+ core_test_template + {
+ 'sources' : files('test-path.c'),
+ 'dependencies' : common_test_dependencies,
+ 'timeout' : 120,
+ },
+ core_test_template + {
+ 'sources' : files('test-sched-prio.c'),
+ 'dependencies' : common_test_dependencies,
+ },
+ core_test_template + {
+ 'sources' : files('test-socket-bind.c'),
+ 'dependencies' : libdl,
+ 'conditions' : ['BPF_FRAMEWORK'],
+ },
+ core_test_template + {
+ 'sources' : files('test-tables.c'),
+ },
+ core_test_template + {
+ 'sources' : files('test-unit-name.c'),
+ 'dependencies' : common_test_dependencies,
+ },
+ core_test_template + {
+ 'sources' : files('test-unit-serialize.c'),
+ 'dependencies' : common_test_dependencies,
+ },
+ core_test_template + {
+ 'sources' : files('test-watch-pid.c'),
+ 'dependencies' : common_test_dependencies,
+ },
+
+ # Tests from other directories that have link_with deps that were not defined earlier
+ test_template + {
+ 'sources' : files('../libsystemd/sd-bus/test-bus-error.c'),
+ 'link_with' : [
+ libshared_static,
+ libsystemd_static,
+ ],
+ },
+ test_template + {
+ 'sources' : files('../libsystemd/sd-device/test-sd-device-thread.c'),
+ 'link_with' : libsystemd,
+ 'dependencies' : threads,
+ },
+ test_template + {
+ 'sources' : files('../libudev/test-udev-device-thread.c'),
+ 'link_with' : libudev,
+ 'dependencies' : threads,
+ },
+ test_template + {
+ 'sources' : files('../libudev/test-libudev.c'),
+ 'link_with' : [
+ libshared,
+ libudev_basic,
+ ],
+ },
+]
diff --git a/src/test/nss-test-util.c b/src/test/nss-test-util.c
new file mode 100644
index 0000000..20643f8
--- /dev/null
+++ b/src/test/nss-test-util.c
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <dlfcn.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "nss-test-util.h"
+#include "string-util.h"
+
+const char* nss_status_to_string(enum nss_status status, char *buf, size_t buf_len) {
+ switch (status) {
+ case NSS_STATUS_TRYAGAIN:
+ return "NSS_STATUS_TRYAGAIN";
+ case NSS_STATUS_UNAVAIL:
+ return "NSS_STATUS_UNAVAIL";
+ case NSS_STATUS_NOTFOUND:
+ return "NSS_STATUS_NOTFOUND";
+ case NSS_STATUS_SUCCESS:
+ return "NSS_STATUS_SUCCESS";
+ case NSS_STATUS_RETURN:
+ return "NSS_STATUS_RETURN";
+ default:
+ (void) snprintf(buf, buf_len, "%i", status);
+ return buf;
+ }
+};
+
+void* nss_open_handle(const char *dir, const char *module, int flags) {
+ const char *path = NULL;
+ void *handle;
+
+ if (dir)
+ path = strjoina(dir, "/libnss_", module, ".so.2");
+ if (!path || access(path, F_OK) < 0)
+ path = strjoina("libnss_", module, ".so.2");
+
+ log_debug("Using %s", path);
+ handle = dlopen(path, flags);
+ if (!handle)
+ log_error("Failed to load module %s: %s", module, dlerror());
+ return handle;
+}
diff --git a/src/test/nss-test-util.h b/src/test/nss-test-util.h
new file mode 100644
index 0000000..f081e64
--- /dev/null
+++ b/src/test/nss-test-util.h
@@ -0,0 +1,8 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <nss.h>
+#include <stdint.h>
+
+const char* nss_status_to_string(enum nss_status status, char *buf, size_t buf_len);
+void* nss_open_handle(const char *dir, const char *module, int flags);
diff --git a/src/test/test-acl-util.c b/src/test/test-acl-util.c
new file mode 100644
index 0000000..eb9678a
--- /dev/null
+++ b/src/test/test-acl-util.c
@@ -0,0 +1,130 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "acl-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "format-util.h"
+#include "fs-util.h"
+#include "string-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+#include "user-util.h"
+
+TEST_RET(add_acls_for_user) {
+ _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-empty.XXXXXX";
+ _cleanup_close_ int fd = -EBADF;
+ char *cmd;
+ uid_t uid;
+ int r;
+
+ fd = mkostemp_safe(fn);
+ assert_se(fd >= 0);
+
+ /* Use the mode that user journal files use */
+ assert_se(fchmod(fd, 0640) == 0);
+
+ cmd = strjoina("ls -l ", fn);
+ assert_se(system(cmd) == 0);
+
+ cmd = strjoina("getfacl -p ", fn);
+ assert_se(system(cmd) == 0);
+
+ if (getuid() == 0) {
+ const char *nobody = NOBODY_USER_NAME;
+ r = get_user_creds(&nobody, &uid, NULL, NULL, NULL, 0);
+ if (r < 0)
+ uid = 0;
+ } else
+ uid = getuid();
+
+ r = fd_add_uid_acl_permission(fd, uid, ACL_READ);
+ if (ERRNO_IS_NOT_SUPPORTED(r))
+ return log_tests_skipped("no ACL support on /tmp");
+
+ log_info_errno(r, "fd_add_uid_acl_permission(%i, "UID_FMT", ACL_READ): %m", fd, uid);
+ assert_se(r >= 0);
+
+ cmd = strjoina("ls -l ", fn);
+ assert_se(system(cmd) == 0);
+
+ cmd = strjoina("getfacl -p ", fn);
+ assert_se(system(cmd) == 0);
+
+ /* set the acls again */
+
+ r = fd_add_uid_acl_permission(fd, uid, ACL_READ);
+ assert_se(r >= 0);
+
+ cmd = strjoina("ls -l ", fn);
+ assert_se(system(cmd) == 0);
+
+ cmd = strjoina("getfacl -p ", fn);
+ assert_se(system(cmd) == 0);
+
+ return 0;
+}
+
+TEST(fd_acl_make_read_only) {
+ _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-empty.XXXXXX";
+ _cleanup_close_ int fd = -EBADF;
+ const char *cmd;
+ struct stat st;
+
+ fd = mkostemp_safe(fn);
+ assert_se(fd >= 0);
+
+ /* make it more exciting */
+ (void) fd_add_uid_acl_permission(fd, 1, ACL_READ|ACL_WRITE|ACL_EXECUTE);
+
+ assert_se(fstat(fd, &st) >= 0);
+ assert_se((st.st_mode & 0200) == 0200);
+
+ cmd = strjoina("getfacl -p ", fn);
+ assert_se(system(cmd) == 0);
+
+ cmd = strjoina("stat ", fn);
+ assert_se(system(cmd) == 0);
+
+ log_info("read-only");
+ assert_se(fd_acl_make_read_only(fd));
+
+ assert_se(fstat(fd, &st) >= 0);
+ assert_se((st.st_mode & 0222) == 0000);
+
+ cmd = strjoina("getfacl -p ", fn);
+ assert_se(system(cmd) == 0);
+
+ cmd = strjoina("stat ", fn);
+ assert_se(system(cmd) == 0);
+
+ log_info("writable");
+ assert_se(fd_acl_make_writable(fd));
+
+ assert_se(fstat(fd, &st) >= 0);
+ assert_se((st.st_mode & 0222) == 0200);
+
+ cmd = strjoina("getfacl -p ", fn);
+ assert_se(system(cmd) == 0);
+
+ cmd = strjoina("stat ", fn);
+ assert_se(system(cmd) == 0);
+
+ log_info("read-only");
+ assert_se(fd_acl_make_read_only(fd));
+
+ assert_se(fstat(fd, &st) >= 0);
+ assert_se((st.st_mode & 0222) == 0000);
+
+ cmd = strjoina("getfacl -p ", fn);
+ assert_se(system(cmd) == 0);
+
+ cmd = strjoina("stat ", fn);
+ assert_se(system(cmd) == 0);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-af-list.c b/src/test/test-af-list.c
new file mode 100644
index 0000000..45655d7
--- /dev/null
+++ b/src/test/test-af-list.c
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/socket.h>
+
+#include "macro.h"
+#include "string-util.h"
+#include "tests.h"
+
+_unused_
+static const struct af_name* lookup_af(register const char *str, register GPERF_LEN_TYPE len);
+
+#include "af-from-name.h"
+#include "af-list.h"
+#include "af-to-name.h"
+
+TEST(af_list) {
+ for (unsigned i = 0; i < ELEMENTSOF(af_names); i++) {
+ if (af_names[i]) {
+ assert_se(streq(af_to_name(i), af_names[i]));
+ assert_se(af_from_name(af_names[i]) == (int) i);
+ }
+ }
+
+ assert_se(af_to_name(af_max()) == NULL);
+ assert_se(af_to_name(-1) == NULL);
+ assert_se(af_from_name("huddlduddl") == -EINVAL);
+ assert_se(af_from_name("") == -EINVAL);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-alloc-util.c b/src/test/test-alloc-util.c
new file mode 100644
index 0000000..24cb5f7
--- /dev/null
+++ b/src/test/test-alloc-util.c
@@ -0,0 +1,233 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <malloc.h>
+#include <stdint.h>
+
+#include "alloc-util.h"
+#include "macro.h"
+#include "memory-util.h"
+#include "random-util.h"
+#include "tests.h"
+
+TEST(alloca) {
+ static const uint8_t zero[997] = { };
+ char *t;
+
+ t = alloca_align(17, 512);
+ assert_se(!((uintptr_t)t & 0xff));
+ memzero(t, 17);
+
+ t = alloca0_align(997, 1024);
+ assert_se(!((uintptr_t)t & 0x1ff));
+ assert_se(!memcmp(t, zero, 997));
+}
+
+TEST(GREEDY_REALLOC) {
+ _cleanup_free_ int *a = NULL, *b = NULL, *c = NULL;
+ size_t i, j, n_c = 0;
+
+ /* Give valgrind a chance to verify our realloc() operations */
+
+ for (i = 0; i < 20480; i++) {
+ assert_se(GREEDY_REALLOC(a, i + 1));
+ assert_se(MALLOC_ELEMENTSOF(a) >= i + 1);
+ assert_se(MALLOC_SIZEOF_SAFE(a) >= (i + 1) * sizeof(int));
+ a[i] = (int) i;
+ assert_se(GREEDY_REALLOC(a, i / 2));
+ assert_se(MALLOC_ELEMENTSOF(a) >= i / 2);
+ assert_se(MALLOC_SIZEOF_SAFE(a) >= (i / 2) * sizeof(int));
+ }
+
+ for (j = 0; j < i / 2; j++)
+ assert_se(a[j] == (int) j);
+
+ for (i = 30; i < 20480; i += 7) {
+ assert_se(GREEDY_REALLOC(b, i + 1));
+ assert_se(MALLOC_ELEMENTSOF(b) >= i + 1);
+ assert_se(MALLOC_SIZEOF_SAFE(b) >= (i + 1) * sizeof(int));
+ b[i] = (int) i;
+ assert_se(GREEDY_REALLOC(b, i / 2));
+ assert_se(MALLOC_ELEMENTSOF(b) >= i / 2);
+ assert_se(MALLOC_SIZEOF_SAFE(b) >= (i / 2) * sizeof(int));
+ }
+
+ for (j = 30; j < i / 2; j += 7)
+ assert_se(b[j] == (int) j);
+
+ size_t n_from = 10;
+ int from[n_from];
+ for (i = 0; i < 2048; i++) {
+ for (j = 0; j < n_from; j++)
+ from[j] = n_from * i + j;
+
+ _cleanup_free_ int *before = NULL;
+ size_t n_before = 0;
+ assert_se(GREEDY_REALLOC_APPEND(before, n_before, c, n_c));
+ assert_se(before);
+ assert_se(n_before == n_c);
+ assert_se(memcmp_safe(c, before, n_c) == 0);
+
+ assert_se(GREEDY_REALLOC_APPEND(c, n_c, from, n_from));
+ assert_se(n_c == n_before + n_from);
+ assert_se(MALLOC_ELEMENTSOF(c) >= n_c);
+ assert_se(MALLOC_SIZEOF_SAFE(c) >= n_c * sizeof(int));
+ assert_se(memcmp_safe(c, before, n_before) == 0);
+ assert_se(memcmp_safe(&c[n_before], from, n_from) == 0);
+
+ before = mfree(before);
+ assert_se(!before);
+ n_before = 0;
+ assert_se(GREEDY_REALLOC_APPEND(before, n_before, c, n_c));
+ assert_se(before);
+ assert_se(n_before == n_c);
+ assert_se(memcmp_safe(c, before, n_c) == 0);
+
+ assert_se(GREEDY_REALLOC_APPEND(c, n_c, NULL, 0));
+ assert_se(c);
+ assert_se(n_c == n_before);
+ assert_se(MALLOC_ELEMENTSOF(c) >= n_c);
+ assert_se(MALLOC_SIZEOF_SAFE(c) >= n_c * sizeof(int));
+ assert_se(memcmp_safe(c, before, n_c) == 0);
+ }
+
+ for (j = 0; j < i * n_from; j++)
+ assert_se(c[j] == (int) j);
+}
+
+TEST(memdup_multiply_and_greedy_realloc) {
+ static const int org[] = { 1, 2, 3 };
+ _cleanup_free_ int *dup;
+ size_t i;
+ int *p;
+
+ dup = memdup_suffix0_multiply(org, 3, sizeof(int));
+ assert_se(dup);
+ assert_se(dup[0] == 1);
+ assert_se(dup[1] == 2);
+ assert_se(dup[2] == 3);
+ assert_se(((uint8_t*) dup)[sizeof(int) * 3] == 0);
+ free(dup);
+
+ dup = memdup_multiply(org, 3, sizeof(int));
+ assert_se(dup);
+ assert_se(dup[0] == 1);
+ assert_se(dup[1] == 2);
+ assert_se(dup[2] == 3);
+
+ memzero(dup + 3, malloc_usable_size(dup) - sizeof(int) * 3);
+
+ p = dup;
+ assert_se(GREEDY_REALLOC0(dup, 2) == p);
+
+ p = GREEDY_REALLOC0(dup, 10);
+ assert_se(p == dup);
+ assert_se(MALLOC_ELEMENTSOF(p) >= 10);
+ assert_se(p[0] == 1);
+ assert_se(p[1] == 2);
+ assert_se(p[2] == 3);
+ for (i = 3; i < MALLOC_ELEMENTSOF(p); i++)
+ assert_se(p[i] == 0);
+}
+
+TEST(bool_assign) {
+ bool b, c, *cp = &c, d, e, f, g, h;
+
+ b = 123;
+ *cp = -11;
+ d = 0xF & 0xFF;
+ e = b & d;
+ f = 0x0;
+ g = cp; /* cast from pointer */
+ h = NULL; /* cast from pointer */
+
+ assert_se(b);
+ assert_se(c);
+ assert_se(d);
+ assert_se(e);
+ assert_se(!f);
+ assert_se(g);
+ assert_se(!h);
+}
+
+static int cleanup_counter = 0;
+
+static void cleanup1(void *a) {
+ log_info("%s(%p)", __func__, a);
+ assert_se(++cleanup_counter == *(int*) a);
+}
+static void cleanup2(void *a) {
+ log_info("%s(%p)", __func__, a);
+ assert_se(++cleanup_counter == *(int*) a);
+}
+static void cleanup3(void *a) {
+ log_info("%s(%p)", __func__, a);
+ assert_se(++cleanup_counter == *(int*) a);
+}
+
+TEST(cleanup_order) {
+ _cleanup_(cleanup1) int x1 = 4, x2 = 3;
+ _cleanup_(cleanup3) int z = 2;
+ _cleanup_(cleanup2) int y = 1;
+ log_debug("x1: %p", &x1);
+ log_debug("x2: %p", &x2);
+ log_debug("y: %p", &y);
+ log_debug("z: %p", &z);
+}
+
+TEST(auto_erase_memory) {
+ _cleanup_(erase_and_freep) uint8_t *p1, *p2;
+
+ /* print address of p2, else e.g. clang-11 will optimize it out */
+ log_debug("p1: %p p2: %p", &p1, &p2);
+
+ assert_se(p1 = new(uint8_t, 4703)); /* use prime size, to ensure that there will be free space at the
+ * end of the allocation, since malloc() enforces alignment */
+ assert_se(p2 = new(uint8_t, 4703));
+
+ assert_se(crypto_random_bytes(p1, 4703) == 0);
+
+ /* before we exit the scope, do something with this data, so that the compiler won't optimize this away */
+ memcpy(p2, p1, 4703);
+ for (size_t i = 0; i < 4703; i++)
+ assert_se(p1[i] == p2[i]);
+}
+
+#define TEST_SIZES(f, n) \
+ do { \
+ log_debug("requested=%zu vs. malloc_size=%zu vs. gcc_size=%zu", \
+ n * sizeof(*f), \
+ malloc_usable_size(f), \
+ __builtin_object_size(f, 0)); \
+ assert_se(MALLOC_ELEMENTSOF(f) >= n); \
+ assert_se(MALLOC_SIZEOF_SAFE(f) >= sizeof(*f) * n); \
+ assert_se(malloc_usable_size(f) >= sizeof(*f) * n); \
+ assert_se(__builtin_object_size(f, 0) >= sizeof(*f) * n); \
+ } while (false)
+
+TEST(malloc_size_safe) {
+ _cleanup_free_ uint32_t *f = NULL;
+ size_t n = 4711;
+
+ /* Let's check the macros and built-ins work on NULL and return the expected values */
+ assert_se(MALLOC_ELEMENTSOF((float*) NULL) == 0);
+ assert_se(MALLOC_SIZEOF_SAFE((float*) NULL) == 0);
+ assert_se(malloc_usable_size(NULL) == 0); /* as per man page, this is safe and defined */
+ assert_se(__builtin_object_size(NULL, 0) == SIZE_MAX); /* as per docs SIZE_MAX is returned for pointers where the size isn't known */
+
+ /* Then, let's try these macros once with constant size values, so that __builtin_object_size()
+ * definitely can work (as long as -O2 is used when compiling) */
+ assert_se(f = new(uint32_t, n));
+ TEST_SIZES(f, n);
+
+ /* Finally, let's use some dynamically sized allocations, to make sure this doesn't deteriorate */
+ for (unsigned i = 0; i < 50; i++) {
+ _cleanup_free_ uint64_t *g = NULL;
+ size_t m;
+
+ m = random_u64_range(16*1024);
+ assert_se(g = new(uint64_t, m));
+ TEST_SIZES(g, m);
+ }
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-architecture.c b/src/test/test-architecture.c
new file mode 100644
index 0000000..8731e1c
--- /dev/null
+++ b/src/test/test-architecture.c
@@ -0,0 +1,53 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "architecture.h"
+#include "errno-util.h"
+#include "log.h"
+#include "tests.h"
+#include "virt.h"
+
+int main(int argc, char *argv[]) {
+ Virtualization v;
+ Architecture a;
+ const char *p;
+
+ test_setup_logging(LOG_INFO);
+
+ assert_se(architecture_from_string("") < 0);
+ assert_se(architecture_from_string(NULL) < 0);
+ assert_se(architecture_from_string("hoge") < 0);
+ assert_se(architecture_to_string(-1) == NULL);
+ assert_se(architecture_from_string(architecture_to_string(0)) == 0);
+ assert_se(architecture_from_string(architecture_to_string(1)) == 1);
+
+ v = detect_virtualization();
+ if (ERRNO_IS_NEG_PRIVILEGE(v))
+ return log_tests_skipped("Cannot detect virtualization");
+
+ assert_se(v >= 0);
+
+ log_info("virtualization=%s id=%s",
+ VIRTUALIZATION_IS_CONTAINER(v) ? "container" :
+ VIRTUALIZATION_IS_VM(v) ? "vm" : "n/a",
+ virtualization_to_string(v));
+
+ a = uname_architecture();
+ assert_se(a >= 0);
+
+ p = architecture_to_string(a);
+ assert_se(p);
+ log_info("uname architecture=%s", p);
+ assert_se(architecture_from_string(p) == a);
+
+ a = native_architecture();
+ assert_se(a >= 0);
+
+ p = architecture_to_string(a);
+ assert_se(p);
+ log_info("native architecture=%s", p);
+ assert_se(architecture_from_string(p) == a);
+
+ log_info("primary library architecture=" LIB_ARCH_TUPLE);
+
+ return 0;
+}
diff --git a/src/test/test-argv-util.c b/src/test/test-argv-util.c
new file mode 100644
index 0000000..5bf2903
--- /dev/null
+++ b/src/test/test-argv-util.c
@@ -0,0 +1,132 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sched.h>
+#include <unistd.h>
+
+#if HAVE_VALGRIND_VALGRIND_H
+# include <valgrind/valgrind.h>
+#endif
+
+#include "argv-util.h"
+#include "missing_sched.h"
+#include "process-util.h"
+#include "tests.h"
+#include "virt.h"
+
+static void test_rename_process_now(const char *p, int ret) {
+ _cleanup_free_ char *comm = NULL, *cmdline = NULL;
+ int r;
+
+ log_info("/* %s(%s) */", __func__, p);
+
+ r = rename_process(p);
+ assert_se(r == ret ||
+ (ret == 0 && r >= 0) ||
+ (ret > 0 && r > 0));
+
+ log_debug_errno(r, "rename_process(%s): %m", p);
+
+ if (r < 0)
+ return;
+
+#if HAVE_VALGRIND_VALGRIND_H
+ /* see above, valgrind is weird, we can't verify what we are doing here */
+ if (RUNNING_ON_VALGRIND)
+ return;
+#endif
+
+ assert_se(pid_get_comm(0, &comm) >= 0);
+ log_debug("comm = <%s>", comm);
+ assert_se(strneq(comm, p, TASK_COMM_LEN-1));
+ /* We expect comm to be at most 16 bytes (TASK_COMM_LEN). The kernel may raise this limit in the
+ * future. We'd only check the initial part, at least until we recompile, but this will still pass. */
+
+ r = pid_get_cmdline(0, SIZE_MAX, 0, &cmdline);
+ assert_se(r >= 0);
+ /* we cannot expect cmdline to be renamed properly without privileges */
+ if (geteuid() == 0) {
+ if (r == 0 && detect_container() > 0)
+ log_info("cmdline = <%s> (not verified, Running in unprivileged container?)", cmdline);
+ else {
+ log_info("cmdline = <%s> (expected <%.*s>)", cmdline, (int) strlen("test-process-util"), p);
+
+ bool skip = cmdline[0] == '"'; /* A shortcut to check if the string is quoted */
+
+ assert_se(strneq(cmdline + skip, p, strlen("test-process-util")));
+ assert_se(startswith(cmdline + skip, p));
+ }
+ } else
+ log_info("cmdline = <%s> (not verified)", cmdline);
+}
+
+static void test_rename_process_one(const char *p, int ret) {
+ siginfo_t si;
+ pid_t pid;
+
+ log_info("/* %s(%s) */", __func__, p);
+
+ pid = fork();
+ assert_se(pid >= 0);
+
+ if (pid == 0) {
+ /* child */
+ test_rename_process_now(p, ret);
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(wait_for_terminate(pid, &si) >= 0);
+ assert_se(si.si_code == CLD_EXITED);
+ assert_se(si.si_status == EXIT_SUCCESS);
+}
+
+TEST(rename_process_invalid) {
+ assert_se(rename_process(NULL) == -EINVAL);
+ assert_se(rename_process("") == -EINVAL);
+}
+
+TEST(rename_process_multi) {
+ pid_t pid;
+
+ pid = fork();
+ assert_se(pid >= 0);
+
+ if (pid > 0) {
+ siginfo_t si;
+
+ assert_se(wait_for_terminate(pid, &si) >= 0);
+ assert_se(si.si_code == CLD_EXITED);
+ assert_se(si.si_status == EXIT_SUCCESS);
+
+ return;
+ }
+
+ /* child */
+ test_rename_process_now("one", 1);
+ test_rename_process_now("more", 0); /* longer than "one", hence truncated */
+ (void) setresuid(99, 99, 99); /* change uid when running privileged */
+ test_rename_process_now("time!", 0);
+ test_rename_process_now("0", 1); /* shorter than "one", should fit */
+ _exit(EXIT_SUCCESS);
+}
+
+TEST(rename_process) {
+ test_rename_process_one("foo", 1); /* should always fit */
+ test_rename_process_one("this is a really really long process name, followed by some more words", 0); /* unlikely to fit */
+ test_rename_process_one("1234567", 1); /* should always fit */
+}
+
+TEST(argv_help) {
+ assert_se(argv_looks_like_help(1, STRV_MAKE("program")));
+ assert_se(argv_looks_like_help(2, STRV_MAKE("program", "help")));
+ assert_se(argv_looks_like_help(3, STRV_MAKE("program", "arg1", "--help")));
+ assert_se(argv_looks_like_help(4, STRV_MAKE("program", "arg1", "arg2", "-h")));
+ assert_se(!argv_looks_like_help(2, STRV_MAKE("program", "arg1")));
+ assert_se(!argv_looks_like_help(4, STRV_MAKE("program", "arg1", "arg2", "--h")));
+}
+
+static int intro(void) {
+ log_show_color(true);
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_INFO, intro);
diff --git a/src/test/test-arphrd-util.c b/src/test/test-arphrd-util.c
new file mode 100644
index 0000000..d8dd464
--- /dev/null
+++ b/src/test/test-arphrd-util.c
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/if_arp.h>
+
+#include "arphrd-util.h"
+#include "string-util.h"
+#include "tests.h"
+
+TEST(arphrd) {
+ for (int i = 0; i <= ARPHRD_VOID + 1; i++) {
+ const char *name;
+
+ name = arphrd_to_name(i);
+ if (name) {
+ log_info("%i: %s", i, name);
+
+ assert_se(arphrd_from_name(name) == i);
+ }
+ }
+
+ assert_se(arphrd_to_name(ARPHRD_VOID + 1) == NULL);
+ assert_se(arphrd_from_name("huddlduddl") == -EINVAL);
+ assert_se(arphrd_from_name("") == -EINVAL);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-ask-password-api.c b/src/test/test-ask-password-api.c
new file mode 100644
index 0000000..b24159e
--- /dev/null
+++ b/src/test/test-ask-password-api.c
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "ask-password-api.h"
+#include "strv.h"
+#include "tests.h"
+
+TEST(ask_password) {
+ int r;
+ _cleanup_strv_free_ char **ret = NULL;
+
+ r = ask_password_tty(-1, "hello?", "da key", 0, ASK_PASSWORD_CONSOLE_COLOR, NULL, &ret);
+ if (r == -ECANCELED)
+ assert_se(ret == NULL);
+ else {
+ assert_se(r >= 0);
+ assert_se(strv_length(ret) == 1);
+ log_info("Got \"%s\"", *ret);
+ }
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-async.c b/src/test/test-async.c
new file mode 100644
index 0000000..75bc4d8
--- /dev/null
+++ b/src/test/test-async.c
@@ -0,0 +1,100 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <sys/prctl.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "async.h"
+#include "fs-util.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "signal-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+TEST(asynchronous_sync) {
+ assert_se(asynchronous_sync(NULL) >= 0);
+}
+
+TEST(asynchronous_close) {
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-asynchronous_close.XXXXXX";
+ int fd, r;
+
+ fd = mkostemp_safe(name);
+ assert_se(fd >= 0);
+ asynchronous_close(fd);
+
+ sleep(1);
+
+ assert_se(fcntl(fd, F_GETFD) == -1);
+ assert_se(errno == EBADF);
+
+ r = safe_fork("(subreaper)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGKILL|FORK_LOG|FORK_WAIT, NULL);
+ assert(r >= 0);
+
+ if (r == 0) {
+ /* child */
+
+ assert(make_reaper_process(true) >= 0);
+
+ fd = open("/dev/null", O_RDONLY|O_CLOEXEC);
+ assert_se(fd >= 0);
+ asynchronous_close(fd);
+
+ sleep(1);
+
+ assert_se(fcntl(fd, F_GETFD) == -1);
+ assert_se(errno == EBADF);
+
+ _exit(EXIT_SUCCESS);
+ }
+}
+
+TEST(asynchronous_rm_rf) {
+ _cleanup_free_ char *t = NULL, *k = NULL;
+ int r;
+
+ assert_se(mkdtemp_malloc(NULL, &t) >= 0);
+ assert_se(k = path_join(t, "somefile"));
+ assert_se(touch(k) >= 0);
+ assert_se(asynchronous_rm_rf(t, REMOVE_ROOT|REMOVE_PHYSICAL) >= 0);
+
+ /* Do this once more, form a subreaper. Which is nice, because we can watch the async child even
+ * though detached */
+
+ r = safe_fork("(subreaper)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT, NULL);
+ assert_se(r >= 0);
+
+ if (r == 0) {
+ _cleanup_free_ char *tt = NULL, *kk = NULL;
+
+ /* child */
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, -1) >= 0);
+ assert_se(make_reaper_process(true) >= 0);
+
+ assert_se(mkdtemp_malloc(NULL, &tt) >= 0);
+ assert_se(kk = path_join(tt, "somefile"));
+ assert_se(touch(kk) >= 0);
+ assert_se(asynchronous_rm_rf(tt, REMOVE_ROOT|REMOVE_PHYSICAL) >= 0);
+
+ for (;;) {
+ siginfo_t si = {};
+
+ assert_se(waitid(P_ALL, 0, &si, WEXITED) >= 0);
+
+ if (access(tt, F_OK) < 0) {
+ assert_se(errno == ENOENT);
+ break;
+ }
+
+ /* wasn't the rm_rf() call. let's wait longer */
+ }
+
+ _exit(EXIT_SUCCESS);
+ }
+}
+
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-barrier.c b/src/test/test-barrier.c
new file mode 100644
index 0000000..7e8bfc0
--- /dev/null
+++ b/src/test/test-barrier.c
@@ -0,0 +1,441 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+/*
+ * IPC barrier tests
+ * These tests verify the correct behavior of the IPC Barrier implementation.
+ * Note that the tests use alarm-timers to verify dead-locks and timeouts. These
+ * might not work on slow machines where 20ms are too short to perform specific
+ * operations (though, very unlikely). In case that turns out true, we have to
+ * increase it at the slightly cost of lengthen test-duration on other machines.
+ */
+
+#include <stdio.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "barrier.h"
+#include "errno-util.h"
+#include "tests.h"
+#include "time-util.h"
+#include "virt.h"
+
+/* 20ms to test deadlocks; All timings use multiples of this constant as
+ * alarm/sleep timers. If this timeout is too small for slow machines to perform
+ * the requested operations, we have to increase it. On an i7 this works fine
+ * with 1ms base-time, so 20ms should be just fine for everyone. */
+#define BASE_TIME (20 * USEC_PER_MSEC)
+
+static void set_alarm(usec_t usecs) {
+ struct itimerval v = { };
+
+ timeval_store(&v.it_value, usecs);
+ assert_se(setitimer(ITIMER_REAL, &v, NULL) >= 0);
+}
+
+#define TEST_BARRIER(_FUNCTION, _CHILD_CODE, _WAIT_CHILD, _PARENT_CODE, _WAIT_PARENT) \
+ TEST(_FUNCTION) { \
+ Barrier b = BARRIER_NULL; \
+ pid_t pid1, pid2; \
+ \
+ assert_se(barrier_create(&b) >= 0); \
+ assert_se(b.me > 0); \
+ assert_se(b.them > 0); \
+ assert_se(b.pipe[0] > 0); \
+ assert_se(b.pipe[1] > 0); \
+ \
+ pid1 = fork(); \
+ assert_se(pid1 >= 0); \
+ if (pid1 == 0) { \
+ barrier_set_role(&b, BARRIER_CHILD); \
+ { _CHILD_CODE; } \
+ exit(42); \
+ } \
+ \
+ pid2 = fork(); \
+ assert_se(pid2 >= 0); \
+ if (pid2 == 0) { \
+ barrier_set_role(&b, BARRIER_PARENT); \
+ { _PARENT_CODE; } \
+ exit(42); \
+ } \
+ \
+ barrier_destroy(&b); \
+ set_alarm(999999); \
+ { _WAIT_CHILD; } \
+ { _WAIT_PARENT; } \
+ set_alarm(0); \
+ }
+
+#define TEST_BARRIER_WAIT_SUCCESS(_pid) \
+ ({ \
+ int pidr, status; \
+ pidr = waitpid(_pid, &status, 0); \
+ assert_se(pidr == _pid); \
+ assert_se(WIFEXITED(status)); \
+ assert_se(WEXITSTATUS(status) == 42); \
+ })
+
+#define TEST_BARRIER_WAIT_ALARM(_pid) \
+ ({ \
+ int pidr, status; \
+ pidr = waitpid(_pid, &status, 0); \
+ assert_se(pidr == _pid); \
+ assert_se(WIFSIGNALED(status)); \
+ assert_se(WTERMSIG(status) == SIGALRM); \
+ })
+
+/*
+ * Test basic sync points
+ * This places a barrier in both processes and waits synchronously for them.
+ * The timeout makes sure the sync works as expected. The usleep_safe() on one side
+ * makes sure the exit of the parent does not overwrite previous barriers. Due
+ * to the usleep_safe(), we know that the parent already exited, thus there's a
+ * pending HUP on the pipe. However, the barrier_sync() prefers reads on the
+ * eventfd, thus we can safely wait on the barrier.
+ */
+TEST_BARRIER(barrier_sync,
+ ({
+ set_alarm(BASE_TIME * 10);
+ assert_se(barrier_place(&b));
+ usleep_safe(BASE_TIME * 2);
+ assert_se(barrier_sync(&b));
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid1),
+ ({
+ set_alarm(BASE_TIME * 10);
+ assert_se(barrier_place(&b));
+ assert_se(barrier_sync(&b));
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid2));
+
+/*
+ * Test wait_next()
+ * This places a barrier in the parent and syncs on it. The child sleeps while
+ * the parent places the barrier and then waits for a barrier. The wait will
+ * succeed as the child hasn't read the parent's barrier, yet. The following
+ * barrier and sync synchronize the exit.
+ */
+TEST_BARRIER(barrier_wait_next,
+ ({
+ usleep_safe(BASE_TIME);
+ set_alarm(BASE_TIME * 10);
+ assert_se(barrier_wait_next(&b));
+ assert_se(barrier_place(&b));
+ assert_se(barrier_sync(&b));
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid1),
+ ({
+ set_alarm(BASE_TIME * 4);
+ assert_se(barrier_place(&b));
+ assert_se(barrier_sync(&b));
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid2));
+
+/*
+ * Test wait_next() multiple times
+ * This places two barriers in the parent and waits for the child to exit. The
+ * child sleeps 20ms so both barriers _should_ be in place. It then waits for
+ * the parent to place the next barrier twice. The first call will fetch both
+ * barriers and return. However, the second call will stall as the parent does
+ * not place a 3rd barrier (the sleep caught two barriers). wait_next() is does
+ * not look at barrier-links so this stall is expected. Thus this test times
+ * out.
+ */
+TEST_BARRIER(barrier_wait_next_twice,
+ ({
+ usleep_safe(BASE_TIME);
+ set_alarm(BASE_TIME);
+ assert_se(barrier_wait_next(&b));
+ assert_se(barrier_wait_next(&b));
+ assert_se(0);
+ }),
+ TEST_BARRIER_WAIT_ALARM(pid1),
+ ({
+ set_alarm(BASE_TIME * 10);
+ assert_se(barrier_place(&b));
+ assert_se(barrier_place(&b));
+ usleep_safe(BASE_TIME * 4);
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid2));
+
+/*
+ * Test wait_next() with local barriers
+ * This is the same as test_barrier_wait_next_twice, but places local barriers
+ * between both waits. This does not have any effect on the wait so it times out
+ * like the other test.
+ */
+TEST_BARRIER(barrier_wait_next_twice_local,
+ ({
+ usleep_safe(BASE_TIME);
+ set_alarm(BASE_TIME);
+ assert_se(barrier_wait_next(&b));
+ assert_se(barrier_place(&b));
+ assert_se(barrier_place(&b));
+ assert_se(barrier_wait_next(&b));
+ assert_se(0);
+ }),
+ TEST_BARRIER_WAIT_ALARM(pid1),
+ ({
+ set_alarm(BASE_TIME * 10);
+ assert_se(barrier_place(&b));
+ assert_se(barrier_place(&b));
+ usleep_safe(BASE_TIME * 4);
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid2));
+
+/*
+ * Test wait_next() with sync_next()
+ * This is again the same as test_barrier_wait_next_twice but uses a
+ * synced wait as the second wait. This works just fine because the local state
+ * has no barriers placed, therefore, the remote is always in sync.
+ */
+TEST_BARRIER(barrier_wait_next_twice_sync,
+ ({
+ usleep_safe(BASE_TIME);
+ set_alarm(BASE_TIME);
+ assert_se(barrier_wait_next(&b));
+ assert_se(barrier_sync_next(&b));
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid1),
+ ({
+ set_alarm(BASE_TIME * 10);
+ assert_se(barrier_place(&b));
+ assert_se(barrier_place(&b));
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid2));
+
+/*
+ * Test wait_next() with sync_next() and local barriers
+ * This is again the same as test_barrier_wait_next_twice_local but uses a
+ * synced wait as the second wait. This works just fine because the local state
+ * is in sync with the remote.
+ */
+TEST_BARRIER(barrier_wait_next_twice_local_sync,
+ ({
+ usleep_safe(BASE_TIME);
+ set_alarm(BASE_TIME);
+ assert_se(barrier_wait_next(&b));
+ assert_se(barrier_place(&b));
+ assert_se(barrier_place(&b));
+ assert_se(barrier_sync_next(&b));
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid1),
+ ({
+ set_alarm(BASE_TIME * 10);
+ assert_se(barrier_place(&b));
+ assert_se(barrier_place(&b));
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid2));
+
+/*
+ * Test sync_next() and sync()
+ * This tests sync_*() synchronizations and makes sure they work fine if the
+ * local state is behind the remote state.
+ */
+TEST_BARRIER(barrier_sync_next,
+ ({
+ set_alarm(BASE_TIME * 10);
+ assert_se(barrier_sync_next(&b));
+ assert_se(barrier_sync(&b));
+ assert_se(barrier_place(&b));
+ assert_se(barrier_place(&b));
+ assert_se(barrier_sync_next(&b));
+ assert_se(barrier_sync_next(&b));
+ assert_se(barrier_sync(&b));
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid1),
+ ({
+ set_alarm(BASE_TIME * 10);
+ usleep_safe(BASE_TIME);
+ assert_se(barrier_place(&b));
+ assert_se(barrier_place(&b));
+ assert_se(barrier_sync(&b));
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid2));
+
+/*
+ * Test sync_next() and sync() with local barriers
+ * This tests timeouts if sync_*() is used if local barriers are placed but the
+ * remote didn't place any.
+ */
+TEST_BARRIER(barrier_sync_next_local,
+ ({
+ set_alarm(BASE_TIME);
+ assert_se(barrier_place(&b));
+ assert_se(barrier_sync_next(&b));
+ assert_se(0);
+ }),
+ TEST_BARRIER_WAIT_ALARM(pid1),
+ ({
+ usleep_safe(BASE_TIME * 2);
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid2));
+
+/*
+ * Test sync_next() and sync() with local barriers and abortion
+ * This is the same as test_barrier_sync_next_local but aborts the sync in the
+ * parent. Therefore, the sync_next() succeeds just fine due to the abortion.
+ */
+TEST_BARRIER(barrier_sync_next_local_abort,
+ ({
+ set_alarm(BASE_TIME * 10);
+ assert_se(barrier_place(&b));
+ assert_se(!barrier_sync_next(&b));
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid1),
+ ({
+ assert_se(barrier_abort(&b));
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid2));
+
+/*
+ * Test matched wait_abortion()
+ * This runs wait_abortion() with remote abortion.
+ */
+TEST_BARRIER(barrier_wait_abortion,
+ ({
+ set_alarm(BASE_TIME * 10);
+ assert_se(barrier_wait_abortion(&b));
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid1),
+ ({
+ assert_se(barrier_abort(&b));
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid2));
+
+/*
+ * Test unmatched wait_abortion()
+ * This runs wait_abortion() without any remote abortion going on. It thus must
+ * timeout.
+ */
+TEST_BARRIER(barrier_wait_abortion_unmatched,
+ ({
+ set_alarm(BASE_TIME);
+ assert_se(barrier_wait_abortion(&b));
+ assert_se(0);
+ }),
+ TEST_BARRIER_WAIT_ALARM(pid1),
+ ({
+ usleep_safe(BASE_TIME * 2);
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid2));
+
+/*
+ * Test matched wait_abortion() with local abortion
+ * This runs wait_abortion() with local and remote abortion.
+ */
+TEST_BARRIER(barrier_wait_abortion_local,
+ ({
+ set_alarm(BASE_TIME * 10);
+ assert_se(barrier_abort(&b));
+ assert_se(!barrier_wait_abortion(&b));
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid1),
+ ({
+ assert_se(barrier_abort(&b));
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid2));
+
+/*
+ * Test unmatched wait_abortion() with local abortion
+ * This runs wait_abortion() with only local abortion. This must time out.
+ */
+TEST_BARRIER(barrier_wait_abortion_local_unmatched,
+ ({
+ set_alarm(BASE_TIME);
+ assert_se(barrier_abort(&b));
+ assert_se(!barrier_wait_abortion(&b));
+ assert_se(0);
+ }),
+ TEST_BARRIER_WAIT_ALARM(pid1),
+ ({
+ usleep_safe(BASE_TIME * 2);
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid2));
+
+/*
+ * Test child exit
+ * Place barrier and sync with the child. The child only exits()s, which should
+ * cause an implicit abortion and wake the parent.
+ */
+TEST_BARRIER(barrier_exit,
+ ({
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid1),
+ ({
+ set_alarm(BASE_TIME * 10);
+ assert_se(barrier_place(&b));
+ assert_se(!barrier_sync(&b));
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid2));
+
+/*
+ * Test child exit with sleep
+ * Same as test_barrier_exit but verifies the test really works due to the
+ * child-exit. We add a usleep_safe() which triggers the alarm in the parent and
+ * causes the test to time out.
+ */
+TEST_BARRIER(barrier_no_exit,
+ ({
+ usleep_safe(BASE_TIME * 2);
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid1),
+ ({
+ set_alarm(BASE_TIME);
+ assert_se(barrier_place(&b));
+ assert_se(!barrier_sync(&b));
+ }),
+ TEST_BARRIER_WAIT_ALARM(pid2));
+
+/*
+ * Test pending exit against sync
+ * The parent places a barrier *and* exits. The 20ms wait in the child
+ * guarantees both are pending. However, our logic prefers pending barriers over
+ * pending exit-abortions (unlike normal abortions), thus the wait_next() must
+ * succeed, same for the sync_next() as our local barrier-count is smaller than
+ * the remote. Once we place a barrier our count is equal, so the sync still
+ * succeeds. Only if we place one more barrier, we're ahead of the remote, thus
+ * we will fail due to HUP on the pipe.
+ */
+TEST_BARRIER(barrier_pending_exit,
+ ({
+ set_alarm(BASE_TIME * 4);
+ usleep_safe(BASE_TIME * 2);
+ assert_se(barrier_wait_next(&b));
+ assert_se(barrier_sync_next(&b));
+ assert_se(barrier_place(&b));
+ assert_se(barrier_sync_next(&b));
+ assert_se(barrier_place(&b));
+ assert_se(!barrier_sync_next(&b));
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid1),
+ ({
+ assert_se(barrier_place(&b));
+ }),
+ TEST_BARRIER_WAIT_SUCCESS(pid2));
+
+
+static int intro(void) {
+ if (!slow_tests_enabled())
+ return log_tests_skipped("slow tests are disabled");
+
+ /*
+ * This test uses real-time alarms and sleeps to test for CPU races explicitly. This is highly
+ * fragile if your system is under load. We already increased the BASE_TIME value to make the tests
+ * more robust, but that just makes the test take significantly longer. Given the recent issues when
+ * running the test in a virtualized environments, limit it to bare metal machines only, to minimize
+ * false-positives in CIs.
+ */
+
+ Virtualization v = detect_virtualization();
+ if (ERRNO_IS_NEG_PRIVILEGE(v))
+ return log_tests_skipped("Cannot detect virtualization");
+
+ if (v != VIRTUALIZATION_NONE)
+ return log_tests_skipped("This test requires a baremetal machine");
+
+ return EXIT_SUCCESS;
+ }
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_INFO, intro);
diff --git a/src/test/test-bitfield.c b/src/test/test-bitfield.c
new file mode 100644
index 0000000..f26b423
--- /dev/null
+++ b/src/test/test-bitfield.c
@@ -0,0 +1,230 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stddef.h>
+
+#include "bitfield.h"
+#include "log.h"
+#include "tests.h"
+
+#define TEST_BITS(bits, v, ...) \
+ ({ \
+ assert_se((!!BITS_SET(bits, ##__VA_ARGS__)) == v); \
+ assert_se((!!BITS_SET(~(bits), ##__VA_ARGS__)) == !v); \
+ })
+#define TEST_BIT(bits, v, i) \
+ ({ \
+ assert_se((!!BIT_SET(bits, i)) == v); \
+ assert_se((!!BIT_SET(~(bits), i)) == !v); \
+ TEST_BITS(bits, v, i); \
+ })
+
+#define TEST_BIT_SET(bits, i) TEST_BIT(bits, 1, i)
+#define TEST_BIT_CLEAR(bits, i) TEST_BIT(bits, 0, i)
+
+#define TEST_BITS_SET(bits, ...) TEST_BITS(bits, 1, ##__VA_ARGS__)
+#define TEST_BITS_CLEAR(bits, ...) TEST_BITS(bits, 0, ##__VA_ARGS__)
+
+TEST(bits) {
+ int count;
+
+ /* Test uint8_t */
+ TEST_BIT_SET(0x81, 0);
+ TEST_BIT_SET(0x81, 7);
+ TEST_BITS_SET(0x81, 0, 7);
+ TEST_BIT_CLEAR(0x81, 4);
+ TEST_BIT_CLEAR(0x81, 6);
+ TEST_BITS_CLEAR(0x81, 1, 2, 3, 4, 5, 6);
+ uint8_t expected8 = 0;
+ BIT_FOREACH(i, 0x81)
+ expected8 |= UINT8_C(1) << i;
+ assert_se(expected8 == 0x81);
+ uint8_t u8 = 0x91;
+ TEST_BIT_SET(u8, 4);
+ TEST_BITS_SET(u8, 0, 4, 7);
+ TEST_BIT_CLEAR(u8, 2);
+ TEST_BITS_CLEAR(u8, 1, 2, 3, 5, 6);
+ SET_BIT(u8, 1);
+ TEST_BITS_SET(u8, 0, 1, 4, 7);
+ TEST_BITS_CLEAR(u8, 2, 3, 5, 6);
+ SET_BITS(u8, 3, 5);
+ TEST_BITS_SET(u8, 0, 1, 3, 4, 5, 7);
+ TEST_BITS_CLEAR(u8, 2, 6);
+ CLEAR_BIT(u8, 4);
+ TEST_BITS_SET(u8, 0, 1, 3, 5, 7);
+ TEST_BITS_CLEAR(u8, 2, 4, 6);
+ CLEAR_BITS(u8, 1);
+ CLEAR_BITS(u8, 0, 7);
+ TEST_BITS_SET(u8, 3, 5);
+ TEST_BITS_CLEAR(u8, 0, 1, 2, 4, 6, 7);
+ expected8 = 0;
+ BIT_FOREACH(i, u8)
+ expected8 |= UINT8_C(1) << i;
+ assert_se(expected8 == u8);
+ u8 = 0;
+ TEST_BITS_CLEAR(u8, 0, 1, 2, 3, 4, 5, 6, 7);
+ BIT_FOREACH(i, u8)
+ assert_se(0);
+ u8 = ~u8;
+ TEST_BITS_SET(u8, 0, 1, 2, 3, 4, 5, 6, 7);
+ count = 0;
+ BIT_FOREACH(i, u8)
+ count++;
+ assert_se(count == 8);
+ uint8_t _u8 = u8;
+ SET_BITS(u8);
+ assert_se(_u8 == u8);
+ CLEAR_BITS(u8);
+ assert_se(_u8 == u8);
+
+ /* Test uint16_t */
+ TEST_BIT_SET(0x1f81, 10);
+ TEST_BITS_SET(0x1f81, 0, 7, 8, 9, 10, 11, 12);
+ TEST_BIT_CLEAR(0x1f81, 13);
+ TEST_BITS_CLEAR(0x1f81, 1, 2, 3, 4, 5, 6, 13, 14, 15);
+ uint16_t expected16 = 0;
+ BIT_FOREACH(i, 0x1f81)
+ expected16 |= UINT16_C(1) << i;
+ assert_se(expected16 == 0x1f81);
+ uint16_t u16 = 0xf060;
+ TEST_BIT_SET(u16, 12);
+ TEST_BITS_SET(u16, 5, 6, 12, 13, 14, 15);
+ TEST_BIT_CLEAR(u16, 9);
+ TEST_BITS_CLEAR(u16, 0, 1, 2, 3, 4, 7, 8, 9, 10, 11);
+ SET_BITS(u16, 1, 8);
+ TEST_BITS_SET(u16, 1, 5, 6, 8, 12, 13, 14, 15);
+ TEST_BITS_CLEAR(u16, 0, 2, 3, 4, 7, 9, 10, 11);
+ CLEAR_BITS(u16, 13, 14);
+ TEST_BITS_SET(u16, 1, 5, 6, 8, 12, 15);
+ TEST_BITS_CLEAR(u16, 0, 2, 3, 4, 7, 9, 10, 11, 13, 14);
+ expected16 = 0;
+ BIT_FOREACH(i, u16)
+ expected16 |= UINT16_C(1) << i;
+ assert_se(expected16 == u16);
+ u16 = 0;
+ TEST_BITS_CLEAR(u16, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15);
+ BIT_FOREACH(i, u16)
+ assert_se(0);
+ u16 = ~u16;
+ TEST_BITS_SET(u16, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15);
+ count = 0;
+ BIT_FOREACH(i, u16)
+ count++;
+ assert_se(count == 16);
+ uint16_t _u16 = u16;
+ SET_BITS(u16);
+ assert_se(_u16 == u16);
+ CLEAR_BITS(u16);
+ assert_se(_u16 == u16);
+
+ /* Test uint32_t */
+ TEST_BIT_SET(0x80224f10, 11);
+ TEST_BITS_SET(0x80224f10, 4, 8, 9, 10, 11, 14, 17, 21, 31);
+ TEST_BIT_CLEAR(0x80224f10, 28);
+ TEST_BITS_CLEAR(0x80224f10, 0, 1, 2, 3, 5, 6, 7, 12, 13, 15, 16, 18, 19, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30);
+ uint32_t expected32 = 0;
+ BIT_FOREACH(i, 0x80224f10)
+ expected32 |= UINT32_C(1) << i;
+ assert_se(expected32 == 0x80224f10);
+ uint32_t u32 = 0x605e0388;
+ TEST_BIT_SET(u32, 3);
+ TEST_BIT_SET(u32, 30);
+ TEST_BITS_SET(u32, 3, 7, 8, 9, 17, 18, 19, 20, 22, 29, 30);
+ TEST_BIT_CLEAR(u32, 0);
+ TEST_BIT_CLEAR(u32, 31);
+ TEST_BITS_CLEAR(u32, 0, 1, 2, 4, 5, 6, 10, 11, 12, 13, 14, 15, 16, 21, 23, 24, 25, 26, 27, 28, 31);
+ SET_BITS(u32, 1, 25, 26);
+ TEST_BITS_SET(u32, 1, 3, 7, 8, 9, 17, 18, 19, 20, 22, 25, 26, 29, 30);
+ TEST_BITS_CLEAR(u32, 0, 2, 4, 5, 6, 10, 11, 12, 13, 14, 15, 16, 21, 23, 24, 27, 28, 31);
+ CLEAR_BITS(u32, 29, 17, 1);
+ TEST_BITS_SET(u32, 3, 7, 8, 9, 18, 19, 20, 22, 25, 26, 30);
+ TEST_BITS_CLEAR(u32, 0, 1, 2, 4, 5, 6, 10, 11, 12, 13, 14, 15, 16, 17, 21, 23, 24, 27, 28, 29, 31);
+ expected32 = 0;
+ BIT_FOREACH(i, u32)
+ expected32 |= UINT32_C(1) << i;
+ assert_se(expected32 == u32);
+ u32 = 0;
+ TEST_BITS_CLEAR(u32, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31);
+ BIT_FOREACH(i, u32)
+ assert_se(0);
+ u32 = ~u32;
+ TEST_BITS_SET(u32, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31);
+ count = 0;
+ BIT_FOREACH(i, u32)
+ count++;
+ assert_se(count == 32);
+ uint32_t _u32 = u32;
+ SET_BITS(u32);
+ assert_se(_u32 == u32);
+ CLEAR_BITS(u32);
+ assert_se(_u32 == u32);
+
+ /* Test uint64_t */
+ TEST_BIT_SET(0x18ba1400f4857460, 60);
+ TEST_BITS_SET(0x18ba1400f4857460, 5, 6, 10, 12, 13, 14, 16, 18, 23, 26, 28, 29, 30, 31, 42, 44, 49, 51, 52, 53, 55, 59, 60);
+ TEST_BIT_CLEAR(UINT64_C(0x18ba1400f4857460), 0);
+ TEST_BIT_CLEAR(UINT64_C(0x18ba1400f4857460), 63);
+ TEST_BITS_CLEAR(UINT64_C(0x18ba1400f4857460), 0, 1, 2, 3, 4, 7, 8, 9, 11, 15, 17, 19, 20, 21, 22, 24, 25, 27, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 43, 45, 46, 47, 48, 50, 54, 56, 57, 58, 61, 62, 63);
+ uint64_t expected64 = 0;
+ BIT_FOREACH(i, 0x18ba1400f4857460)
+ expected64 |= UINT64_C(1) << i;
+ assert_se(expected64 == 0x18ba1400f4857460);
+ uint64_t u64 = 0xa90e2d8507a65739;
+ TEST_BIT_SET(u64, 0);
+ TEST_BIT_SET(u64, 63);
+ TEST_BITS_SET(u64, 0, 3, 4, 5, 8, 9, 10, 12, 14, 17, 18, 21, 23, 24, 25, 26, 32, 34, 39, 40, 42, 43, 45, 49, 50, 51, 56, 59, 61, 63);
+ TEST_BIT_CLEAR(u64, 1);
+ TEST_BITS_CLEAR(u64, 1, 2, 6, 7, 11, 13, 15, 16, 19, 20, 22, 27, 28, 29, 30, 31, 33, 35, 36, 37, 38, 41, 44, 46, 47, 48, 52, 53, 54, 55, 57, 58, 60, 62);
+ SET_BIT(u64, 1);
+ TEST_BITS_SET(u64, 0, 1, 3, 4, 5, 8, 9, 10, 12, 14, 17, 18, 21, 23, 24, 25, 26, 32, 34, 39, 40, 42, 43, 45, 49, 50, 51, 56, 59, 61, 63);
+ TEST_BITS_CLEAR(u64, 2, 6, 7, 11, 13, 15, 16, 19, 20, 22, 27, 28, 29, 30, 31, 33, 35, 36, 37, 38, 41, 44, 46, 47, 48, 52, 53, 54, 55, 57, 58, 60, 62);
+ CLEAR_BIT(u64, 63);
+ TEST_BITS_SET(u64, 0, 1, 3, 4, 5, 8, 9, 10, 12, 14, 17, 18, 21, 23, 24, 25, 26, 32, 34, 39, 40, 42, 43, 45, 49, 50, 51, 56, 59, 61);
+ TEST_BITS_CLEAR(u64, 2, 6, 7, 11, 13, 15, 16, 19, 20, 22, 27, 28, 29, 30, 31, 33, 35, 36, 37, 38, 41, 44, 46, 47, 48, 52, 53, 54, 55, 57, 58, 60, 62, 63);
+ SET_BIT(u64, 62);
+ TEST_BITS_SET(u64, 0, 1, 3, 4, 5, 8, 9, 10, 12, 14, 17, 18, 21, 23, 24, 25, 26, 32, 34, 39, 40, 42, 43, 45, 49, 50, 51, 56, 59, 61, 62);
+ TEST_BITS_CLEAR(u64, 2, 6, 7, 11, 13, 15, 16, 19, 20, 22, 27, 28, 29, 30, 31, 33, 35, 36, 37, 38, 41, 44, 46, 47, 48, 52, 53, 54, 55, 57, 58, 60, 63);
+ SET_BITS(u64, 63, 62, 7, 13, 38, 40);
+ TEST_BITS_SET(u64, 0, 1, 3, 4, 5, 7, 8, 9, 10, 12, 13, 14, 17, 18, 21, 23, 24, 25, 26, 32, 34, 38, 39, 40, 42, 43, 45, 49, 50, 51, 56, 59, 61, 62, 63);
+ TEST_BITS_CLEAR(u64, 2, 6, 11, 15, 16, 19, 20, 22, 27, 28, 29, 30, 31, 33, 35, 36, 37, 41, 44, 46, 47, 48, 52, 53, 54, 55, 57, 58, 60);
+ CLEAR_BIT(u64, 32);
+ TEST_BITS_SET(u64, 0, 1, 3, 4, 5, 7, 8, 9, 10, 12, 13, 14, 17, 18, 21, 23, 24, 25, 26, 34, 38, 39, 40, 42, 43, 45, 49, 50, 51, 56, 59, 61, 62, 63);
+ TEST_BITS_CLEAR(u64, 2, 6, 11, 15, 16, 19, 20, 22, 27, 28, 29, 30, 31, 32, 33, 35, 36, 37, 41, 44, 46, 47, 48, 52, 53, 54, 55, 57, 58, 60);
+ CLEAR_BITS(u64, 0, 2, 11, 63, 32, 58);
+ TEST_BITS_SET(u64, 1, 3, 4, 5, 7, 8, 9, 10, 12, 13, 14, 17, 18, 21, 23, 24, 25, 26, 34, 38, 39, 40, 42, 43, 45, 49, 50, 51, 56, 59, 61, 62);
+ TEST_BITS_CLEAR(u64, 0, 2, 6, 11, 15, 16, 19, 20, 22, 27, 28, 29, 30, 31, 32, 33, 35, 36, 37, 41, 44, 46, 47, 48, 52, 53, 54, 55, 57, 58, 60, 63);
+ expected64 = 0;
+ BIT_FOREACH(i, u64)
+ expected64 |= UINT64_C(1) << i;
+ assert_se(expected64 == u64);
+ u64 = 0;
+ TEST_BITS_CLEAR(u64, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63);
+ BIT_FOREACH(i, u64)
+ assert_se(0);
+ u64 = ~u64;
+ TEST_BITS_SET(u64, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63);
+ count = 0;
+ BIT_FOREACH(i, u64)
+ count++;
+ assert_se(count == 64);
+ uint64_t _u64 = u64;
+ SET_BITS(u64);
+ assert_se(_u64 == u64);
+ CLEAR_BITS(u64);
+ assert_se(_u64 == u64);
+
+ /* Verify these use cases are constant-folded. */
+#if !defined(__clang__) || (__clang_major__ >= 13)
+ /* Clang 11 and 12 (and possibly older) do not grok those; skip them. */
+ assert_cc(__builtin_constant_p(INDEX_TO_MASK(uint8_t, 1)));
+ assert_cc(__builtin_constant_p(INDEX_TO_MASK(uint16_t, 1)));
+ assert_cc(__builtin_constant_p(INDEX_TO_MASK(uint32_t, 1)));
+ assert_cc(__builtin_constant_p(INDEX_TO_MASK(uint64_t, 1)));
+
+ assert_cc(__builtin_constant_p(BIT_SET((uint8_t)2, 1)));
+ assert_cc(__builtin_constant_p(BIT_SET((uint16_t)2, 1)));
+ assert_cc(__builtin_constant_p(BIT_SET((uint32_t)2, 1)));
+ assert_cc(__builtin_constant_p(BIT_SET((uint64_t)2, 1)));
+#endif
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-bitmap.c b/src/test/test-bitmap.c
new file mode 100644
index 0000000..8acf833
--- /dev/null
+++ b/src/test/test-bitmap.c
@@ -0,0 +1,116 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "bitmap.h"
+#include "tests.h"
+
+int main(int argc, const char *argv[]) {
+ _cleanup_bitmap_free_ Bitmap *b = NULL, *b2 = NULL;
+ unsigned n = UINT_MAX, i = 0;
+
+ test_setup_logging(LOG_DEBUG);
+
+ b = bitmap_new();
+ assert_se(b);
+
+ assert_se(bitmap_ensure_allocated(&b) == 0);
+ b = bitmap_free(b);
+ assert_se(bitmap_ensure_allocated(&b) == 0);
+
+ assert_se(bitmap_isset(b, 0) == false);
+ assert_se(bitmap_isset(b, 1) == false);
+ assert_se(bitmap_isset(b, 256) == false);
+ assert_se(bitmap_isclear(b) == true);
+
+ assert_se(bitmap_set(b, 0) == 0);
+ assert_se(bitmap_isset(b, 0) == true);
+ assert_se(bitmap_isclear(b) == false);
+ bitmap_unset(b, 0);
+ assert_se(bitmap_isset(b, 0) == false);
+ assert_se(bitmap_isclear(b) == true);
+
+ assert_se(bitmap_set(b, 1) == 0);
+ assert_se(bitmap_isset(b, 1) == true);
+ assert_se(bitmap_isclear(b) == false);
+ bitmap_unset(b, 1);
+ assert_se(bitmap_isset(b, 1) == false);
+ assert_se(bitmap_isclear(b) == true);
+
+ assert_se(bitmap_set(b, 256) == 0);
+ assert_se(bitmap_isset(b, 256) == true);
+ assert_se(bitmap_isclear(b) == false);
+ bitmap_unset(b, 256);
+ assert_se(bitmap_isset(b, 256) == false);
+ assert_se(bitmap_isclear(b) == true);
+
+ assert_se(bitmap_set(b, 32) == 0);
+ bitmap_unset(b, 0);
+ assert_se(bitmap_isset(b, 32) == true);
+ bitmap_unset(b, 32);
+
+ BITMAP_FOREACH(n, NULL)
+ assert_not_reached();
+
+ assert_se(bitmap_set(b, 0) == 0);
+ assert_se(bitmap_set(b, 1) == 0);
+ assert_se(bitmap_set(b, 256) == 0);
+
+ BITMAP_FOREACH(n, b) {
+ assert_se(n == i);
+ if (i == 0)
+ i = 1;
+ else if (i == 1)
+ i = 256;
+ else if (i == 256)
+ i = UINT_MAX;
+ }
+
+ assert_se(i == UINT_MAX);
+
+ i = 0;
+
+ BITMAP_FOREACH(n, b) {
+ assert_se(n == i);
+ if (i == 0)
+ i = 1;
+ else if (i == 1)
+ i = 256;
+ else if (i == 256)
+ i = UINT_MAX;
+ }
+
+ assert_se(i == UINT_MAX);
+
+ b2 = bitmap_copy(b);
+ assert_se(b2);
+ assert_se(bitmap_equal(b, b2) == true);
+ assert_se(bitmap_equal(b, b) == true);
+ assert_se(bitmap_equal(b, NULL) == false);
+ assert_se(bitmap_equal(NULL, b) == false);
+ assert_se(bitmap_equal(NULL, NULL) == true);
+
+ bitmap_clear(b);
+ assert_se(bitmap_isclear(b) == true);
+ assert_se(bitmap_equal(b, b2) == false);
+ b2 = bitmap_free(b2);
+
+ assert_se(bitmap_set(b, UINT_MAX) == -ERANGE);
+
+ b = bitmap_free(b);
+ assert_se(bitmap_ensure_allocated(&b) == 0);
+ assert_se(bitmap_ensure_allocated(&b2) == 0);
+
+ assert_se(bitmap_equal(b, b2));
+ assert_se(bitmap_set(b, 0) == 0);
+ bitmap_unset(b, 0);
+ assert_se(bitmap_equal(b, b2));
+
+ assert_se(bitmap_set(b, 1) == 0);
+ bitmap_clear(b);
+ assert_se(bitmap_equal(b, b2));
+
+ assert_se(bitmap_set(b, 0) == 0);
+ assert_se(bitmap_set(b2, 0) == 0);
+ assert_se(bitmap_equal(b, b2));
+
+ return 0;
+}
diff --git a/src/test/test-blockdev-util.c b/src/test/test-blockdev-util.c
new file mode 100644
index 0000000..134386c
--- /dev/null
+++ b/src/test/test-blockdev-util.c
@@ -0,0 +1,41 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "blockdev-util.h"
+#include "errno-util.h"
+#include "tests.h"
+
+static void test_path_is_encrypted_one(const char *p, int expect) {
+ int r;
+
+ r = path_is_encrypted(p);
+ if (IN_SET(r, -ENOENT, -ELOOP) || ERRNO_IS_NEG_PRIVILEGE(r))
+ /* This might fail, if btrfs is used and we run in a container. In that case we cannot
+ * resolve the device node paths that BTRFS_IOC_DEV_INFO returns, because the device nodes
+ * are unlikely to exist in the container. But if we can't stat() them we cannot determine
+ * the dev_t of them, and thus cannot figure out if they are encrypted. Hence let's just
+ * ignore ENOENT here. Also skip the test if we lack privileges.
+ * ELOOP might happen if the mount point is a symlink, as seen with under
+ * some rpm-ostree distros */
+ return;
+ assert_se(r >= 0);
+
+ log_info("%s encrypted: %s", p, yes_no(r));
+
+ assert_se(expect < 0 || ((r > 0) == (expect > 0)));
+}
+
+TEST(path_is_encrypted) {
+ int booted = sd_booted(); /* If this is run in build environments such as koji, /dev/ might be a
+ * regular fs. Don't assume too much if not running under systemd. */
+
+ log_info("/* %s (sd_booted=%d) */", __func__, booted);
+
+ test_path_is_encrypted_one("/home", -1);
+ test_path_is_encrypted_one("/var", -1);
+ test_path_is_encrypted_one("/", -1);
+ test_path_is_encrypted_one("/proc", false);
+ test_path_is_encrypted_one("/sys", false);
+ test_path_is_encrypted_one("/dev", booted > 0 ? false : -1);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-boot-timestamps.c b/src/test/test-boot-timestamps.c
new file mode 100644
index 0000000..c3e4876
--- /dev/null
+++ b/src/test/test-boot-timestamps.c
@@ -0,0 +1,89 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "acpi-fpdt.h"
+#include "boot-timestamps.h"
+#include "efi-loader.h"
+#include "errno-util.h"
+#include "log.h"
+#include "tests.h"
+
+static int test_acpi_fpdt(void) {
+ usec_t loader_start, loader_exit;
+ int r;
+
+ r = acpi_get_boot_usec(&loader_start, &loader_exit);
+ if (r < 0) {
+ bool ok = IN_SET(r, -ENOENT, -ENODATA, -ERANGE) || ERRNO_IS_PRIVILEGE(r);
+
+ log_full_errno(ok ? LOG_DEBUG : LOG_ERR, r, "Failed to read ACPI FPDT: %m");
+ return ok ? 0 : r;
+ }
+
+ log_info("ACPI FPDT: loader start=%s exit=%s duration=%s",
+ FORMAT_TIMESPAN(loader_start, USEC_PER_MSEC),
+ FORMAT_TIMESPAN(loader_exit, USEC_PER_MSEC),
+ FORMAT_TIMESPAN(loader_exit - loader_start, USEC_PER_MSEC));
+ return 1;
+}
+
+static int test_efi_loader(void) {
+ usec_t loader_start, loader_exit;
+ int r;
+
+ r = efi_loader_get_boot_usec(&loader_start, &loader_exit);
+ if (r < 0) {
+ bool ok = IN_SET(r, -ENOENT, -EOPNOTSUPP) || ERRNO_IS_PRIVILEGE(r);
+
+ log_full_errno(ok ? LOG_DEBUG : LOG_ERR, r, "Failed to read EFI loader data: %m");
+ return ok ? 0 : r;
+ }
+
+ log_info("EFI Loader: start=%s exit=%s duration=%s",
+ FORMAT_TIMESPAN(loader_start, USEC_PER_MSEC),
+ FORMAT_TIMESPAN(loader_exit, USEC_PER_MSEC),
+ FORMAT_TIMESPAN(loader_exit - loader_start, USEC_PER_MSEC));
+ return 1;
+}
+
+static int test_boot_timestamps(void) {
+ dual_timestamp fw, l, k;
+ int r;
+
+ dual_timestamp_from_monotonic(&k, 0);
+
+ r = boot_timestamps(NULL, &fw, &l);
+ if (r < 0) {
+ bool ok = IN_SET(r, -ENOENT, -EOPNOTSUPP) || ERRNO_IS_PRIVILEGE(r);
+
+ log_full_errno(ok ? LOG_DEBUG : LOG_ERR, r, "Failed to read variables: %m");
+ return ok ? 0 : r;
+ }
+
+ log_info("Firmware began %s before kernel.", FORMAT_TIMESPAN(fw.monotonic, 0));
+ log_info("Loader began %s before kernel.", FORMAT_TIMESPAN(l.monotonic, 0));
+ log_info("Firmware began %s.", FORMAT_TIMESTAMP(fw.realtime));
+ log_info("Loader began %s.", FORMAT_TIMESTAMP(l.realtime));
+ log_info("Kernel began %s.", FORMAT_TIMESTAMP(k.realtime));
+ return 1;
+}
+
+int main(int argc, char* argv[]) {
+ int p, q, r;
+
+ test_setup_logging(LOG_DEBUG);
+
+ p = test_acpi_fpdt();
+ assert_se(p >= 0);
+ q = test_efi_loader();
+ assert_se(q >= 0);
+ r = test_boot_timestamps();
+ assert_se(r >= 0);
+
+ if (p == 0 && q == 0 && r == 0)
+ return log_tests_skipped("access to firmware variables not possible");
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/test/test-bootspec.c b/src/test/test-bootspec.c
new file mode 100644
index 0000000..18611fc
--- /dev/null
+++ b/src/test/test-bootspec.c
@@ -0,0 +1,211 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "bootspec.h"
+#include "fileio.h"
+#include "path-util.h"
+#include "rm-rf.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+TEST_RET(bootspec_sort) {
+
+ static const struct {
+ const char *fname;
+ const char *contents;
+ } entries[] = {
+ {
+ .fname = "a-10.conf",
+ .contents =
+ "title A\n"
+ "version 10\n"
+ "machine-id dd235d00696545768f6f693bfd23b15f\n",
+ },
+ {
+ .fname = "a-5.conf",
+ .contents =
+ "title A\n"
+ "version 5\n"
+ "machine-id dd235d00696545768f6f693bfd23b15f\n",
+ },
+ {
+ .fname = "b.conf",
+ .contents =
+ "title B\n"
+ "version 3\n"
+ "machine-id b75451ad92f94feeab50b0b442768dbd\n",
+ },
+ {
+ .fname = "c.conf",
+ .contents =
+ "title C\n"
+ "sort-key xxxx\n"
+ "version 5\n"
+ "machine-id 309de666fd5044268a9a26541ac93176\n",
+ },
+ {
+ .fname = "cx.conf",
+ .contents =
+ "title C\n"
+ "sort-key xxxx\n"
+ "version 10\n"
+ "machine-id 309de666fd5044268a9a26541ac93176\n",
+ },
+ {
+ .fname = "d.conf",
+ .contents =
+ "title D\n"
+ "sort-key kkkk\n"
+ "version 100\n"
+ "machine-id 81c6e3147cf544c19006af023e22b292\n",
+ },
+ };
+
+ _cleanup_(rm_rf_physical_and_freep) char *d = NULL;
+ _cleanup_(boot_config_free) BootConfig config = BOOT_CONFIG_NULL;
+
+ assert_se(mkdtemp_malloc("/tmp/bootspec-testXXXXXX", &d) >= 0);
+
+ for (size_t i = 0; i < ELEMENTSOF(entries); i++) {
+ _cleanup_free_ char *j = NULL;
+
+ j = path_join(d, "/loader/entries/", entries[i].fname);
+ assert_se(j);
+
+ assert_se(write_string_file(j, entries[i].contents, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755) >= 0);
+ }
+
+ assert_se(boot_config_load(&config, d, NULL) >= 0);
+
+ assert_se(config.n_entries == 6);
+
+ /* First, because has sort key, and its the lowest one */
+ assert_se(streq(config.entries[0].id, "d.conf"));
+
+ /* These two have a sort key, and newest must be first */
+ assert_se(streq(config.entries[1].id, "cx.conf"));
+ assert_se(streq(config.entries[2].id, "c.conf"));
+
+ /* The following ones have no sort key, hence order by version compared ids, lowest first */
+ assert_se(streq(config.entries[3].id, "b.conf"));
+ assert_se(streq(config.entries[4].id, "a-10.conf"));
+ assert_se(streq(config.entries[5].id, "a-5.conf"));
+
+ return 0;
+}
+
+static void test_extract_tries_one(const char *fname, int ret, const char *stripped, unsigned tries_left, unsigned tries_done) {
+ _cleanup_free_ char *p = NULL;
+ unsigned l, d;
+
+ assert_se(boot_filename_extract_tries(fname, &p, &l, &d) == ret);
+ if (ret < 0)
+ return;
+
+ assert_se(streq_ptr(p, stripped));
+ assert_se(l == tries_left);
+ assert_se(d == tries_done);
+}
+
+TEST_RET(bootspec_extract_tries) {
+ test_extract_tries_one("foo.conf", 0, "foo.conf", UINT_MAX, UINT_MAX);
+
+ test_extract_tries_one("foo+0.conf", 0, "foo.conf", 0, UINT_MAX);
+ test_extract_tries_one("foo+1.conf", 0, "foo.conf", 1, UINT_MAX);
+ test_extract_tries_one("foo+2.conf", 0, "foo.conf", 2, UINT_MAX);
+ test_extract_tries_one("foo+33.conf", 0, "foo.conf", 33, UINT_MAX);
+
+ assert_cc(INT_MAX == INT32_MAX);
+ test_extract_tries_one("foo+2147483647.conf", 0, "foo.conf", 2147483647, UINT_MAX);
+ test_extract_tries_one("foo+2147483648.conf", -ERANGE, NULL, UINT_MAX, UINT_MAX);
+
+ test_extract_tries_one("foo+33-0.conf", 0, "foo.conf", 33, 0);
+ test_extract_tries_one("foo+33-1.conf", 0, "foo.conf", 33, 1);
+ test_extract_tries_one("foo+33-107.conf", 0, "foo.conf", 33, 107);
+ test_extract_tries_one("foo+33-107.efi", 0, "foo.efi", 33, 107);
+ test_extract_tries_one("foo+33-2147483647.conf", 0, "foo.conf", 33, 2147483647);
+ test_extract_tries_one("foo+33-2147483648.conf", -ERANGE, NULL, UINT_MAX, UINT_MAX);
+
+ test_extract_tries_one("foo+007-000008.conf", 0, "foo.conf", 7, 8);
+
+ test_extract_tries_one("foo-1.conf", 0, "foo-1.conf", UINT_MAX, UINT_MAX);
+ test_extract_tries_one("foo-999.conf", 0, "foo-999.conf", UINT_MAX, UINT_MAX);
+ test_extract_tries_one("foo-.conf", 0, "foo-.conf", UINT_MAX, UINT_MAX);
+
+ test_extract_tries_one("foo+.conf", 0, "foo+.conf", UINT_MAX, UINT_MAX);
+ test_extract_tries_one("+.conf", 0, "+.conf", UINT_MAX, UINT_MAX);
+ test_extract_tries_one("-.conf", 0, "-.conf", UINT_MAX, UINT_MAX);
+ test_extract_tries_one("", 0, "", UINT_MAX, UINT_MAX);
+
+ test_extract_tries_one("+1.", 0, ".", 1, UINT_MAX);
+ test_extract_tries_one("+1-7.", 0, ".", 1, 7);
+
+ test_extract_tries_one("some+name+24324-22.efi", 0, "some+name.efi", 24324, 22);
+ test_extract_tries_one("sels+2-3+7-6.", 0, "sels+2-3.", 7, 6);
+ test_extract_tries_one("a+1-2..", 0, "a+1-2..", UINT_MAX, UINT_MAX);
+ test_extract_tries_one("ses.sgesge.+4-1.efi", 0, "ses.sgesge..efi", 4, 1);
+ test_extract_tries_one("abc+0x4.conf", 0, "abc+0x4.conf", UINT_MAX, UINT_MAX);
+ test_extract_tries_one("def+1-0x3.conf", 0, "def+1-0x3.conf", UINT_MAX, UINT_MAX);
+
+ return 0;
+}
+
+TEST_RET(bootspec_boot_config_find_entry) {
+
+ static const struct {
+ const char *fname;
+ const char *contents;
+ } entries[] = {
+ {
+ .fname = "a-10.conf",
+ .contents =
+ "title A\n"
+ "version 10\n"
+ "machine-id dd235d00696545768f6f693bfd23b15f\n",
+ },
+ {
+ .fname = "a-05.conf",
+ .contents =
+ "title A\n"
+ "version 10\n"
+ "machine-id dd235d00696545768f6f693bfd23b15f\n",
+ },
+ };
+
+ _cleanup_(rm_rf_physical_and_freep) char *d = NULL;
+ _cleanup_(boot_config_free) BootConfig config = BOOT_CONFIG_NULL;
+
+ assert_se(mkdtemp_malloc("/tmp/bootspec-testXXXXXX", &d) >= 0);
+
+ for (size_t i = 0; i < ELEMENTSOF(entries); i++) {
+ _cleanup_free_ char *j = NULL;
+
+ j = path_join(d, "/loader/entries/", entries[i].fname);
+ assert_se(j);
+
+ assert_se(write_string_file(j, entries[i].contents, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755) >= 0);
+ }
+
+ assert_se(boot_config_load(&config, d, NULL) >= 0);
+ assert_se(config.n_entries == 2);
+
+ // Test finding the first entry
+ BootEntry *entry = boot_config_find_entry(&config, "a-10.conf");
+ assert_se(entry && streq(entry->id, "a-10.conf"));
+
+ // Test finding the second entry
+ entry = boot_config_find_entry(&config, "a-05.conf");
+ assert_se(entry && streq(entry->id, "a-05.conf"));
+
+ // Test finding a non-existent entry
+ entry = boot_config_find_entry(&config, "nonexistent.conf");
+ assert_se(entry == NULL);
+
+ // Test case-insensitivity
+ entry = boot_config_find_entry(&config, "A-10.CONF");
+ assert_se(entry && streq(entry->id, "a-10.conf"));
+
+
+ return 0;
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-bpf-devices.c b/src/test/test-bpf-devices.c
new file mode 100644
index 0000000..4bd606e
--- /dev/null
+++ b/src/test/test-bpf-devices.c
@@ -0,0 +1,307 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/resource.h>
+#include <sys/time.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "bpf-devices.h"
+#include "bpf-program.h"
+#include "cgroup-setup.h"
+#include "errno-list.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "path-util.h"
+#include "tests.h"
+
+static void test_policy_closed(const char *cgroup_path, BPFProgram **installed_prog) {
+ _cleanup_(bpf_program_freep) BPFProgram *prog = NULL;
+ unsigned wrong = 0;
+ int r;
+
+ log_info("/* %s */", __func__);
+
+ r = bpf_devices_cgroup_init(&prog, CGROUP_DEVICE_POLICY_CLOSED, true);
+ assert_se(r >= 0);
+
+ r = bpf_devices_allow_list_static(prog, cgroup_path);
+ assert_se(r >= 0);
+
+ r = bpf_devices_apply_policy(&prog, CGROUP_DEVICE_POLICY_CLOSED, true, cgroup_path, installed_prog);
+ assert_se(r >= 0);
+
+ FOREACH_STRING(s, "/dev/null",
+ "/dev/zero",
+ "/dev/full",
+ "/dev/random",
+ "/dev/urandom",
+ "/dev/tty",
+ "/dev/ptmx") {
+ _cleanup_close_ int fd = -EBADF, fd2 = -EBADF;
+
+ fd = open(s, O_CLOEXEC|O_RDONLY|O_NOCTTY);
+ log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
+ wrong += fd < 0 && errno == EPERM;
+ /* We ignore errors other than EPERM, e.g. ENOENT or ENXIO */
+
+ fd2 = open(s, O_CLOEXEC|O_WRONLY|O_NOCTTY);
+ log_debug("open(%s, \"w\") = %d/%s", s, fd2, fd2 < 0 ? errno_to_name(errno) : "-");
+ wrong += fd2 < 0 && errno == EPERM;
+ }
+ assert_se(wrong == 0);
+}
+
+static void test_policy_strict(const char *cgroup_path, BPFProgram **installed_prog) {
+ _cleanup_(bpf_program_freep) BPFProgram *prog = NULL;
+ unsigned wrong = 0;
+ int r;
+
+ log_info("/* %s */", __func__);
+
+ r = bpf_devices_cgroup_init(&prog, CGROUP_DEVICE_POLICY_STRICT, true);
+ assert_se(r >= 0);
+
+ r = bpf_devices_allow_list_device(prog, cgroup_path, "/dev/null", CGROUP_DEVICE_READ|CGROUP_DEVICE_WRITE);
+ assert_se(r >= 0);
+
+ r = bpf_devices_allow_list_device(prog, cgroup_path, "/dev/random", CGROUP_DEVICE_READ);
+ assert_se(r >= 0);
+
+ r = bpf_devices_allow_list_device(prog, cgroup_path, "/dev/zero", CGROUP_DEVICE_WRITE);
+ assert_se(r >= 0);
+
+ r = bpf_devices_apply_policy(&prog, CGROUP_DEVICE_POLICY_STRICT, true, cgroup_path, installed_prog);
+ assert_se(r >= 0);
+
+ {
+ _cleanup_close_ int fd = -EBADF, fd2 = -EBADF;
+ const char *s = "/dev/null";
+
+ fd = open(s, O_CLOEXEC|O_RDONLY|O_NOCTTY);
+ log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
+ wrong += fd < 0;
+
+ fd2 = open(s, O_CLOEXEC|O_WRONLY|O_NOCTTY);
+ log_debug("open(%s, \"w\") = %d/%s", s, fd2, fd2 < 0 ? errno_to_name(errno) : "-");
+ wrong += fd2 < 0;
+ }
+
+ {
+ _cleanup_close_ int fd = -EBADF, fd2 = -EBADF;
+ const char *s = "/dev/random";
+
+ fd = open(s, O_CLOEXEC|O_RDONLY|O_NOCTTY);
+ log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
+ wrong += fd < 0;
+
+ fd2 = open(s, O_CLOEXEC|O_WRONLY|O_NOCTTY);
+ log_debug("open(%s, \"w\") = %d/%s", s, fd2, fd2 < 0 ? errno_to_name(errno) : "-");
+ wrong += fd2 >= 0;
+ }
+
+ {
+ _cleanup_close_ int fd = -EBADF, fd2 = -EBADF;
+ const char *s = "/dev/zero";
+
+ fd = open(s, O_CLOEXEC|O_RDONLY|O_NOCTTY);
+ log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
+ wrong += fd >= 0;
+
+ fd2 = open(s, O_CLOEXEC|O_WRONLY|O_NOCTTY);
+ log_debug("open(%s, \"w\") = %d/%s", s, fd2, fd2 < 0 ? errno_to_name(errno) : "-");
+ wrong += fd2 < 0;
+ }
+
+ {
+ _cleanup_close_ int fd = -EBADF, fd2 = -EBADF;
+ const char *s = "/dev/full";
+
+ fd = open(s, O_CLOEXEC|O_RDONLY|O_NOCTTY);
+ log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
+ wrong += fd >= 0;
+
+ fd2 = open(s, O_CLOEXEC|O_WRONLY|O_NOCTTY);
+ log_debug("open(%s, \"w\") = %d/%s", s, fd2, fd2 < 0 ? errno_to_name(errno) : "-");
+ wrong += fd2 >= 0;
+ }
+
+ assert_se(wrong == 0);
+}
+
+static void test_policy_allow_list_major(const char *pattern, const char *cgroup_path, BPFProgram **installed_prog) {
+ _cleanup_(bpf_program_freep) BPFProgram *prog = NULL;
+ unsigned wrong = 0;
+ int r;
+
+ log_info("/* %s(%s) */", __func__, pattern);
+
+ r = bpf_devices_cgroup_init(&prog, CGROUP_DEVICE_POLICY_STRICT, true);
+ assert_se(r >= 0);
+
+ r = bpf_devices_allow_list_major(prog, cgroup_path, pattern, 'c', CGROUP_DEVICE_READ|CGROUP_DEVICE_WRITE);
+ assert_se(r >= 0);
+
+ r = bpf_devices_apply_policy(&prog, CGROUP_DEVICE_POLICY_STRICT, true, cgroup_path, installed_prog);
+ assert_se(r >= 0);
+
+ /* /dev/null, /dev/full have major==1, /dev/tty has major==5 */
+ {
+ _cleanup_close_ int fd = -EBADF, fd2 = -EBADF;
+ const char *s = "/dev/null";
+
+ fd = open(s, O_CLOEXEC|O_RDONLY|O_NOCTTY);
+ log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
+ wrong += fd < 0;
+
+ fd2 = open(s, O_CLOEXEC|O_WRONLY|O_NOCTTY);
+ log_debug("open(%s, \"w\") = %d/%s", s, fd2, fd2 < 0 ? errno_to_name(errno) : "-");
+ wrong += fd2 < 0;
+ }
+
+ {
+ _cleanup_close_ int fd = -EBADF, fd2 = -EBADF;
+ const char *s = "/dev/full";
+
+ fd = open(s, O_CLOEXEC|O_RDONLY|O_NOCTTY);
+ log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
+ wrong += fd < 0;
+
+ fd2 = open(s, O_CLOEXEC|O_WRONLY|O_NOCTTY);
+ log_debug("open(%s, \"w\") = %d/%s", s, fd2, fd2 < 0 ? errno_to_name(errno) : "-");
+ wrong += fd2 < 0;
+ }
+
+ {
+ _cleanup_close_ int fd = -EBADF, fd2 = -EBADF;
+ const char *s = "/dev/tty";
+
+ fd = open(s, O_CLOEXEC|O_RDONLY|O_NOCTTY);
+ log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
+ wrong += fd >= 0;
+
+ fd2 = open(s, O_CLOEXEC|O_WRONLY|O_NOCTTY);
+ log_debug("open(%s, \"w\") = %d/%s", s, fd2, fd2 < 0 ? errno_to_name(errno) : "-");
+ wrong += fd2 >= 0;
+ }
+
+ assert_se(wrong == 0);
+}
+
+static void test_policy_allow_list_major_star(char type, const char *cgroup_path, BPFProgram **installed_prog) {
+ _cleanup_(bpf_program_freep) BPFProgram *prog = NULL;
+ unsigned wrong = 0;
+ int r;
+
+ log_info("/* %s(type=%c) */", __func__, type);
+
+ r = bpf_devices_cgroup_init(&prog, CGROUP_DEVICE_POLICY_STRICT, true);
+ assert_se(r >= 0);
+
+ r = bpf_devices_allow_list_major(prog, cgroup_path, "*", type, CGROUP_DEVICE_READ|CGROUP_DEVICE_WRITE);
+ assert_se(r >= 0);
+
+ r = bpf_devices_apply_policy(&prog, CGROUP_DEVICE_POLICY_STRICT, true, cgroup_path, installed_prog);
+ assert_se(r >= 0);
+
+ {
+ _cleanup_close_ int fd = -EBADF;
+ const char *s = "/dev/null";
+
+ fd = open(s, O_CLOEXEC|O_RDWR|O_NOCTTY);
+ log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
+ if (type == 'c')
+ wrong += fd < 0;
+ else
+ wrong += fd >= 0;
+ }
+
+ assert_se(wrong == 0);
+}
+
+static void test_policy_empty(bool add_mismatched, const char *cgroup_path, BPFProgram **installed_prog) {
+ _cleanup_(bpf_program_freep) BPFProgram *prog = NULL;
+ unsigned wrong = 0;
+ int r;
+
+ log_info("/* %s(add_mismatched=%s) */", __func__, yes_no(add_mismatched));
+
+ r = bpf_devices_cgroup_init(&prog, CGROUP_DEVICE_POLICY_STRICT, add_mismatched);
+ assert_se(r >= 0);
+
+ if (add_mismatched) {
+ r = bpf_devices_allow_list_major(prog, cgroup_path, "foobarxxx", 'c', CGROUP_DEVICE_READ|CGROUP_DEVICE_WRITE);
+ assert_se(r < 0);
+ }
+
+ r = bpf_devices_apply_policy(&prog, CGROUP_DEVICE_POLICY_STRICT, false, cgroup_path, installed_prog);
+ assert_se(r >= 0);
+
+ {
+ _cleanup_close_ int fd = -EBADF;
+ const char *s = "/dev/null";
+
+ fd = open(s, O_CLOEXEC|O_RDWR|O_NOCTTY);
+ log_debug("open(%s, \"r\") = %d/%s", s, fd, fd < 0 ? errno_to_name(errno) : "-");
+ wrong += fd >= 0;
+ }
+
+ assert_se(wrong == 0);
+}
+
+
+int main(int argc, char *argv[]) {
+ _cleanup_free_ char *cgroup = NULL, *parent = NULL;
+ _cleanup_(rmdir_and_freep) char *controller_path = NULL;
+ CGroupMask supported;
+ struct rlimit rl;
+ int r;
+
+ test_setup_logging(LOG_DEBUG);
+
+ assert_se(getrlimit(RLIMIT_MEMLOCK, &rl) >= 0);
+ rl.rlim_cur = rl.rlim_max = MAX(rl.rlim_max, CAN_MEMLOCK_SIZE);
+ (void) setrlimit(RLIMIT_MEMLOCK, &rl);
+
+ r = cg_all_unified();
+ if (r <= 0)
+ return log_tests_skipped("We don't seem to be running with unified cgroup hierarchy");
+
+ if (!can_memlock())
+ return log_tests_skipped("Can't use mlock()");
+
+ r = enter_cgroup_subroot(&cgroup);
+ if (r == -ENOMEDIUM)
+ return log_tests_skipped("cgroupfs not available");
+ if (r < 0)
+ return log_tests_skipped_errno(r, "Failed to prepare cgroup subtree");
+
+ r = bpf_devices_supported();
+ if (r == 0)
+ return log_tests_skipped("BPF device filter not supported");
+ assert_se(r == 1);
+
+ r = cg_get_path(SYSTEMD_CGROUP_CONTROLLER, cgroup, NULL, &controller_path);
+ assert_se(r >= 0);
+
+ _cleanup_(bpf_program_freep) BPFProgram *prog = NULL;
+
+ test_policy_closed(cgroup, &prog);
+ test_policy_strict(cgroup, &prog);
+
+ test_policy_allow_list_major("mem", cgroup, &prog);
+ test_policy_allow_list_major("1", cgroup, &prog);
+
+ test_policy_allow_list_major_star('c', cgroup, &prog);
+ test_policy_allow_list_major_star('b', cgroup, &prog);
+
+ test_policy_empty(false, cgroup, &prog);
+ test_policy_empty(true, cgroup, &prog);
+
+ assert_se(path_extract_directory(cgroup, &parent) >= 0);
+
+ assert_se(cg_mask_supported(&supported) >= 0);
+ r = cg_attach_everywhere(supported, parent, 0, NULL, NULL);
+ assert_se(r >= 0);
+
+ return 0;
+}
diff --git a/src/test/test-bpf-firewall.c b/src/test/test-bpf-firewall.c
new file mode 100644
index 0000000..c4175bc
--- /dev/null
+++ b/src/test/test-bpf-firewall.c
@@ -0,0 +1,217 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/bpf_insn.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "bpf-firewall.h"
+#include "bpf-program.h"
+#include "in-addr-prefix-util.h"
+#include "load-fragment.h"
+#include "manager.h"
+#include "memory-util.h"
+#include "rm-rf.h"
+#include "service.h"
+#include "tests.h"
+#include "unit-serialize.h"
+#include "virt.h"
+
+int main(int argc, char *argv[]) {
+ const struct bpf_insn exit_insn[] = {
+ BPF_MOV64_IMM(BPF_REG_0, 0), /* drop */
+ BPF_EXIT_INSN()
+ };
+
+ _cleanup_(rm_rf_physical_and_freep) char *runtime_dir = NULL;
+ CGroupContext *cc = NULL;
+ _cleanup_(bpf_program_freep) BPFProgram *p = NULL;
+ _cleanup_(manager_freep) Manager *m = NULL;
+ Unit *u;
+ char log_buf[65535];
+ struct rlimit rl;
+ int r;
+ union bpf_attr attr;
+ bool test_custom_filter = false;
+ const char *test_prog = "/sys/fs/bpf/test-dropper";
+
+ test_setup_logging(LOG_DEBUG);
+
+ if (detect_container() > 0)
+ return log_tests_skipped("test-bpf-firewall fails inside LXC and Docker containers: https://github.com/systemd/systemd/issues/9666");
+
+ assert_se(getrlimit(RLIMIT_MEMLOCK, &rl) >= 0);
+ rl.rlim_cur = rl.rlim_max = MAX(rl.rlim_max, CAN_MEMLOCK_SIZE);
+ (void) setrlimit(RLIMIT_MEMLOCK, &rl);
+
+ if (!can_memlock())
+ return log_tests_skipped("Can't use mlock()");
+
+ r = enter_cgroup_subroot(NULL);
+ if (r == -ENOMEDIUM)
+ return log_tests_skipped("cgroupfs not available");
+
+ _cleanup_free_ char *unit_dir = NULL;
+ assert_se(get_testdata_dir("units", &unit_dir) >= 0);
+ assert_se(set_unit_path(unit_dir) >= 0);
+ assert_se(runtime_dir = setup_fake_runtime_dir());
+
+ r = bpf_program_new(BPF_PROG_TYPE_CGROUP_SKB, "sd_trivial", &p);
+ assert_se(r == 0);
+
+ r = bpf_program_add_instructions(p, exit_insn, ELEMENTSOF(exit_insn));
+ assert_se(r == 0);
+
+ r = bpf_firewall_supported();
+ if (r == BPF_FIREWALL_UNSUPPORTED)
+ return log_tests_skipped("BPF firewalling not supported");
+ assert_se(r > 0);
+
+ if (r == BPF_FIREWALL_SUPPORTED_WITH_MULTI) {
+ log_notice("BPF firewalling with BPF_F_ALLOW_MULTI supported. Yay!");
+ test_custom_filter = true;
+ } else
+ log_notice("BPF firewalling (though without BPF_F_ALLOW_MULTI) supported. Good.");
+
+ r = bpf_program_load_kernel(p, log_buf, ELEMENTSOF(log_buf));
+ assert_se(r >= 0);
+
+ if (test_custom_filter) {
+ zero(attr);
+ attr.pathname = PTR_TO_UINT64(test_prog);
+ attr.bpf_fd = p->kernel_fd;
+ attr.file_flags = 0;
+
+ (void) unlink(test_prog);
+
+ r = bpf(BPF_OBJ_PIN, &attr, sizeof(attr));
+ if (r < 0) {
+ log_warning_errno(errno, "BPF object pinning failed, will not run custom filter test: %m");
+ test_custom_filter = false;
+ }
+ }
+
+ p = bpf_program_free(p);
+
+ /* The simple tests succeeded. Now let's try full unit-based use-case. */
+
+ assert_se(manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_BASIC, &m) >= 0);
+ assert_se(manager_startup(m, NULL, NULL, NULL) >= 0);
+
+ assert_se(u = unit_new(m, sizeof(Service)));
+ assert_se(unit_add_name(u, "foo.service") == 0);
+ assert_se(cc = unit_get_cgroup_context(u));
+ u->perpetual = true;
+
+ cc->ip_accounting = true;
+
+ assert_se(config_parse_in_addr_prefixes(u->id, "filename", 1, "Service", 1, "IPAddressAllow", 0, "10.0.1.0/24", &cc->ip_address_allow, NULL) == 0);
+ assert_se(config_parse_in_addr_prefixes(u->id, "filename", 1, "Service", 1, "IPAddressAllow", 0, "127.0.0.2", &cc->ip_address_allow, NULL) == 0);
+ assert_se(config_parse_in_addr_prefixes(u->id, "filename", 1, "Service", 1, "IPAddressDeny", 0, "127.0.0.3", &cc->ip_address_deny, NULL) == 0);
+ assert_se(config_parse_in_addr_prefixes(u->id, "filename", 1, "Service", 1, "IPAddressDeny", 0, "10.0.3.2/24", &cc->ip_address_deny, NULL) == 0);
+ assert_se(config_parse_in_addr_prefixes(u->id, "filename", 1, "Service", 1, "IPAddressDeny", 0, "127.0.0.1/25", &cc->ip_address_deny, NULL) == 0);
+ assert_se(config_parse_in_addr_prefixes(u->id, "filename", 1, "Service", 1, "IPAddressDeny", 0, "127.0.0.4", &cc->ip_address_deny, NULL) == 0);
+
+ assert_se(set_size(cc->ip_address_allow) == 2);
+ assert_se(set_size(cc->ip_address_deny) == 4);
+
+ /* The deny list is defined redundantly, let's ensure it will be properly reduced */
+ assert_se(in_addr_prefixes_reduce(cc->ip_address_allow) >= 0);
+ assert_se(in_addr_prefixes_reduce(cc->ip_address_deny) >= 0);
+
+ assert_se(set_size(cc->ip_address_allow) == 2);
+ assert_se(set_size(cc->ip_address_deny) == 2);
+
+ assert_se(set_contains(cc->ip_address_allow, &(struct in_addr_prefix) {
+ .family = AF_INET,
+ .address.in.s_addr = htobe32((UINT32_C(10) << 24) | (UINT32_C(1) << 8)),
+ .prefixlen = 24 }));
+ assert_se(set_contains(cc->ip_address_allow, &(struct in_addr_prefix) {
+ .family = AF_INET,
+ .address.in.s_addr = htobe32(0x7f000002),
+ .prefixlen = 32 }));
+ assert_se(set_contains(cc->ip_address_deny, &(struct in_addr_prefix) {
+ .family = AF_INET,
+ .address.in.s_addr = htobe32(0x7f000000),
+ .prefixlen = 25 }));
+ assert_se(set_contains(cc->ip_address_deny, &(struct in_addr_prefix) {
+ .family = AF_INET,
+ .address.in.s_addr = htobe32((UINT32_C(10) << 24) | (UINT32_C(3) << 8)),
+ .prefixlen = 24 }));
+
+ assert_se(config_parse_exec(u->id, "filename", 1, "Service", 1, "ExecStart", SERVICE_EXEC_START, "/bin/ping -c 1 127.0.0.2 -W 5", SERVICE(u)->exec_command, u) == 0);
+ assert_se(config_parse_exec(u->id, "filename", 1, "Service", 1, "ExecStart", SERVICE_EXEC_START, "/bin/ping -c 1 127.0.0.3 -W 5", SERVICE(u)->exec_command, u) == 0);
+
+ assert_se(SERVICE(u)->exec_command[SERVICE_EXEC_START]);
+ assert_se(SERVICE(u)->exec_command[SERVICE_EXEC_START]->command_next);
+ assert_se(!SERVICE(u)->exec_command[SERVICE_EXEC_START]->command_next->command_next);
+
+ SERVICE(u)->type = SERVICE_ONESHOT;
+ u->load_state = UNIT_LOADED;
+
+ unit_dump(u, stdout, NULL);
+
+ r = bpf_firewall_compile(u);
+ if (IN_SET(r, -ENOTTY, -ENOSYS, -EPERM))
+ return log_tests_skipped("Kernel doesn't support the necessary bpf bits (masked out via seccomp?)");
+ assert_se(r >= 0);
+
+ assert_se(u->ip_bpf_ingress);
+ assert_se(u->ip_bpf_egress);
+
+ r = bpf_program_load_kernel(u->ip_bpf_ingress, log_buf, ELEMENTSOF(log_buf));
+
+ log_notice("log:");
+ log_notice("-------");
+ log_notice("%s", log_buf);
+ log_notice("-------");
+
+ assert_se(r >= 0);
+
+ r = bpf_program_load_kernel(u->ip_bpf_egress, log_buf, ELEMENTSOF(log_buf));
+
+ log_notice("log:");
+ log_notice("-------");
+ log_notice("%s", log_buf);
+ log_notice("-------");
+
+ assert_se(r >= 0);
+
+ assert_se(unit_start(u, NULL) >= 0);
+
+ while (!IN_SET(SERVICE(u)->state, SERVICE_DEAD, SERVICE_FAILED))
+ assert_se(sd_event_run(m->event, UINT64_MAX) >= 0);
+
+ assert_se(SERVICE(u)->exec_command[SERVICE_EXEC_START]->exec_status.code == CLD_EXITED &&
+ SERVICE(u)->exec_command[SERVICE_EXEC_START]->exec_status.status == EXIT_SUCCESS);
+
+ assert_se(SERVICE(u)->exec_command[SERVICE_EXEC_START]->command_next->exec_status.code != CLD_EXITED ||
+ SERVICE(u)->exec_command[SERVICE_EXEC_START]->command_next->exec_status.status != EXIT_SUCCESS);
+
+ if (test_custom_filter) {
+ assert_se(u = unit_new(m, sizeof(Service)));
+ assert_se(unit_add_name(u, "custom-filter.service") == 0);
+ assert_se(cc = unit_get_cgroup_context(u));
+ u->perpetual = true;
+
+ cc->ip_accounting = true;
+
+ assert_se(config_parse_ip_filter_bpf_progs(u->id, "filename", 1, "Service", 1, "IPIngressFilterPath", 0, test_prog, &cc->ip_filters_ingress, u) == 0);
+ assert_se(config_parse_exec(u->id, "filename", 1, "Service", 1, "ExecStart", SERVICE_EXEC_START, "-/bin/ping -c 1 127.0.0.1 -W 5", SERVICE(u)->exec_command, u) == 0);
+
+ SERVICE(u)->type = SERVICE_ONESHOT;
+ u->load_state = UNIT_LOADED;
+
+ assert_se(unit_start(u, NULL) >= 0);
+
+ while (!IN_SET(SERVICE(u)->state, SERVICE_DEAD, SERVICE_FAILED))
+ assert_se(sd_event_run(m->event, UINT64_MAX) >= 0);
+
+ assert_se(SERVICE(u)->exec_command[SERVICE_EXEC_START]->exec_status.code != CLD_EXITED ||
+ SERVICE(u)->exec_command[SERVICE_EXEC_START]->exec_status.status != EXIT_SUCCESS);
+
+ (void) unlink(test_prog);
+ assert_se(SERVICE(u)->state == SERVICE_DEAD);
+ }
+
+ return 0;
+}
diff --git a/src/test/test-bpf-foreign-programs.c b/src/test/test-bpf-foreign-programs.c
new file mode 100644
index 0000000..35c7e0d
--- /dev/null
+++ b/src/test/test-bpf-foreign-programs.c
@@ -0,0 +1,330 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <linux/bpf_insn.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include "bpf-foreign.h"
+#include "load-fragment.h"
+#include "manager.h"
+#include "process-util.h"
+#include "rlimit-util.h"
+#include "rm-rf.h"
+#include "service.h"
+#include "tests.h"
+#include "unit.h"
+#include "virt.h"
+
+struct Test {
+ const char *option_name;
+ enum bpf_prog_type prog_type;
+ enum bpf_attach_type attach_type;
+ const char *bpffs_path;
+};
+
+typedef struct Test Test;
+
+#define BPFFS_PATH(prog_suffix) ("/sys/fs/bpf/test-bpf-foreing-" # prog_suffix)
+static const Test single_prog[] = {
+ {
+ .option_name = "BPFProgram",
+ .prog_type = BPF_PROG_TYPE_CGROUP_SKB,
+ .attach_type = BPF_CGROUP_INET_INGRESS,
+ .bpffs_path = BPFFS_PATH("trivial-skb"),
+ },
+};
+static const Test path_split_test[] = {
+ {
+ .option_name = "BPFProgram",
+ .prog_type = BPF_PROG_TYPE_CGROUP_SKB,
+ .attach_type = BPF_CGROUP_INET_INGRESS,
+ .bpffs_path = BPFFS_PATH("path:split:test"),
+ },
+};
+
+static const Test same_prog_same_hook[] = {
+ {
+ .option_name = "BPFProgram",
+ .prog_type = BPF_PROG_TYPE_CGROUP_SOCK,
+ .attach_type = BPF_CGROUP_INET_SOCK_CREATE,
+ .bpffs_path = BPFFS_PATH("trivial-sock"),
+ },
+ {
+ .option_name = "BPFProgram",
+ .prog_type = BPF_PROG_TYPE_CGROUP_SOCK,
+ .attach_type = BPF_CGROUP_INET_SOCK_CREATE,
+ .bpffs_path = BPFFS_PATH("trivial-sock"),
+ }
+};
+
+static const Test multi_prog_same_hook[] = {
+ {
+ .option_name = "BPFProgram",
+ .prog_type = BPF_PROG_TYPE_CGROUP_SOCK,
+ .attach_type = BPF_CGROUP_INET_SOCK_CREATE,
+ .bpffs_path = BPFFS_PATH("trivial-sock-0"),
+ },
+ {
+ .option_name = "BPFProgram",
+ .prog_type = BPF_PROG_TYPE_CGROUP_SOCK,
+ .attach_type = BPF_CGROUP_INET_SOCK_CREATE,
+ .bpffs_path = BPFFS_PATH("trivial-sock-1"),
+ }
+};
+
+static const Test same_prog_multi_hook[] = {
+ {
+ .option_name = "BPFProgram",
+ .prog_type = BPF_PROG_TYPE_CGROUP_SKB,
+ .attach_type = BPF_CGROUP_INET_INGRESS,
+ .bpffs_path = BPFFS_PATH("trivial-skb"),
+ },
+ {
+ .option_name = "BPFProgram",
+ .prog_type = BPF_PROG_TYPE_CGROUP_SKB,
+ .attach_type = BPF_CGROUP_INET_EGRESS,
+ .bpffs_path = BPFFS_PATH("trivial-skb"),
+ }
+};
+
+static const Test same_prog_multi_option_0[] = {
+ {
+ .option_name = "BPFProgram",
+ .prog_type = BPF_PROG_TYPE_CGROUP_SKB,
+ .attach_type = BPF_CGROUP_INET_INGRESS,
+ .bpffs_path = BPFFS_PATH("trivial-skb"),
+ },
+ {
+ .option_name = "IPIngressFilterPath",
+ .prog_type = BPF_PROG_TYPE_CGROUP_SKB,
+ .attach_type = BPF_CGROUP_INET_INGRESS,
+ .bpffs_path = BPFFS_PATH("trivial-skb"),
+ }
+};
+
+static const Test same_prog_multi_option_1[] = {
+ {
+ .option_name = "IPEgressFilterPath",
+ .prog_type = BPF_PROG_TYPE_CGROUP_SKB,
+ .attach_type = BPF_CGROUP_INET_EGRESS,
+ .bpffs_path = BPFFS_PATH("trivial-skb"),
+ },
+ {
+ .option_name = "BPFProgram",
+ .prog_type = BPF_PROG_TYPE_CGROUP_SKB,
+ .attach_type = BPF_CGROUP_INET_EGRESS,
+ .bpffs_path = BPFFS_PATH("trivial-skb"),
+ }
+};
+#undef BPFFS_PATH
+
+static int bpf_foreign_test_to_string(enum bpf_attach_type attach_type, const char *bpffs_path, char **ret_str) {
+ const char *s = NULL;
+
+ assert_se(bpffs_path);
+ assert_se(ret_str);
+
+ assert_se(s = bpf_cgroup_attach_type_to_string(attach_type));
+ assert_se(*ret_str = strjoin(s, ":", bpffs_path));
+
+ return 0;
+}
+
+static char **unlink_paths_and_free(char **paths) {
+ STRV_FOREACH(i, paths)
+ (void) unlink(*i);
+
+ return strv_free(paths);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(char **, unlink_paths_and_free);
+
+static int pin_programs(Unit *u, CGroupContext *cc, const Test *test_suite, size_t test_suite_size, char ***paths_ret) {
+ _cleanup_(unlink_paths_and_freep) char **bpffs_paths = NULL;
+ static const struct bpf_insn trivial[] = {
+ BPF_MOV64_IMM(BPF_REG_0, 0),
+ BPF_EXIT_INSN()
+ };
+ char log_buf[0xffff];
+ int r;
+
+ assert_se(paths_ret);
+
+ for (size_t i = 0; i < test_suite_size; i++) {
+ _cleanup_(bpf_program_freep) BPFProgram *prog = NULL;
+ _cleanup_free_ char *str = NULL;
+
+ r = bpf_foreign_test_to_string(test_suite[i].attach_type, test_suite[i].bpffs_path, &str);
+ if (r < 0)
+ return log_error_errno(r, "Failed to convert program to string");
+
+ r = bpf_program_new(test_suite[i].prog_type, "sd_trivial", &prog);
+ if (r < 0)
+ return log_error_errno(r, "Failed to create program '%s'", str);
+
+ r = bpf_program_add_instructions(prog, trivial, ELEMENTSOF(trivial));
+ if (r < 0)
+ return log_error_errno(r, "Failed to add trivial instructions for '%s'", str);
+
+ r = bpf_program_load_kernel(prog, log_buf, ELEMENTSOF(log_buf));
+ if (r < 0)
+ return log_error_errno(r, "Failed to load BPF program '%s'", str);
+
+ if (strv_contains(bpffs_paths, test_suite[i].bpffs_path))
+ continue;
+
+ r = strv_extend(&bpffs_paths, test_suite[i].bpffs_path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to put path into a vector: %m");
+
+ r = bpf_program_pin(prog->kernel_fd, test_suite[i].bpffs_path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to pin BPF program '%s'", str);
+ }
+
+ *paths_ret = TAKE_PTR(bpffs_paths);
+ return 0;
+}
+
+static int test_bpf_cgroup_programs(Manager *m, const char *unit_name, const Test *test_suite, size_t test_suite_size) {
+ _cleanup_(unlink_paths_and_freep) char **bpffs_paths = NULL;
+ _cleanup_(unit_freep) Unit *u = NULL;
+ CGroupContext *cc = NULL;
+ int cld_code, r;
+
+ assert_se(u = unit_new(m, sizeof(Service)));
+ assert_se(unit_add_name(u, unit_name) == 0);
+ assert_se(cc = unit_get_cgroup_context(u));
+
+ r = pin_programs(u, cc, test_suite, test_suite_size, &bpffs_paths);
+ if (r < 0)
+ return log_error_errno(r, "Failed to pin programs: %m");
+
+ for (size_t i = 0; i < test_suite_size; i++) {
+ if (streq(test_suite[i].option_name, "BPFProgram")) {
+ _cleanup_free_ char *option = NULL;
+ r = bpf_foreign_test_to_string(test_suite[i].attach_type, test_suite[i].bpffs_path, &option);
+ if (r < 0)
+ return log_error_errno(r, "Failed to compose option string: %m");
+ r = config_parse_bpf_foreign_program(
+ u->id, "filename", 1, "Service", 1, test_suite[i].option_name, 0, option, cc, u);
+
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse option string '%s': %m", option);
+ } else if (STR_IN_SET(test_suite[i].option_name, "IPIngressFilterPath", "IPEgressFilterPath")) {
+ const char *option = test_suite[i].bpffs_path;
+ void *paths = NULL;
+
+ if (streq(test_suite[i].option_name, "IPIngressFilterPath"))
+ paths = &cc->ip_filters_ingress;
+ else
+ paths = &cc->ip_filters_egress;
+
+ r = config_parse_ip_filter_bpf_progs(
+ u->id, "filename", 1, "Service", 1, test_suite[i].option_name, 0, option, paths, u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse option string '%s': %m", option);
+ }
+ }
+
+ r = config_parse_exec(
+ u->id,
+ "filename",
+ 1,
+ "Service",
+ 1,
+ "ExecStart",
+ SERVICE_EXEC_START,
+ "-/bin/ping -c 5 127.0.0.1 -W 1",
+ SERVICE(u)->exec_command,
+ u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse ExecStart");
+
+ SERVICE(u)->type = SERVICE_ONESHOT;
+ u->load_state = UNIT_LOADED;
+
+ r = unit_start(u, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Unit start failed %m");
+
+ while (!IN_SET(SERVICE(u)->state, SERVICE_DEAD, SERVICE_FAILED)) {
+ r = sd_event_run(m->event, UINT64_MAX);
+ if (r < 0)
+ return log_error_errno(errno, "Event run failed %m");
+ }
+
+ cld_code = SERVICE(u)->exec_command[SERVICE_EXEC_START]->exec_status.code;
+ if (cld_code != CLD_EXITED)
+ return log_error_errno(SYNTHETIC_ERRNO(EBUSY),
+ "Child didn't exit normally, code='%s'", sigchld_code_to_string(cld_code));
+
+ if (SERVICE(u)->state != SERVICE_DEAD)
+ return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Service is not dead");
+
+ return r;
+}
+
+int main(int argc, char *argv[]) {
+ _cleanup_(rm_rf_physical_and_freep) char *runtime_dir = NULL;
+ _cleanup_(manager_freep) Manager *m = NULL;
+ _cleanup_free_ char *unit_dir = NULL;
+ struct rlimit rl;
+ int r;
+
+ test_setup_logging(LOG_DEBUG);
+
+ if (detect_container() > 0)
+ return log_tests_skipped("test-bpf fails inside LXC and Docker containers: https://github.com/systemd/systemd/issues/9666");
+
+ if (getuid() != 0)
+ return log_tests_skipped("not running as root");
+
+ assert_se(getrlimit(RLIMIT_MEMLOCK, &rl) >= 0);
+ rl.rlim_cur = rl.rlim_max = MAX(rl.rlim_max, CAN_MEMLOCK_SIZE);
+ (void) setrlimit_closest(RLIMIT_MEMLOCK, &rl);
+
+ if (!can_memlock())
+ return log_tests_skipped("Can't use mlock()");
+
+ r = cg_all_unified();
+ if (r <= 0)
+ return log_tests_skipped("Unified hierarchy is required");
+
+ r = enter_cgroup_subroot(NULL);
+ if (r == -ENOMEDIUM)
+ return log_tests_skipped("cgroupfs not available");
+
+ assert_se(get_testdata_dir("units", &unit_dir) >= 0);
+ assert_se(set_unit_path(unit_dir) >= 0);
+ assert_se(runtime_dir = setup_fake_runtime_dir());
+
+ assert_se(manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_BASIC, &m) >= 0);
+ assert_se(manager_startup(m, NULL, NULL, NULL) >= 0);
+
+ assert_se(test_bpf_cgroup_programs(m,
+ "single_prog.service", single_prog, ELEMENTSOF(single_prog)) >= 0);
+ assert_se(test_bpf_cgroup_programs(m,
+ "multi_prog_same_hook.service",
+ multi_prog_same_hook, ELEMENTSOF(multi_prog_same_hook)) >= 0);
+ assert_se(test_bpf_cgroup_programs(m,
+ "same_prog_multi_hook.service",
+ same_prog_multi_hook, ELEMENTSOF(same_prog_multi_hook)) >= 0);
+ assert_se(test_bpf_cgroup_programs(m,
+ "same_prog_multi_option_0.service",
+ same_prog_multi_option_0, ELEMENTSOF(same_prog_multi_option_0)) >= 0);
+ assert_se(test_bpf_cgroup_programs(m,
+ "same_prog_multi_option_1.service",
+ same_prog_multi_option_1, ELEMENTSOF(same_prog_multi_option_1)) >= 0);
+ assert_se(test_bpf_cgroup_programs(m,
+ "same_prog_same_hook.service",
+ same_prog_same_hook,
+ ELEMENTSOF(same_prog_same_hook)) >= 0);
+ assert_se(test_bpf_cgroup_programs(m,
+ "path_split_test.service",
+ path_split_test,
+ ELEMENTSOF(path_split_test)) >= 0);
+ return 0;
+}
diff --git a/src/test/test-bpf-lsm.c b/src/test/test-bpf-lsm.c
new file mode 100644
index 0000000..42ea64c
--- /dev/null
+++ b/src/test/test-bpf-lsm.c
@@ -0,0 +1,102 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "bpf-lsm.h"
+#include "load-fragment.h"
+#include "manager.h"
+#include "process-util.h"
+#include "rlimit-util.h"
+#include "rm-rf.h"
+#include "service.h"
+#include "strv.h"
+#include "tests.h"
+#include "unit.h"
+#include "virt.h"
+
+static int test_restrict_filesystems(Manager *m, const char *unit_name, const char *file_path, char **allowed_filesystems) {
+ _cleanup_free_ char *exec_start = NULL;
+ _cleanup_(unit_freep) Unit *u = NULL;
+ ExecContext *ec = NULL;
+ int cld_code, r;
+
+ assert_se(u = unit_new(m, sizeof(Service)));
+ assert_se(unit_add_name(u, unit_name) == 0);
+ assert_se(ec = unit_get_exec_context(u));
+
+ STRV_FOREACH(allow_filesystem, allowed_filesystems) {
+ r = config_parse_restrict_filesystems(
+ u->id, "filename", 1, "Service", 1, "RestrictFileSystems", 0,
+ *allow_filesystem, ec, u);
+ if (r < 0)
+ return log_unit_error_errno(u, r, "Failed to parse RestrictFileSystems: %m");
+ }
+
+ assert_se(exec_start = strjoin("cat ", file_path));
+ r = config_parse_exec(u->id, "filename", 1, "Service", 1, "ExecStart",
+ SERVICE_EXEC_START, exec_start, SERVICE(u)->exec_command, u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse ExecStart");
+
+ SERVICE(u)->type = SERVICE_ONESHOT;
+ u->load_state = UNIT_LOADED;
+
+ r = unit_start(u, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Unit start failed %m");
+
+ while (!IN_SET(SERVICE(u)->state, SERVICE_DEAD, SERVICE_FAILED)) {
+ r = sd_event_run(m->event, UINT64_MAX);
+ if (r < 0)
+ return log_error_errno(errno, "Event run failed %m");
+ }
+
+ cld_code = SERVICE(u)->exec_command[SERVICE_EXEC_START]->exec_status.code;
+ if (cld_code != CLD_EXITED)
+ return log_error_errno(-SYNTHETIC_ERRNO(EBUSY), "ExecStart didn't exited, code='%s'", sigchld_code_to_string(cld_code));
+
+ if (SERVICE(u)->state != SERVICE_DEAD)
+ return log_error_errno(-SYNTHETIC_ERRNO(EBUSY), "Service is not dead");
+
+ return 0;
+}
+
+int main(int argc, char *argv[]) {
+ _cleanup_(rm_rf_physical_and_freep) char *runtime_dir = NULL;
+ _cleanup_(manager_freep) Manager *m = NULL;
+ _cleanup_free_ char *unit_dir = NULL;
+ struct rlimit rl;
+ int r;
+
+ test_setup_logging(LOG_DEBUG);
+
+ assert_se(getrlimit(RLIMIT_MEMLOCK, &rl) >= 0);
+ rl.rlim_cur = rl.rlim_max = MAX(rl.rlim_max, CAN_MEMLOCK_SIZE);
+ (void) setrlimit_closest(RLIMIT_MEMLOCK, &rl);
+
+ if (!can_memlock())
+ return log_tests_skipped("Can't use mlock()");
+
+ if (!lsm_bpf_supported(/* initialize = */ true))
+ return log_tests_skipped("LSM BPF hooks are not supported");
+
+ r = enter_cgroup_subroot(NULL);
+ if (r == -ENOMEDIUM)
+ return log_tests_skipped("cgroupfs not available");
+
+ assert_se(get_testdata_dir("units", &unit_dir) >= 0);
+ assert_se(set_unit_path(unit_dir) >= 0);
+ assert_se(runtime_dir = setup_fake_runtime_dir());
+
+ assert_se(manager_new(RUNTIME_SCOPE_SYSTEM, MANAGER_TEST_RUN_BASIC, &m) >= 0);
+ assert_se(manager_startup(m, NULL, NULL, NULL) >= 0);
+
+ /* We need to enable access to the filesystem where the binary is so we
+ * add @common-block */
+ assert_se(test_restrict_filesystems(m, "restrict_filesystems_test.service", "/sys/kernel/tracing/printk_formats", STRV_MAKE("@common-block")) < 0);
+ assert_se(test_restrict_filesystems(m, "restrict_filesystems_test.service", "/sys/kernel/tracing/printk_formats", STRV_MAKE("tracefs", "@common-block")) >= 0);
+ assert_se(test_restrict_filesystems(m, "restrict_filesystems_test.service", "/sys/kernel/tracing/printk_formats", STRV_MAKE("tracefs", "@common-block", "~tracefs")) < 0);
+ assert_se(test_restrict_filesystems(m, "restrict_filesystems_test.service", "/sys/kernel/debug/sleep_time", STRV_MAKE("@common-block")) < 0);
+ assert_se(test_restrict_filesystems(m, "restrict_filesystems_test.service", "/sys/kernel/debug/sleep_time", STRV_MAKE("debugfs", "@common-block")) >= 0);
+ assert_se(test_restrict_filesystems(m, "restrict_filesystems_test.service", "/sys/kernel/debug/sleep_time", STRV_MAKE("~debugfs")) < 0);
+
+ return 0;
+}
diff --git a/src/test/test-btrfs-physical-offset.c b/src/test/test-btrfs-physical-offset.c
new file mode 100644
index 0000000..221c08e
--- /dev/null
+++ b/src/test/test-btrfs-physical-offset.c
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <fcntl.h>
+
+#include "btrfs-util.h"
+#include "fd-util.h"
+#include "format-util.h"
+#include "log.h"
+#include "memory-util.h"
+#include "tests.h"
+
+int main(int argc, char *argv[]) {
+ _cleanup_close_ int fd = -EBADF;
+ uint64_t offset;
+ int r;
+
+ assert(argc == 2);
+ assert(!isempty(argv[1]));
+
+ test_setup_logging(LOG_DEBUG);
+
+ fd = open(argv[1], O_RDONLY|O_CLOEXEC|O_NOCTTY);
+ if (fd < 0) {
+ log_error_errno(errno, "Failed to open '%s': %m", argv[1]);
+ return EXIT_FAILURE;
+ }
+
+ r = btrfs_get_file_physical_offset_fd(fd, &offset);
+ if (r < 0) {
+ log_error_errno(r, "Failed to get physical offset of '%s': %m", argv[1]);
+ return EXIT_FAILURE;
+ }
+
+ printf("%" PRIu64 "\n", offset / page_size());
+ return EXIT_SUCCESS;
+}
diff --git a/src/test/test-btrfs.c b/src/test/test-btrfs.c
new file mode 100644
index 0000000..205142e
--- /dev/null
+++ b/src/test/test-btrfs.c
@@ -0,0 +1,200 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <sys/file.h>
+
+#include "btrfs-util.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "fileio.h"
+#include "format-util.h"
+#include "log.h"
+#include "string-util.h"
+#include "tests.h"
+
+int main(int argc, char *argv[]) {
+ BtrfsQuotaInfo quota;
+ int r, fd;
+
+ test_setup_logging(LOG_DEBUG);
+
+ fd = open("/", O_RDONLY|O_CLOEXEC|O_DIRECTORY);
+ if (fd < 0)
+ log_error_errno(errno, "Failed to open root directory: %m");
+ else {
+ BtrfsSubvolInfo info;
+
+ r = btrfs_subvol_get_info_fd(fd, 0, &info);
+ if (r < 0)
+ log_error_errno(r, "Failed to get subvolume info: %m");
+ else {
+ log_info("otime: %s", FORMAT_TIMESTAMP(info.otime));
+ log_info("read-only (search): %s", yes_no(info.read_only));
+ }
+
+ r = btrfs_qgroup_get_quota_fd(fd, 0, &quota);
+ if (r < 0)
+ log_error_errno(r, "Failed to get quota info: %m");
+ else {
+ log_info("referenced: %s", strna(FORMAT_BYTES(quota.referenced)));
+ log_info("exclusive: %s", strna(FORMAT_BYTES(quota.exclusive)));
+ log_info("referenced_max: %s", strna(FORMAT_BYTES(quota.referenced_max)));
+ log_info("exclusive_max: %s", strna(FORMAT_BYTES(quota.exclusive_max)));
+ }
+
+ r = btrfs_subvol_get_read_only_fd(fd);
+ if (r < 0)
+ log_error_errno(r, "Failed to get read only flag: %m");
+ else
+ log_info("read-only (ioctl): %s", yes_no(r));
+
+ safe_close(fd);
+ }
+
+ r = btrfs_subvol_make(AT_FDCWD, "/xxxtest");
+ if (r < 0)
+ log_error_errno(r, "Failed to make subvolume: %m");
+
+ r = write_string_file("/xxxtest/file", "ljsadhfljasdkfhlkjdsfha", WRITE_STRING_FILE_CREATE);
+ if (r < 0)
+ log_error_errno(r, "Failed to write file: %m");
+
+ r = btrfs_subvol_snapshot_at(AT_FDCWD, "/xxxtest", AT_FDCWD, "/xxxtest2", 0);
+ if (r < 0)
+ log_error_errno(r, "Failed to make snapshot: %m");
+
+ r = btrfs_subvol_snapshot_at(AT_FDCWD, "/xxxtest", AT_FDCWD, "/xxxtest3", BTRFS_SNAPSHOT_READ_ONLY);
+ if (r < 0)
+ log_error_errno(r, "Failed to make snapshot: %m");
+
+ r = btrfs_subvol_snapshot_at(AT_FDCWD, "/xxxtest", AT_FDCWD, "/xxxtest4", BTRFS_SNAPSHOT_LOCK_BSD);
+ if (r < 0)
+ log_error_errno(r, "Failed to make snapshot: %m");
+ if (r >= 0)
+ assert_se(xopenat_lock(AT_FDCWD, "/xxxtest4", 0, 0, 0, LOCK_BSD, LOCK_EX|LOCK_NB) == -EAGAIN);
+
+ safe_close(r);
+
+ r = btrfs_subvol_remove("/xxxtest", BTRFS_REMOVE_QUOTA);
+ if (r < 0)
+ log_error_errno(r, "Failed to remove subvolume: %m");
+
+ r = btrfs_subvol_remove("/xxxtest2", BTRFS_REMOVE_QUOTA);
+ if (r < 0)
+ log_error_errno(r, "Failed to remove subvolume: %m");
+
+ r = btrfs_subvol_remove("/xxxtest3", BTRFS_REMOVE_QUOTA);
+ if (r < 0)
+ log_error_errno(r, "Failed to remove subvolume: %m");
+
+ r = btrfs_subvol_remove("/xxxtest4", BTRFS_REMOVE_QUOTA);
+ if (r < 0)
+ log_error_errno(r, "Failed to remove subvolume: %m");
+
+ r = btrfs_subvol_snapshot_at(AT_FDCWD, "/etc", AT_FDCWD, "/etc2",
+ BTRFS_SNAPSHOT_READ_ONLY|BTRFS_SNAPSHOT_FALLBACK_COPY);
+ if (r < 0)
+ log_error_errno(r, "Failed to make snapshot: %m");
+
+ r = btrfs_subvol_remove("/etc2", BTRFS_REMOVE_QUOTA);
+ if (r < 0)
+ log_error_errno(r, "Failed to remove subvolume: %m");
+
+ r = btrfs_subvol_make(AT_FDCWD, "/xxxrectest");
+ if (r < 0)
+ log_error_errno(r, "Failed to make subvolume: %m");
+
+ r = btrfs_subvol_make(AT_FDCWD, "/xxxrectest/xxxrectest2");
+ if (r < 0)
+ log_error_errno(r, "Failed to make subvolume: %m");
+
+ r = btrfs_subvol_make(AT_FDCWD, "/xxxrectest/xxxrectest3");
+ if (r < 0)
+ log_error_errno(r, "Failed to make subvolume: %m");
+
+ r = btrfs_subvol_make(AT_FDCWD, "/xxxrectest/xxxrectest3/sub");
+ if (r < 0)
+ log_error_errno(r, "Failed to make subvolume: %m");
+
+ if (mkdir("/xxxrectest/dir", 0755) < 0)
+ log_error_errno(errno, "Failed to make directory: %m");
+
+ r = btrfs_subvol_make(AT_FDCWD, "/xxxrectest/dir/xxxrectest4");
+ if (r < 0)
+ log_error_errno(r, "Failed to make subvolume: %m");
+
+ if (mkdir("/xxxrectest/dir/xxxrectest4/dir", 0755) < 0)
+ log_error_errno(errno, "Failed to make directory: %m");
+
+ r = btrfs_subvol_make(AT_FDCWD, "/xxxrectest/dir/xxxrectest4/dir/xxxrectest5");
+ if (r < 0)
+ log_error_errno(r, "Failed to make subvolume: %m");
+
+ if (mkdir("/xxxrectest/mnt", 0755) < 0)
+ log_error_errno(errno, "Failed to make directory: %m");
+
+ r = btrfs_subvol_snapshot_at(AT_FDCWD, "/xxxrectest", AT_FDCWD, "/xxxrectest2", BTRFS_SNAPSHOT_RECURSIVE);
+ if (r < 0)
+ log_error_errno(r, "Failed to snapshot subvolume: %m");
+
+ r = btrfs_subvol_remove("/xxxrectest", BTRFS_REMOVE_QUOTA|BTRFS_REMOVE_RECURSIVE);
+ if (r < 0)
+ log_error_errno(r, "Failed to recursively remove subvolume: %m");
+
+ r = btrfs_subvol_remove("/xxxrectest2", BTRFS_REMOVE_QUOTA|BTRFS_REMOVE_RECURSIVE);
+ if (r < 0)
+ log_error_errno(r, "Failed to recursively remove subvolume: %m");
+
+ r = btrfs_subvol_make(AT_FDCWD, "/xxxquotatest");
+ if (r < 0)
+ log_error_errno(r, "Failed to make subvolume: %m");
+
+ r = btrfs_subvol_auto_qgroup("/xxxquotatest", 0, true);
+ if (r < 0)
+ log_error_errno(r, "Failed to set up auto qgroup: %m");
+
+ r = btrfs_subvol_make(AT_FDCWD, "/xxxquotatest/beneath");
+ if (r < 0)
+ log_error_errno(r, "Failed to make subvolume: %m");
+
+ r = btrfs_subvol_auto_qgroup("/xxxquotatest/beneath", 0, false);
+ if (r < 0)
+ log_error_errno(r, "Failed to set up auto qgroup: %m");
+
+ r = btrfs_qgroup_set_limit("/xxxquotatest/beneath", 0, 4ULL * 1024 * 1024 * 1024);
+ if (r < 0)
+ log_error_errno(r, "Failed to set up quota limit: %m");
+
+ r = btrfs_subvol_set_subtree_quota_limit("/xxxquotatest", 0, 5ULL * 1024 * 1024 * 1024);
+ if (r < 0)
+ log_error_errno(r, "Failed to set up quota limit: %m");
+
+ r = btrfs_subvol_snapshot_at(AT_FDCWD, "/xxxquotatest", AT_FDCWD, "/xxxquotatest2",
+ BTRFS_SNAPSHOT_RECURSIVE|BTRFS_SNAPSHOT_QUOTA);
+ if (r < 0)
+ log_error_errno(r, "Failed to set up snapshot: %m");
+
+ r = btrfs_qgroup_get_quota("/xxxquotatest2/beneath", 0, &quota);
+ if (r < 0)
+ log_error_errno(r, "Failed to query quota: %m");
+
+ if (r >= 0)
+ assert_se(quota.referenced_max == 4ULL * 1024 * 1024 * 1024);
+
+ r = btrfs_subvol_get_subtree_quota("/xxxquotatest2", 0, &quota);
+ if (r < 0)
+ log_error_errno(r, "Failed to query quota: %m");
+
+ if (r >= 0)
+ assert_se(quota.referenced_max == 5ULL * 1024 * 1024 * 1024);
+
+ r = btrfs_subvol_remove("/xxxquotatest", BTRFS_REMOVE_QUOTA|BTRFS_REMOVE_RECURSIVE);
+ if (r < 0)
+ log_error_errno(r, "Failed remove subvolume: %m");
+
+ r = btrfs_subvol_remove("/xxxquotatest2", BTRFS_REMOVE_QUOTA|BTRFS_REMOVE_RECURSIVE);
+ if (r < 0)
+ log_error_errno(r, "Failed remove subvolume: %m");
+
+ return 0;
+}
diff --git a/src/test/test-bus-util.c b/src/test/test-bus-util.c
new file mode 100644
index 0000000..2f52bca
--- /dev/null
+++ b/src/test/test-bus-util.c
@@ -0,0 +1,47 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "bus-util.h"
+#include "log.h"
+#include "tests.h"
+
+static int callback(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
+ return 1;
+}
+
+static void destroy_callback(void *userdata) {
+ int *n_called = userdata;
+
+ (*n_called) ++;
+}
+
+TEST(destroy_callback) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ sd_bus_slot *slot = NULL;
+ sd_bus_destroy_t t;
+
+ int r, n_called = 0;
+
+ r = bus_open_system_watch_bind_with_description(&bus, "test-bus");
+ if (r < 0) {
+ log_error_errno(r, "Failed to connect to bus: %m");
+ return;
+ }
+
+ r = sd_bus_request_name_async(bus, &slot, "org.freedesktop.systemd.test-bus-util", 0, callback, &n_called);
+ assert_se(r == 1);
+
+ assert_se(sd_bus_slot_get_destroy_callback(slot, NULL) == 0);
+ assert_se(sd_bus_slot_get_destroy_callback(slot, &t) == 0);
+
+ assert_se(sd_bus_slot_set_destroy_callback(slot, destroy_callback) == 0);
+ assert_se(sd_bus_slot_get_destroy_callback(slot, NULL) == 1);
+ assert_se(sd_bus_slot_get_destroy_callback(slot, &t) == 1);
+ assert_se(t == destroy_callback);
+
+ /* Force cleanup so we can look at n_called */
+ assert_se(n_called == 0);
+ sd_bus_slot_unref(slot);
+ assert_se(n_called == 1);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-calendarspec.c b/src/test/test-calendarspec.c
new file mode 100644
index 0000000..18a0f8f
--- /dev/null
+++ b/src/test/test-calendarspec.c
@@ -0,0 +1,264 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "calendarspec.h"
+#include "env-util.h"
+#include "errno-util.h"
+#include "string-util.h"
+#include "tests.h"
+
+static void _test_one(int line, const char *input, const char *output) {
+ _cleanup_(calendar_spec_freep) CalendarSpec *c = NULL;
+ _cleanup_free_ char *p = NULL, *q = NULL;
+ usec_t u;
+ int r;
+
+ r = calendar_spec_from_string(input, &c);
+ if (r < 0)
+ log_error_errno(r, "Failed to parse \"%s\": %m", input);
+ assert_se(r >= 0);
+
+ assert_se(calendar_spec_to_string(c, &p) >= 0);
+ log_info("line %d: \"%s\" → \"%s\"%s%s", line, input, p,
+ !streq(p, output) ? " expected:" : "",
+ !streq(p, output) ? output : "");
+
+ assert_se(streq(p, output));
+
+ u = now(CLOCK_REALTIME);
+ r = calendar_spec_next_usec(c, u, &u);
+ log_info("Next: %s", r < 0 ? STRERROR(r) : FORMAT_TIMESTAMP(u));
+ c = calendar_spec_free(c);
+
+ assert_se(calendar_spec_from_string(p, &c) >= 0);
+ assert_se(calendar_spec_to_string(c, &q) >= 0);
+
+ assert_se(streq(q, p));
+}
+#define test_one(input, output) _test_one(__LINE__, input, output)
+
+static void _test_next(int line, const char *input, const char *new_tz, usec_t after, usec_t expect) {
+ _cleanup_(calendar_spec_freep) CalendarSpec *c = NULL;
+ usec_t u;
+ char *old_tz;
+ int r;
+
+ old_tz = getenv("TZ");
+ if (old_tz)
+ old_tz = strdupa_safe(old_tz);
+
+ if (!isempty(new_tz))
+ new_tz = strjoina(":", new_tz);
+
+ assert_se(set_unset_env("TZ", new_tz, true) == 0);
+ tzset();
+
+ assert_se(calendar_spec_from_string(input, &c) >= 0);
+
+ log_info("line %d: \"%s\" new_tz=%s", line, input, strnull(new_tz));
+
+ u = after;
+ r = calendar_spec_next_usec(c, after, &u);
+ log_info("At: %s", r < 0 ? STRERROR(r) : FORMAT_TIMESTAMP_STYLE(u, TIMESTAMP_US));
+ if (expect != USEC_INFINITY)
+ assert_se(r >= 0 && u == expect);
+ else
+ assert_se(r == -ENOENT);
+
+ assert_se(set_unset_env("TZ", old_tz, true) == 0);
+ tzset();
+}
+#define test_next(input, new_tz, after, expect) _test_next(__LINE__, input,new_tz,after,expect)
+
+TEST(timestamp) {
+ char buf[FORMAT_TIMESTAMP_MAX];
+ _cleanup_free_ char *t = NULL;
+ _cleanup_(calendar_spec_freep) CalendarSpec *c = NULL;
+ usec_t x, y;
+
+ /* Ensure that a timestamp is also a valid calendar specification. Convert forth and back */
+
+ x = now(CLOCK_REALTIME);
+
+ assert_se(format_timestamp_style(buf, sizeof buf, x, TIMESTAMP_US));
+ log_info("%s", buf);
+ assert_se(calendar_spec_from_string(buf, &c) >= 0);
+ assert_se(calendar_spec_to_string(c, &t) >= 0);
+ log_info("%s", t);
+
+ assert_se(parse_timestamp(t, &y) >= 0);
+ assert_se(y == x);
+}
+
+TEST(hourly_bug_4031) {
+ _cleanup_(calendar_spec_freep) CalendarSpec *c = NULL;
+ usec_t n, u, w;
+ int r;
+
+ assert_se(calendar_spec_from_string("hourly", &c) >= 0);
+ n = now(CLOCK_REALTIME);
+ assert_se((r = calendar_spec_next_usec(c, n, &u)) >= 0);
+
+ log_info("Now: %s (%"PRIu64")", FORMAT_TIMESTAMP_STYLE(n, TIMESTAMP_US), n);
+ log_info("Next hourly: %s (%"PRIu64")", r < 0 ? STRERROR(r) : FORMAT_TIMESTAMP_STYLE(u, TIMESTAMP_US), u);
+
+ assert_se((r = calendar_spec_next_usec(c, u, &w)) >= 0);
+ log_info("Next hourly: %s (%"PRIu64")", r < 0 ? STRERROR(r) : FORMAT_TIMESTAMP_STYLE(w, TIMESTAMP_US), w);
+
+ assert_se(n < u);
+ assert_se(u <= n + USEC_PER_HOUR);
+ assert_se(u < w);
+ assert_se(w <= u + USEC_PER_HOUR);
+}
+
+TEST(calendar_spec_one) {
+ test_one("Sat,Thu,Mon-Wed,Sat-Sun", "Mon..Thu,Sat,Sun *-*-* 00:00:00");
+ test_one("Sat,Thu,Mon..Wed,Sat..Sun", "Mon..Thu,Sat,Sun *-*-* 00:00:00");
+ test_one("Mon,Sun 12-*-* 2,1:23", "Mon,Sun 2012-*-* 01,02:23:00");
+ test_one("Wed *-1", "Wed *-*-01 00:00:00");
+ test_one("Wed-Wed,Wed *-1", "Wed *-*-01 00:00:00");
+ test_one("Wed..Wed,Wed *-1", "Wed *-*-01 00:00:00");
+ test_one("Wed, 17:48", "Wed *-*-* 17:48:00");
+ test_one("Wednesday,", "Wed *-*-* 00:00:00");
+ test_one("Wed-Sat,Tue 12-10-15 1:2:3", "Tue..Sat 2012-10-15 01:02:03");
+ test_one("Wed..Sat,Tue 12-10-15 1:2:3", "Tue..Sat 2012-10-15 01:02:03");
+ test_one("*-*-7 0:0:0", "*-*-07 00:00:00");
+ test_one("10-15", "*-10-15 00:00:00");
+ test_one("monday *-12-* 17:00", "Mon *-12-* 17:00:00");
+ test_one("Mon,Fri *-*-3,1,2 *:30:45", "Mon,Fri *-*-01,02,03 *:30:45");
+ test_one("12,14,13,12:20,10,30", "*-*-* 12,13,14:10,20,30:00");
+ test_one("mon,fri *-1/2-1,3 *:30:45", "Mon,Fri *-01/2-01,03 *:30:45");
+ test_one("03-05 08:05:40", "*-03-05 08:05:40");
+ test_one("08:05:40", "*-*-* 08:05:40");
+ test_one("05:40", "*-*-* 05:40:00");
+ test_one("Sat,Sun 12-05 08:05:40", "Sat,Sun *-12-05 08:05:40");
+ test_one("Sat,Sun 08:05:40", "Sat,Sun *-*-* 08:05:40");
+ test_one("2003-03-05 05:40", "2003-03-05 05:40:00");
+ test_one("2003-03-05", "2003-03-05 00:00:00");
+ test_one("03-05", "*-03-05 00:00:00");
+ test_one("hourly", "*-*-* *:00:00");
+ test_one("daily", "*-*-* 00:00:00");
+ test_one("monthly", "*-*-01 00:00:00");
+ test_one("weekly", "Mon *-*-* 00:00:00");
+ test_one("minutely", "*-*-* *:*:00");
+ test_one("quarterly", "*-01,04,07,10-01 00:00:00");
+ test_one("semi-annually", "*-01,07-01 00:00:00");
+ test_one("annually", "*-01-01 00:00:00");
+ test_one("*:2/3", "*-*-* *:02/3:00");
+ test_one("2015-10-25 01:00:00 uTc", "2015-10-25 01:00:00 UTC");
+ test_one("2015-10-25 01:00:00 Asia/Vladivostok", "2015-10-25 01:00:00 Asia/Vladivostok");
+ test_one("weekly Pacific/Auckland", "Mon *-*-* 00:00:00 Pacific/Auckland");
+ test_one("2016-03-27 03:17:00.4200005", "2016-03-27 03:17:00.420001");
+ test_one("2016-03-27 03:17:00/0.42", "2016-03-27 03:17:00/0.420000");
+ test_one("9..11,13:00,30", "*-*-* 09..11,13:00,30:00");
+ test_one("1..3-1..3 1..3:1..3", "*-01..03-01..03 01..03:01..03:00");
+ test_one("00:00:1.125..2.125", "*-*-* 00:00:01.125000..02.125000");
+ test_one("00:00:1.0..3.8", "*-*-* 00:00:01..03");
+ test_one("00:00:01..03", "*-*-* 00:00:01..03");
+ test_one("00:00:01/2,02..03", "*-*-* 00:00:01/2,02..03");
+ test_one("*:4,30:0..3", "*-*-* *:04,30:00..03");
+ test_one("*:4,30:0/1", "*-*-* *:04,30:*");
+ test_one("*:4,30:0/1,3,5", "*-*-* *:04,30:*");
+ test_one("*-*~1 Utc", "*-*~01 00:00:00 UTC");
+ test_one("*-*~05,3 ", "*-*~03,05 00:00:00");
+ test_one("*-*~* 00:00:00", "*-*-* 00:00:00");
+ test_one("Monday", "Mon *-*-* 00:00:00");
+ test_one("Monday *-*-*", "Mon *-*-* 00:00:00");
+ test_one("*-*-*", "*-*-* 00:00:00");
+ test_one("*:*:*", "*-*-* *:*:*");
+ test_one("*:*", "*-*-* *:*:00");
+ test_one("12:*", "*-*-* 12:*:00");
+ test_one("*:30", "*-*-* *:30:00");
+ test_one("93..00-*-*", "1993..2000-*-* 00:00:00");
+ test_one("00..07-*-*", "2000..2007-*-* 00:00:00");
+ test_one("*:20..39/5", "*-*-* *:20..35/5:00");
+ test_one("00:00:20..40/1", "*-*-* 00:00:20..40");
+ test_one("*~03/1,03..05", "*-*~03/1,03..05 00:00:00");
+ /* UNIX timestamps are always UTC */
+ test_one("@1493187147", "2017-04-26 06:12:27 UTC");
+ test_one("@1493187147 UTC", "2017-04-26 06:12:27 UTC");
+ test_one("@0", "1970-01-01 00:00:00 UTC");
+ test_one("@0 UTC", "1970-01-01 00:00:00 UTC");
+ test_one("*:05..05", "*-*-* *:05:00");
+ test_one("*:05..10/6", "*-*-* *:05:00");
+}
+
+TEST(calendar_spec_next) {
+ test_next("2016-03-27 03:17:00", "", 12345, 1459048620000000);
+ test_next("2016-03-27 03:17:00", "CET", 12345, 1459041420000000);
+ test_next("2016-03-27 03:17:00", "EET", 12345, -1);
+ test_next("2016-03-27 03:17:00 UTC", NULL, 12345, 1459048620000000);
+ test_next("2016-03-27 03:17:00 UTC", "", 12345, 1459048620000000);
+ test_next("2016-03-27 03:17:00 UTC", "CET", 12345, 1459048620000000);
+ test_next("2016-03-27 03:17:00 UTC", "EET", 12345, 1459048620000000);
+ test_next("2016-03-27 03:17:00.420000001 UTC", "EET", 12345, 1459048620420000);
+ test_next("2016-03-27 03:17:00.4200005 UTC", "EET", 12345, 1459048620420001);
+ test_next("2015-11-13 09:11:23.42", "EET", 12345, 1447398683420000);
+ test_next("2015-11-13 09:11:23.42/1.77", "EET", 1447398683420000, 1447398685190000);
+ test_next("2015-11-13 09:11:23.42/1.77", "EET", 1447398683419999, 1447398683420000);
+ test_next("Sun 16:00:00", "CET", 1456041600123456, 1456066800000000);
+ test_next("*-04-31", "", 12345, -1);
+ test_next("2016-02~01 UTC", "", 12345, 1456704000000000);
+ test_next("Mon 2017-05~01..07 UTC", "", 12345, 1496016000000000);
+ test_next("Mon 2017-05~07/1 UTC", "", 12345, 1496016000000000);
+ test_next("*-*-01/5 04:00:00 UTC", "", 1646010000000000, 1646107200000000);
+ test_next("*-01/7-01 04:00:00 UTC", "", 1664607600000000, 1672545600000000);
+ test_next("2017-08-06 9,11,13,15,17:00 UTC", "", 1502029800000000, 1502031600000000);
+ test_next("2017-08-06 9..17/2:00 UTC", "", 1502029800000000, 1502031600000000);
+ test_next("2016-12-* 3..21/6:00 UTC", "", 1482613200000001, 1482634800000000);
+ test_next("2017-09-24 03:30:00 Pacific/Auckland", "", 12345, 1506177000000000);
+ /* Due to daylight saving time - 2017-09-24 02:30:00 does not exist */
+ test_next("2017-09-24 02:30:00 Pacific/Auckland", "", 12345, -1);
+ test_next("2017-04-02 02:30:00 Pacific/Auckland", "", 12345, 1491053400000000);
+ /* Confirm that even though it's a time change here (backward) 02:30 happens only once */
+ test_next("2017-04-02 02:30:00 Pacific/Auckland", "", 1491053400000000, -1);
+ test_next("2017-04-02 03:30:00 Pacific/Auckland", "", 12345, 1491060600000000);
+ /* Confirm that timezones in the Spec work regardless of current timezone */
+ test_next("2017-09-09 20:42:00 Pacific/Auckland", "", 12345, 1504946520000000);
+ test_next("2017-09-09 20:42:00 Pacific/Auckland", "EET", 12345, 1504946520000000);
+ /* Check that we don't start looping if mktime() moves us backwards */
+ test_next("Sun *-*-* 01:00:00 Europe/Dublin", "", 1616412478000000, 1617494400000000);
+ test_next("Sun *-*-* 01:00:00 Europe/Dublin", "IST", 1616412478000000, 1617494400000000);
+}
+
+TEST(calendar_spec_from_string) {
+ CalendarSpec *c;
+
+ assert_se(calendar_spec_from_string("test", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string(" utc", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string(" ", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("7", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("121212:1:2", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("2000-03-05.23 00:00:00", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("2000-03-05 00:00.1:00", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("00:00:00/0.00000001", &c) == -ERANGE);
+ assert_se(calendar_spec_from_string("00:00:00.0..00.9", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("2016~11-22", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("*-*~5/5", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("Monday.. 12:00", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("Monday..", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("-00:+00/-5", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("00:+00/-5", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("2016- 11- 24 12: 30: 00", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("*~29", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("*~16..31", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("12..1/2-*", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("20/4:00", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("00:00/60", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("00:00:2300", &c) == -ERANGE);
+ assert_se(calendar_spec_from_string("00:00:18446744073709551615", &c) == -ERANGE);
+ assert_se(calendar_spec_from_string("@88588582097858858", &c) == -ERANGE);
+ assert_se(calendar_spec_from_string("*:4,30:*,5", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("*:4,30:5,*", &c) == -EINVAL);
+ assert_se(calendar_spec_from_string("*:4,30:*\n", &c) == -EINVAL);
+}
+
+static int intro(void) {
+ /* Tests have hard-coded results that do not expect a specific timezone to be set by the caller */
+ assert_se(unsetenv("TZ") >= 0);
+
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_INFO, intro);
diff --git a/src/test/test-cap-list.c b/src/test/test-cap-list.c
new file mode 100644
index 0000000..a9cbf69
--- /dev/null
+++ b/src/test/test-cap-list.c
@@ -0,0 +1,177 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+#include <sys/prctl.h>
+
+#include "alloc-util.h"
+#include "cap-list.h"
+#include "capability-util.h"
+#include "parse-util.h"
+#include "random-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+
+/* verify the capability parser */
+TEST(cap_list) {
+ assert_se(!capability_to_name(-1));
+ assert_se(!capability_to_name(capability_list_length()));
+ assert_se(!capability_to_name(63));
+ assert_se(!capability_to_name(64));
+
+ assert_se(!CAPABILITY_TO_STRING(-1));
+ if (capability_list_length() <= 62)
+ assert_se(streq(CAPABILITY_TO_STRING(62), "0x3e"));
+ assert_se(!CAPABILITY_TO_STRING(64));
+
+ for (int i = 0; i < capability_list_length(); i++) {
+ const char *n;
+
+ assert_se(n = capability_to_name(i));
+ assert_se(capability_from_name(n) == i);
+ printf("%s = %i\n", n, i);
+
+ assert_se(streq(CAPABILITY_TO_STRING(i), n));
+ }
+
+ assert_se(capability_from_name("asdfbsd") == -EINVAL);
+ assert_se(capability_from_name("CAP_AUDIT_READ") == CAP_AUDIT_READ);
+ assert_se(capability_from_name("cap_audit_read") == CAP_AUDIT_READ);
+ assert_se(capability_from_name("cAp_aUdIt_rEAd") == CAP_AUDIT_READ);
+ assert_se(capability_from_name("0") == 0);
+ assert_se(capability_from_name("15") == 15);
+ assert_se(capability_from_name("62") == 62);
+ assert_se(capability_from_name("63") == -EINVAL);
+ assert_se(capability_from_name("64") == -EINVAL);
+ assert_se(capability_from_name("-1") == -EINVAL);
+
+ for (int i = 0; i < capability_list_length(); i++) {
+ _cleanup_cap_free_charp_ char *a = NULL;
+ const char *b;
+ unsigned u;
+
+ assert_se(a = cap_to_name(i));
+
+ /* quit the loop as soon as libcap starts returning
+ * numeric ids, formatted as strings */
+ if (safe_atou(a, &u) >= 0)
+ break;
+
+ assert_se(b = capability_to_name(i));
+
+ printf("%s vs. %s\n", a, b);
+
+ assert_se(strcasecmp(a, b) == 0);
+ }
+}
+
+static void test_capability_set_one(uint64_t c, const char *t) {
+ _cleanup_free_ char *t1 = NULL;
+ uint64_t c1, c_masked = c & all_capabilities();
+
+ assert_se(capability_set_to_string(c, &t1) == 0);
+ assert_se(streq(t1, t));
+
+ assert_se(capability_set_from_string(t1, &c1) > 0);
+ assert_se(c1 == c_masked);
+
+ free(t1);
+ assert_se(t1 = strjoin("'cap_chown cap_dac_override' \"cap_setgid cap_setuid\"", t,
+ " hogehoge foobar 18446744073709551616 3.14 -3 ", t));
+ assert_se(capability_set_from_string(t1, &c1) == 0);
+ assert_se(c1 == c_masked);
+}
+
+TEST(capability_set_from_string) {
+ uint64_t c;
+
+ assert_se(capability_set_from_string(NULL, &c) > 0);
+ assert_se(c == 0);
+
+ assert_se(capability_set_from_string("", &c) > 0);
+ assert_se(c == 0);
+
+ assert_se(capability_set_from_string("0", &c) > 0);
+ assert_se(c == UINT64_C(1));
+
+ assert_se(capability_set_from_string("1", &c) > 0);
+ assert_se(c == UINT64_C(1) << 1);
+
+ assert_se(capability_set_from_string("0 1 2 3", &c) > 0);
+ assert_se(c == (UINT64_C(1) << 4) - 1);
+}
+
+static void test_capability_set_to_strv_one(uint64_t m, char **l) {
+ _cleanup_strv_free_ char **b = NULL;
+
+ assert_se(capability_set_to_strv(m, &b) >= 0);
+ assert_se(strv_equal(l, b));
+}
+
+TEST(capability_set_to_strv) {
+ test_capability_set_to_strv_one(0, STRV_MAKE(NULL));
+ test_capability_set_to_strv_one(UINT64_C(1) << CAP_MKNOD, STRV_MAKE("cap_mknod"));
+ test_capability_set_to_strv_one((UINT64_C(1) << CAP_MKNOD) |
+ (UINT64_C(1) << CAP_NET_BIND_SERVICE), STRV_MAKE("cap_net_bind_service", "cap_mknod"));
+ test_capability_set_to_strv_one((UINT64_C(1) << CAP_MKNOD) |
+ (UINT64_C(1) << CAP_NET_BIND_SERVICE) |
+ (UINT64_C(1) << CAP_IPC_OWNER), STRV_MAKE("cap_net_bind_service", "cap_ipc_owner", "cap_mknod"));
+}
+
+static void test_capability_set_to_string_invalid(uint64_t invalid_cap_set) {
+ uint64_t c;
+
+ test_capability_set_one(invalid_cap_set, "");
+
+ c = (UINT64_C(1) << CAP_DAC_OVERRIDE | invalid_cap_set);
+ test_capability_set_one(c, "cap_dac_override");
+
+ c = (UINT64_C(1) << CAP_CHOWN |
+ UINT64_C(1) << CAP_DAC_OVERRIDE |
+ UINT64_C(1) << CAP_DAC_READ_SEARCH |
+ UINT64_C(1) << CAP_FOWNER |
+ UINT64_C(1) << CAP_SETGID |
+ UINT64_C(1) << CAP_SETUID |
+ UINT64_C(1) << CAP_SYS_PTRACE |
+ UINT64_C(1) << CAP_SYS_ADMIN |
+ UINT64_C(1) << CAP_AUDIT_CONTROL |
+ UINT64_C(1) << CAP_MAC_OVERRIDE |
+ UINT64_C(1) << CAP_SYSLOG |
+ invalid_cap_set);
+ test_capability_set_one(c, ("cap_chown cap_dac_override cap_dac_read_search cap_fowner "
+ "cap_setgid cap_setuid cap_sys_ptrace cap_sys_admin "
+ "cap_audit_control cap_mac_override cap_syslog"));
+}
+
+TEST(capability_set_to_string) {
+ test_capability_set_to_string_invalid(0);
+
+ /* once the kernel supports 62 caps, there are no 'invalid' numbers
+ * for us to test with */
+ if (cap_last_cap() < 62)
+ test_capability_set_to_string_invalid(all_capabilities() + 1);
+}
+
+TEST(capability_set_to_string_negative) {
+
+ for (unsigned i = 0; i < 150; i++) {
+ _cleanup_free_ char *a = NULL, *b = NULL;
+
+ uint64_t m =
+ random_u64() % (UINT64_C(1) << (cap_last_cap() + 1));
+
+ assert_se(capability_set_to_string(m, &a) >= 0);
+ assert_se(capability_set_to_string_negative(m, &b) >= 0);
+
+ printf("%s (%zu) → ", a, strlen(a));
+
+ if (streq(a, b))
+ printf("same\n");
+ else
+ printf("%s (%zu)\n", b, strlen(b));
+
+ assert_se(strlen(b) <= strlen(a));
+ }
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-capability.c b/src/test/test-capability.c
new file mode 100644
index 0000000..e8a0569
--- /dev/null
+++ b/src/test/test-capability.c
@@ -0,0 +1,332 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <pwd.h>
+#include <sys/prctl.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#define TEST_CAPABILITY_C
+
+#include "alloc-util.h"
+#include "capability-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "macro.h"
+#include "missing_prctl.h"
+#include "parse-util.h"
+#include "process-util.h"
+#include "string-util.h"
+#include "tests.h"
+
+static uid_t test_uid = -1;
+static gid_t test_gid = -1;
+
+#if HAS_FEATURE_ADDRESS_SANITIZER
+/* Keep CAP_SYS_PTRACE when running under Address Sanitizer */
+static const uint64_t test_flags = UINT64_C(1) << CAP_SYS_PTRACE;
+#else
+/* We keep CAP_DAC_OVERRIDE to avoid errors with gcov when doing test coverage */
+static const uint64_t test_flags = UINT64_C(1) << CAP_DAC_OVERRIDE;
+#endif
+
+/* verify cap_last_cap() against /proc/sys/kernel/cap_last_cap */
+static void test_last_cap_file(void) {
+ _cleanup_free_ char *content = NULL;
+ unsigned long val = 0;
+ int r;
+
+ r = read_one_line_file("/proc/sys/kernel/cap_last_cap", &content);
+ if (r == -ENOENT || ERRNO_IS_NEG_PRIVILEGE(r)) /* kernel pre 3.2 or no access */
+ return;
+ assert_se(r >= 0);
+
+ r = safe_atolu(content, &val);
+ assert_se(r >= 0);
+ assert_se(val != 0);
+ assert_se(val == cap_last_cap());
+}
+
+/* verify cap_last_cap() against syscall probing */
+static void test_last_cap_probe(void) {
+ unsigned long p = (unsigned long)CAP_LAST_CAP;
+
+ if (prctl(PR_CAPBSET_READ, p) < 0) {
+ for (p--; p > 0; p --)
+ if (prctl(PR_CAPBSET_READ, p) >= 0)
+ break;
+ } else {
+ for (;; p++)
+ if (prctl(PR_CAPBSET_READ, p+1) < 0)
+ break;
+ }
+
+ assert_se(p != 0);
+ assert_se(p == cap_last_cap());
+}
+
+static void fork_test(void (*test_func)(void)) {
+ pid_t pid = 0;
+
+ pid = fork();
+ assert_se(pid >= 0);
+ if (pid == 0) {
+ test_func();
+ exit(EXIT_SUCCESS);
+ } else if (pid > 0) {
+ int status;
+
+ assert_se(waitpid(pid, &status, 0) > 0);
+ assert_se(WIFEXITED(status) && WEXITSTATUS(status) == 0);
+ }
+}
+
+static void show_capabilities(void) {
+ cap_t caps;
+ char *text;
+
+ caps = cap_get_proc();
+ assert_se(caps);
+
+ text = cap_to_text(caps, NULL);
+ assert_se(text);
+
+ log_info("Capabilities:%s", text);
+ cap_free(caps);
+ cap_free(text);
+}
+
+static int setup_tests(bool *run_ambient) {
+ struct passwd *nobody;
+ int r;
+
+ nobody = getpwnam(NOBODY_USER_NAME);
+ if (!nobody)
+ return log_warning_errno(SYNTHETIC_ERRNO(ENOENT), "Couldn't find 'nobody' user: %m");
+
+ test_uid = nobody->pw_uid;
+ test_gid = nobody->pw_gid;
+
+ r = prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0);
+ /* There's support for PR_CAP_AMBIENT if the prctl() call succeeded or error code was something else
+ * than EINVAL. The EINVAL check should be good enough to rule out false positives. */
+ *run_ambient = r >= 0 || errno != EINVAL;
+
+ return 0;
+}
+
+static void test_drop_privileges_keep_net_raw(void) {
+ int sock;
+
+ sock = socket(AF_INET, SOCK_RAW, IPPROTO_UDP);
+ assert_se(sock >= 0);
+ safe_close(sock);
+
+ assert_se(drop_privileges(test_uid, test_gid, test_flags | (1ULL << CAP_NET_RAW)) >= 0);
+ assert_se(getuid() == test_uid);
+ assert_se(getgid() == test_gid);
+ show_capabilities();
+
+ sock = socket(AF_INET, SOCK_RAW, IPPROTO_UDP);
+ assert_se(sock >= 0);
+ safe_close(sock);
+}
+
+static void test_drop_privileges_dontkeep_net_raw(void) {
+ int sock;
+
+ sock = socket(AF_INET, SOCK_RAW, IPPROTO_UDP);
+ assert_se(sock >= 0);
+ safe_close(sock);
+
+ assert_se(drop_privileges(test_uid, test_gid, test_flags) >= 0);
+ assert_se(getuid() == test_uid);
+ assert_se(getgid() == test_gid);
+ show_capabilities();
+
+ sock = socket(AF_INET, SOCK_RAW, IPPROTO_UDP);
+ assert_se(sock < 0);
+}
+
+static void test_drop_privileges_fail(void) {
+ assert_se(drop_privileges(test_uid, test_gid, test_flags) >= 0);
+ assert_se(getuid() == test_uid);
+ assert_se(getgid() == test_gid);
+
+ assert_se(drop_privileges(test_uid, test_gid, test_flags) < 0);
+ assert_se(drop_privileges(0, 0, test_flags) < 0);
+}
+
+static void test_drop_privileges(void) {
+ fork_test(test_drop_privileges_fail);
+
+ if (have_effective_cap(CAP_NET_RAW) <= 0) /* The remaining two tests only work if we have CAP_NET_RAW
+ * in the first place. If we are run in some restricted
+ * container environment we might not. */
+ return;
+
+ fork_test(test_drop_privileges_keep_net_raw);
+ fork_test(test_drop_privileges_dontkeep_net_raw);
+}
+
+static void test_have_effective_cap(void) {
+ assert_se(have_effective_cap(CAP_KILL) > 0);
+ assert_se(have_effective_cap(CAP_CHOWN) > 0);
+
+ assert_se(drop_privileges(test_uid, test_gid, test_flags | (1ULL << CAP_KILL)) >= 0);
+ assert_se(getuid() == test_uid);
+ assert_se(getgid() == test_gid);
+
+ assert_se(have_effective_cap(CAP_KILL) > 0);
+ assert_se(have_effective_cap(CAP_CHOWN) == 0);
+}
+
+static void test_update_inherited_set(void) {
+ cap_t caps;
+ uint64_t set = 0;
+ cap_flag_value_t fv;
+
+ caps = cap_get_proc();
+ assert_se(caps);
+
+ set = (UINT64_C(1) << CAP_CHOWN);
+
+ assert_se(!capability_update_inherited_set(caps, set));
+ assert_se(!cap_get_flag(caps, CAP_CHOWN, CAP_INHERITABLE, &fv));
+ assert_se(fv == CAP_SET);
+
+ cap_free(caps);
+}
+
+static void test_apply_ambient_caps(void) {
+ cap_t caps;
+ uint64_t set = 0;
+ cap_flag_value_t fv;
+
+ assert_se(prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_IS_SET, CAP_CHOWN, 0, 0) == 0);
+
+ set = (UINT64_C(1) << CAP_CHOWN);
+
+ assert_se(!capability_ambient_set_apply(set, true));
+
+ caps = cap_get_proc();
+ assert_se(caps);
+ assert_se(!cap_get_flag(caps, CAP_CHOWN, CAP_INHERITABLE, &fv));
+ assert_se(fv == CAP_SET);
+ cap_free(caps);
+
+ assert_se(prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_IS_SET, CAP_CHOWN, 0, 0) == 1);
+
+ assert_se(!capability_ambient_set_apply(0, true));
+ caps = cap_get_proc();
+ assert_se(caps);
+ assert_se(!cap_get_flag(caps, CAP_CHOWN, CAP_INHERITABLE, &fv));
+ assert_se(fv == CAP_CLEAR);
+ cap_free(caps);
+
+ assert_se(prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_IS_SET, CAP_CHOWN, 0, 0) == 0);
+}
+
+static void test_ensure_cap_64_bit(void) {
+ _cleanup_free_ char *content = NULL;
+ unsigned long p = 0;
+ int r;
+
+ r = read_one_line_file("/proc/sys/kernel/cap_last_cap", &content);
+ if (r == -ENOENT || ERRNO_IS_NEG_PRIVILEGE(r)) /* kernel pre 3.2 or no access */
+ return;
+ assert_se(r >= 0);
+
+ assert_se(safe_atolu(content, &p) >= 0);
+
+ /* If caps don't fit into 64-bit anymore, we have a problem, fail the test. */
+ assert_se(p <= 63);
+
+ /* Also check for the header definition */
+ assert_cc(CAP_LAST_CAP <= 63);
+}
+
+static void test_capability_get_ambient(void) {
+ uint64_t c;
+ int r;
+
+ assert_se(capability_get_ambient(&c) >= 0);
+
+ r = safe_fork("(getambient)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_WAIT|FORK_LOG, NULL);
+ assert_se(r >= 0);
+
+ if (r == 0) {
+ int x, y;
+ /* child */
+ assert_se(capability_get_ambient(&c) >= 0);
+
+ x = capability_ambient_set_apply(
+ (UINT64_C(1) << CAP_MKNOD)|
+ (UINT64_C(1) << CAP_LINUX_IMMUTABLE),
+ /* also_inherit= */ true);
+ assert_se(x >= 0 || ERRNO_IS_PRIVILEGE(x));
+
+ assert_se(capability_get_ambient(&c) >= 0);
+ assert_se(x < 0 || FLAGS_SET(c, UINT64_C(1) << CAP_MKNOD));
+ assert_se(x < 0 || FLAGS_SET(c, UINT64_C(1) << CAP_LINUX_IMMUTABLE));
+ assert_se(x < 0 || !FLAGS_SET(c, UINT64_C(1) << CAP_SETPCAP));
+
+ y = capability_bounding_set_drop(
+ ((UINT64_C(1) << CAP_LINUX_IMMUTABLE)|
+ (UINT64_C(1) << CAP_SETPCAP)),
+ /* right_now= */ true);
+ assert_se(y >= 0 || ERRNO_IS_PRIVILEGE(y));
+
+ assert_se(capability_get_ambient(&c) >= 0);
+ assert_se(x < 0 || y < 0 || !FLAGS_SET(c, UINT64_C(1) << CAP_MKNOD));
+ assert_se(x < 0 || y < 0 || FLAGS_SET(c, UINT64_C(1) << CAP_LINUX_IMMUTABLE));
+ assert_se(x < 0 || y < 0 || !FLAGS_SET(c, UINT64_C(1) << CAP_SETPCAP));
+
+ y = capability_bounding_set_drop(
+ (UINT64_C(1) << CAP_SETPCAP),
+ /* right_now= */ true);
+ assert_se(y >= 0 || ERRNO_IS_PRIVILEGE(y));
+
+ assert_se(capability_get_ambient(&c) >= 0);
+ assert_se(x < 0 || y < 0 || !FLAGS_SET(c, UINT64_C(1) << CAP_MKNOD));
+ assert_se(x < 0 || y < 0 || !FLAGS_SET(c, UINT64_C(1) << CAP_LINUX_IMMUTABLE));
+ assert_se(x < 0 || y < 0 || !FLAGS_SET(c, UINT64_C(1) << CAP_SETPCAP));
+
+ _exit(EXIT_SUCCESS);
+ }
+}
+
+int main(int argc, char *argv[]) {
+ bool run_ambient;
+
+ test_setup_logging(LOG_DEBUG);
+
+ test_ensure_cap_64_bit();
+
+ test_last_cap_file();
+ test_last_cap_probe();
+
+ log_info("have ambient caps: %s", yes_no(ambient_capabilities_supported()));
+
+ if (getuid() != 0)
+ return log_tests_skipped("not running as root");
+
+ if (setup_tests(&run_ambient) < 0)
+ return log_tests_skipped("setup failed");
+
+ show_capabilities();
+
+ test_drop_privileges();
+ test_update_inherited_set();
+
+ fork_test(test_have_effective_cap);
+
+ if (run_ambient)
+ fork_test(test_apply_ambient_caps);
+
+ test_capability_get_ambient();
+
+ return 0;
+}
diff --git a/src/test/test-cgroup-cpu.c b/src/test/test-cgroup-cpu.c
new file mode 100644
index 0000000..fcf84d3
--- /dev/null
+++ b/src/test/test-cgroup-cpu.c
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "cgroup.h"
+#include "log.h"
+#include "tests.h"
+
+TEST(group_cpu_adjust_period) {
+ /* Period 1ms, quota 40% -> Period 2.5ms */
+ assert_se(2500 == cgroup_cpu_adjust_period(USEC_PER_MSEC, 400 * USEC_PER_MSEC, USEC_PER_MSEC, USEC_PER_SEC));
+ /* Period 10ms, quota 10% -> keep. */
+ assert_se(10 * USEC_PER_MSEC == cgroup_cpu_adjust_period(10 * USEC_PER_MSEC, 100 * USEC_PER_MSEC, USEC_PER_MSEC, USEC_PER_SEC));
+ /* Period 1ms, quota 1000% -> keep. */
+ assert_se(USEC_PER_MSEC == cgroup_cpu_adjust_period(USEC_PER_MSEC, 10000 * USEC_PER_MSEC, USEC_PER_MSEC, USEC_PER_SEC));
+ /* Period 100ms, quota 30% -> keep. */
+ assert_se(100 * USEC_PER_MSEC == cgroup_cpu_adjust_period(100 * USEC_PER_MSEC, 300 * USEC_PER_MSEC, USEC_PER_MSEC, USEC_PER_SEC));
+ /* Period 5s, quota 40% -> adjust to 1s. */
+ assert_se(USEC_PER_SEC == cgroup_cpu_adjust_period(5 * USEC_PER_SEC, 400 * USEC_PER_MSEC, USEC_PER_MSEC, USEC_PER_SEC));
+ /* Period 2s, quota 250% -> adjust to 1s. */
+ assert_se(USEC_PER_SEC == cgroup_cpu_adjust_period(2 * USEC_PER_SEC, 2500 * USEC_PER_MSEC, USEC_PER_MSEC, USEC_PER_SEC));
+ /* Period 10us, quota 5,000,000% -> adjust to 1ms. */
+ assert_se(USEC_PER_MSEC == cgroup_cpu_adjust_period(10, 50000000 * USEC_PER_MSEC, USEC_PER_MSEC, USEC_PER_SEC));
+ /* Period 10ms, quota 50,000% -> keep. */
+ assert_se(10 * USEC_PER_MSEC == cgroup_cpu_adjust_period(10 * USEC_PER_MSEC, 500000 * USEC_PER_MSEC, USEC_PER_MSEC, USEC_PER_SEC));
+ /* Period 10ms, quota 1% -> adjust to 100ms. */
+ assert_se(100 * USEC_PER_MSEC == cgroup_cpu_adjust_period(10 * USEC_PER_MSEC, 10 * USEC_PER_MSEC, USEC_PER_MSEC, USEC_PER_SEC));
+ /* Period 10ms, quota .001% -> adjust to 1s. */
+ assert_se(1 * USEC_PER_SEC == cgroup_cpu_adjust_period(10 * USEC_PER_MSEC, 10, USEC_PER_MSEC, USEC_PER_SEC));
+ /* Period 0ms, quota 200% -> adjust to 1ms. */
+ assert_se(1 * USEC_PER_MSEC == cgroup_cpu_adjust_period(0, 2 * USEC_PER_SEC, USEC_PER_MSEC, USEC_PER_SEC));
+ /* Period 0ms, quota 40% -> adjust to 2.5ms. */
+ assert_se(2500 == cgroup_cpu_adjust_period(0, 400 * USEC_PER_MSEC, USEC_PER_MSEC, USEC_PER_SEC));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-cgroup-mask.c b/src/test/test-cgroup-mask.c
new file mode 100644
index 0000000..bfc8fac
--- /dev/null
+++ b/src/test/test-cgroup-mask.c
@@ -0,0 +1,184 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+
+#include "cgroup.h"
+#include "cgroup-util.h"
+#include "macro.h"
+#include "manager.h"
+#include "rm-rf.h"
+#include "string-util.h"
+#include "tests.h"
+#include "unit.h"
+
+#define ASSERT_CGROUP_MASK(got, expected) \
+ log_cgroup_mask(got, expected); \
+ assert_se(got == expected)
+
+#define ASSERT_CGROUP_MASK_JOINED(got, expected) ASSERT_CGROUP_MASK(got, CGROUP_MASK_EXTEND_JOINED(expected))
+
+static void log_cgroup_mask(CGroupMask got, CGroupMask expected) {
+ _cleanup_free_ char *e_store = NULL, *g_store = NULL;
+
+ assert_se(cg_mask_to_string(expected, &e_store) >= 0);
+ log_info("Expected mask: %s", e_store);
+ assert_se(cg_mask_to_string(got, &g_store) >= 0);
+ log_info("Got mask: %s", g_store);
+}
+
+TEST_RET(cgroup_mask, .sd_booted = true) {
+ _cleanup_(rm_rf_physical_and_freep) char *runtime_dir = NULL;
+ _cleanup_(manager_freep) Manager *m = NULL;
+ Unit *son, *daughter, *parent, *root, *grandchild, *parent_deep, *nomem_parent, *nomem_leaf;
+ int r;
+ CGroupMask cpu_accounting_mask = get_cpu_accounting_mask();
+
+ r = enter_cgroup_subroot(NULL);
+ if (r == -ENOMEDIUM)
+ return log_tests_skipped("cgroupfs not available");
+
+ /* Prepare the manager. */
+ _cleanup_free_ char *unit_dir = NULL;
+ assert_se(get_testdata_dir("units", &unit_dir) >= 0);
+ assert_se(set_unit_path(unit_dir) >= 0);
+ assert_se(runtime_dir = setup_fake_runtime_dir());
+ r = manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_BASIC, &m);
+ if (IN_SET(r, -EPERM, -EACCES)) {
+ log_error_errno(r, "manager_new: %m");
+ return log_tests_skipped("cannot create manager");
+ }
+
+ assert_se(r >= 0);
+
+ /* Turn off all kinds of default accounting, so that we can
+ * verify the masks resulting of our configuration and nothing
+ * else. */
+ m->defaults.cpu_accounting =
+ m->defaults.memory_accounting =
+ m->defaults.blockio_accounting =
+ m->defaults.io_accounting =
+ m->defaults.tasks_accounting = false;
+ m->defaults.tasks_max = CGROUP_TASKS_MAX_UNSET;
+
+ assert_se(manager_startup(m, NULL, NULL, NULL) >= 0);
+
+ /* Load units and verify hierarchy. */
+ assert_se(manager_load_startable_unit_or_warn(m, "parent.slice", NULL, &parent) >= 0);
+ assert_se(manager_load_startable_unit_or_warn(m, "son.service", NULL, &son) >= 0);
+ assert_se(manager_load_startable_unit_or_warn(m, "daughter.service", NULL, &daughter) >= 0);
+ assert_se(manager_load_startable_unit_or_warn(m, "grandchild.service", NULL, &grandchild) >= 0);
+ assert_se(manager_load_startable_unit_or_warn(m, "parent-deep.slice", NULL, &parent_deep) >= 0);
+ assert_se(manager_load_startable_unit_or_warn(m, "nomem.slice", NULL, &nomem_parent) >= 0);
+ assert_se(manager_load_startable_unit_or_warn(m, "nomemleaf.service", NULL, &nomem_leaf) >= 0);
+ assert_se(UNIT_GET_SLICE(son) == parent);
+ assert_se(UNIT_GET_SLICE(daughter) == parent);
+ assert_se(UNIT_GET_SLICE(parent_deep) == parent);
+ assert_se(UNIT_GET_SLICE(grandchild) == parent_deep);
+ assert_se(UNIT_GET_SLICE(nomem_leaf) == nomem_parent);
+ root = UNIT_GET_SLICE(parent);
+ assert_se(UNIT_GET_SLICE(nomem_parent) == root);
+
+ /* Verify per-unit cgroups settings. */
+ ASSERT_CGROUP_MASK_JOINED(unit_get_own_mask(son), CGROUP_MASK_CPU);
+ ASSERT_CGROUP_MASK_JOINED(unit_get_own_mask(daughter), cpu_accounting_mask);
+ ASSERT_CGROUP_MASK_JOINED(unit_get_own_mask(grandchild), 0);
+ ASSERT_CGROUP_MASK_JOINED(unit_get_own_mask(parent_deep), CGROUP_MASK_MEMORY);
+ ASSERT_CGROUP_MASK_JOINED(unit_get_own_mask(parent), (CGROUP_MASK_IO | CGROUP_MASK_BLKIO));
+ ASSERT_CGROUP_MASK_JOINED(unit_get_own_mask(nomem_parent), 0);
+ ASSERT_CGROUP_MASK_JOINED(unit_get_own_mask(nomem_leaf), (CGROUP_MASK_IO | CGROUP_MASK_BLKIO | CGROUP_MASK_MEMORY));
+ ASSERT_CGROUP_MASK_JOINED(unit_get_own_mask(root), 0);
+
+ /* Verify aggregation of member masks */
+ ASSERT_CGROUP_MASK_JOINED(unit_get_members_mask(son), 0);
+ ASSERT_CGROUP_MASK_JOINED(unit_get_members_mask(daughter), 0);
+ ASSERT_CGROUP_MASK_JOINED(unit_get_members_mask(grandchild), 0);
+ ASSERT_CGROUP_MASK_JOINED(unit_get_members_mask(parent_deep), 0);
+ ASSERT_CGROUP_MASK_JOINED(unit_get_members_mask(parent), (CGROUP_MASK_CPU | cpu_accounting_mask | CGROUP_MASK_MEMORY));
+ ASSERT_CGROUP_MASK_JOINED(unit_get_members_mask(nomem_parent), (CGROUP_MASK_IO | CGROUP_MASK_BLKIO | CGROUP_MASK_MEMORY));
+ ASSERT_CGROUP_MASK_JOINED(unit_get_members_mask(nomem_leaf), 0);
+ ASSERT_CGROUP_MASK_JOINED(unit_get_members_mask(root), (CGROUP_MASK_CPU | cpu_accounting_mask | CGROUP_MASK_IO | CGROUP_MASK_BLKIO | CGROUP_MASK_MEMORY));
+
+ /* Verify aggregation of sibling masks. */
+ ASSERT_CGROUP_MASK_JOINED(unit_get_siblings_mask(son), (CGROUP_MASK_CPU | cpu_accounting_mask | CGROUP_MASK_MEMORY));
+ ASSERT_CGROUP_MASK_JOINED(unit_get_siblings_mask(daughter), (CGROUP_MASK_CPU | cpu_accounting_mask | CGROUP_MASK_MEMORY));
+ ASSERT_CGROUP_MASK_JOINED(unit_get_siblings_mask(grandchild), 0);
+ ASSERT_CGROUP_MASK_JOINED(unit_get_siblings_mask(parent_deep), (CGROUP_MASK_CPU | cpu_accounting_mask | CGROUP_MASK_MEMORY));
+ ASSERT_CGROUP_MASK_JOINED(unit_get_siblings_mask(parent), (CGROUP_MASK_CPU | cpu_accounting_mask | CGROUP_MASK_IO | CGROUP_MASK_BLKIO | CGROUP_MASK_MEMORY));
+ ASSERT_CGROUP_MASK_JOINED(unit_get_siblings_mask(nomem_parent), (CGROUP_MASK_CPU | CGROUP_MASK_CPUACCT | CGROUP_MASK_IO | CGROUP_MASK_BLKIO | CGROUP_MASK_MEMORY));
+ ASSERT_CGROUP_MASK_JOINED(unit_get_siblings_mask(nomem_leaf), (CGROUP_MASK_IO | CGROUP_MASK_BLKIO | CGROUP_MASK_MEMORY));
+ ASSERT_CGROUP_MASK_JOINED(unit_get_siblings_mask(root), (CGROUP_MASK_CPU | cpu_accounting_mask | CGROUP_MASK_IO | CGROUP_MASK_BLKIO | CGROUP_MASK_MEMORY));
+
+ /* Verify aggregation of target masks. */
+ ASSERT_CGROUP_MASK(unit_get_target_mask(son), (CGROUP_MASK_EXTEND_JOINED(CGROUP_MASK_CPU | cpu_accounting_mask | CGROUP_MASK_MEMORY) & m->cgroup_supported));
+ ASSERT_CGROUP_MASK(unit_get_target_mask(daughter), (CGROUP_MASK_EXTEND_JOINED(CGROUP_MASK_CPU | cpu_accounting_mask | CGROUP_MASK_MEMORY) & m->cgroup_supported));
+ ASSERT_CGROUP_MASK(unit_get_target_mask(grandchild), 0);
+ ASSERT_CGROUP_MASK(unit_get_target_mask(parent_deep), (CGROUP_MASK_EXTEND_JOINED(CGROUP_MASK_CPU | cpu_accounting_mask | CGROUP_MASK_MEMORY) & m->cgroup_supported));
+ ASSERT_CGROUP_MASK(unit_get_target_mask(parent), (CGROUP_MASK_EXTEND_JOINED(CGROUP_MASK_CPU | cpu_accounting_mask | CGROUP_MASK_IO | CGROUP_MASK_BLKIO | CGROUP_MASK_MEMORY) & m->cgroup_supported));
+ ASSERT_CGROUP_MASK(unit_get_target_mask(nomem_parent), (CGROUP_MASK_EXTEND_JOINED(CGROUP_MASK_CPU | CGROUP_MASK_CPUACCT | CGROUP_MASK_IO | CGROUP_MASK_BLKIO) & m->cgroup_supported));
+ ASSERT_CGROUP_MASK(unit_get_target_mask(nomem_leaf), (CGROUP_MASK_EXTEND_JOINED(CGROUP_MASK_IO | CGROUP_MASK_BLKIO) & m->cgroup_supported));
+ ASSERT_CGROUP_MASK(unit_get_target_mask(root), (CGROUP_MASK_EXTEND_JOINED(CGROUP_MASK_CPU | cpu_accounting_mask | CGROUP_MASK_IO | CGROUP_MASK_BLKIO | CGROUP_MASK_MEMORY) & m->cgroup_supported));
+
+ /* Verify aggregation of enable masks. */
+ ASSERT_CGROUP_MASK(unit_get_enable_mask(son), 0);
+ ASSERT_CGROUP_MASK(unit_get_enable_mask(daughter), 0);
+ ASSERT_CGROUP_MASK(unit_get_enable_mask(grandchild), 0);
+ ASSERT_CGROUP_MASK(unit_get_enable_mask(parent_deep), 0);
+ ASSERT_CGROUP_MASK(unit_get_enable_mask(parent), (CGROUP_MASK_EXTEND_JOINED(CGROUP_MASK_CPU | cpu_accounting_mask | CGROUP_MASK_MEMORY) & m->cgroup_supported));
+ ASSERT_CGROUP_MASK(unit_get_enable_mask(nomem_parent), (CGROUP_MASK_EXTEND_JOINED(CGROUP_MASK_IO | CGROUP_MASK_BLKIO) & m->cgroup_supported));
+ ASSERT_CGROUP_MASK(unit_get_enable_mask(nomem_leaf), 0);
+ ASSERT_CGROUP_MASK(unit_get_enable_mask(root), (CGROUP_MASK_EXTEND_JOINED(CGROUP_MASK_CPU | cpu_accounting_mask | CGROUP_MASK_IO | CGROUP_MASK_BLKIO | CGROUP_MASK_MEMORY) & m->cgroup_supported));
+
+ return 0;
+}
+
+static void test_cg_mask_to_string_one(CGroupMask mask, const char *t) {
+ _cleanup_free_ char *b = NULL;
+
+ assert_se(cg_mask_to_string(mask, &b) >= 0);
+ assert_se(streq_ptr(b, t));
+}
+
+TEST(cg_mask_to_string) {
+ test_cg_mask_to_string_one(0, NULL);
+ test_cg_mask_to_string_one(_CGROUP_MASK_ALL, "cpu cpuacct cpuset io blkio memory devices pids bpf-firewall bpf-devices bpf-foreign bpf-socket-bind bpf-restrict-network-interfaces");
+ test_cg_mask_to_string_one(CGROUP_MASK_CPU, "cpu");
+ test_cg_mask_to_string_one(CGROUP_MASK_CPUACCT, "cpuacct");
+ test_cg_mask_to_string_one(CGROUP_MASK_CPUSET, "cpuset");
+ test_cg_mask_to_string_one(CGROUP_MASK_IO, "io");
+ test_cg_mask_to_string_one(CGROUP_MASK_BLKIO, "blkio");
+ test_cg_mask_to_string_one(CGROUP_MASK_MEMORY, "memory");
+ test_cg_mask_to_string_one(CGROUP_MASK_DEVICES, "devices");
+ test_cg_mask_to_string_one(CGROUP_MASK_PIDS, "pids");
+ test_cg_mask_to_string_one(CGROUP_MASK_CPU|CGROUP_MASK_CPUACCT, "cpu cpuacct");
+ test_cg_mask_to_string_one(CGROUP_MASK_CPU|CGROUP_MASK_PIDS, "cpu pids");
+ test_cg_mask_to_string_one(CGROUP_MASK_CPUACCT|CGROUP_MASK_PIDS, "cpuacct pids");
+ test_cg_mask_to_string_one(CGROUP_MASK_DEVICES|CGROUP_MASK_PIDS, "devices pids");
+ test_cg_mask_to_string_one(CGROUP_MASK_IO|CGROUP_MASK_BLKIO, "io blkio");
+}
+
+static void cgroup_device_permissions_test_normalize(const char *a, const char *b) {
+ assert_se(streq_ptr(cgroup_device_permissions_to_string(cgroup_device_permissions_from_string(a)), b));
+}
+
+TEST(cgroup_device_permissions) {
+ for (CGroupDevicePermissions p = 0; p < _CGROUP_DEVICE_PERMISSIONS_MAX; p++) {
+ const char *s;
+
+ assert_se(s = cgroup_device_permissions_to_string(p));
+ assert_se(cgroup_device_permissions_from_string(s) == p);
+ }
+
+ cgroup_device_permissions_test_normalize("", "");
+ cgroup_device_permissions_test_normalize("rw", "rw");
+ cgroup_device_permissions_test_normalize("wr", "rw");
+ cgroup_device_permissions_test_normalize("wwrr", "rw");
+ cgroup_device_permissions_test_normalize("mmmmmmmmmmmmmm", "m");
+ cgroup_device_permissions_test_normalize("mmmmrrrrmmmwwmwmwmwmwmrmrmr", "rwm");
+
+ assert_se(cgroup_device_permissions_from_string(NULL) == -EINVAL);
+ assert_se(cgroup_device_permissions_from_string("rwq") == -EINVAL);
+ assert_se(cgroup_device_permissions_from_string("RW") == -EINVAL);
+ assert_se(cgroup_device_permissions_from_string("") == 0);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-cgroup-setup.c b/src/test/test-cgroup-setup.c
new file mode 100644
index 0000000..e669e9b
--- /dev/null
+++ b/src/test/test-cgroup-setup.c
@@ -0,0 +1,73 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "cgroup-setup.h"
+#include "errno-util.h"
+#include "log.h"
+#include "proc-cmdline.h"
+#include "string-util.h"
+#include "tests.h"
+
+static void test_is_wanted_print_one(bool header) {
+ _cleanup_free_ char *cmdline = NULL;
+
+ log_info("-- %s --", __func__);
+ assert_se(proc_cmdline(&cmdline) >= 0);
+ log_info("cmdline: %s", cmdline);
+ if (header) {
+ log_info("default-hierarchy=" DEFAULT_HIERARCHY_NAME);
+ (void) system("findmnt -n /sys/fs/cgroup");
+ }
+
+ log_info("is_unified_wanted() → %s", yes_no(cg_is_unified_wanted()));
+ log_info("is_hybrid_wanted() → %s", yes_no(cg_is_hybrid_wanted()));
+ log_info("is_legacy_wanted() → %s", yes_no(cg_is_legacy_wanted()));
+ log_info(" ");
+}
+
+TEST(is_wanted_print) {
+ test_is_wanted_print_one(true);
+ test_is_wanted_print_one(false); /* run twice to test caching */
+}
+
+TEST(is_wanted) {
+ assert_se(setenv("SYSTEMD_PROC_CMDLINE",
+ "systemd.unified_cgroup_hierarchy", 1) >= 0);
+ test_is_wanted_print_one(false);
+
+ assert_se(setenv("SYSTEMD_PROC_CMDLINE",
+ "systemd.unified_cgroup_hierarchy=0", 1) >= 0);
+ test_is_wanted_print_one(false);
+
+ assert_se(setenv("SYSTEMD_PROC_CMDLINE",
+ "systemd.unified_cgroup_hierarchy=0 "
+ "systemd.legacy_systemd_cgroup_controller", 1) >= 0);
+ test_is_wanted_print_one(false);
+
+ assert_se(setenv("SYSTEMD_PROC_CMDLINE",
+ "systemd.unified_cgroup_hierarchy=0 "
+ "systemd.legacy_systemd_cgroup_controller=0", 1) >= 0);
+ test_is_wanted_print_one(false);
+
+ /* cgroup_no_v1=all implies unified cgroup hierarchy, unless otherwise
+ * explicitly specified. */
+ assert_se(setenv("SYSTEMD_PROC_CMDLINE",
+ "cgroup_no_v1=all", 1) >= 0);
+ test_is_wanted_print_one(false);
+
+ assert_se(setenv("SYSTEMD_PROC_CMDLINE",
+ "cgroup_no_v1=all "
+ "systemd.unified_cgroup_hierarchy=0", 1) >= 0);
+ test_is_wanted_print_one(false);
+}
+
+static int intro(void) {
+ if (access("/proc/cmdline", R_OK) < 0 && ERRNO_IS_PRIVILEGE(errno))
+ return log_tests_skipped("can't read /proc/cmdline");
+
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_DEBUG, intro);
diff --git a/src/test/test-cgroup-unit-default.c b/src/test/test-cgroup-unit-default.c
new file mode 100644
index 0000000..62618ce
--- /dev/null
+++ b/src/test/test-cgroup-unit-default.c
@@ -0,0 +1,138 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+
+#include "cgroup.h"
+#include "manager.h"
+#include "rm-rf.h"
+#include "tests.h"
+#include "unit.h"
+
+TEST_RET(default_memory_low, .sd_booted = true) {
+ _cleanup_(rm_rf_physical_and_freep) char *runtime_dir = NULL;
+ _cleanup_(manager_freep) Manager *m = NULL;
+ Unit *root, *dml,
+ *dml_passthrough, *dml_passthrough_empty, *dml_passthrough_set_dml, *dml_passthrough_set_ml,
+ *dml_override, *dml_override_empty,
+ *dml_discard, *dml_discard_empty, *dml_discard_set_ml;
+ uint64_t dml_tree_default;
+ int r;
+
+ r = enter_cgroup_subroot(NULL);
+ if (r == -ENOMEDIUM)
+ return log_tests_skipped("cgroupfs not available");
+
+ _cleanup_free_ char *unit_dir = NULL;
+ assert_se(get_testdata_dir("units", &unit_dir) >= 0);
+ assert_se(set_unit_path(unit_dir) >= 0);
+ assert_se(runtime_dir = setup_fake_runtime_dir());
+ r = manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_BASIC, &m);
+ if (IN_SET(r, -EPERM, -EACCES)) {
+ log_error_errno(r, "manager_new: %m");
+ return log_tests_skipped("cannot create manager");
+ }
+
+ assert_se(r >= 0);
+ assert_se(manager_startup(m, NULL, NULL, NULL) >= 0);
+
+ /* dml.slice has DefaultMemoryLow=50. Beyond that, individual subhierarchies look like this:
+ *
+ * 1. dml-passthrough.slice sets MemoryLow=100. This should not affect its children, as only
+ * DefaultMemoryLow is propagated, not MemoryLow. As such, all leaf services should end up with
+ * memory.low as 50, inherited from dml.slice, *except* for dml-passthrough-set-ml.service, which
+ * should have the value of 0, as it has MemoryLow explicitly set.
+ *
+ * ┌───────────┐
+ * │ dml.slice │
+ * └─────┬─────┘
+ * MemoryLow=100
+ * ┌───────────┴───────────┐
+ * │ dml-passthrough.slice │
+ * └───────────┬───────────┘
+ * ┌───────────────────────────────────┼───────────────────────────────────┐
+ * no new settings DefaultMemoryLow=15 MemoryLow=0
+ * ┌───────────────┴───────────────┐ ┌────────────────┴────────────────┐ ┌───────────────┴────────────────┐
+ * │ dml-passthrough-empty.service │ │ dml-passthrough-set-dml.service │ │ dml-passthrough-set-ml.service │
+ * └───────────────────────────────┘ └─────────────────────────────────┘ └────────────────────────────────┘
+ *
+ * 2. dml-override.slice sets DefaultMemoryLow=10. As such, dml-override-empty.service should also
+ * end up with a memory.low of 10. dml-override.slice should still have a memory.low of 50.
+ *
+ * ┌───────────┐
+ * │ dml.slice │
+ * └─────┬─────┘
+ * DefaultMemoryLow=10
+ * ┌─────────┴──────────┐
+ * │ dml-override.slice │
+ * └─────────┬──────────┘
+ * no new settings
+ * ┌─────────────┴──────────────┐
+ * │ dml-override-empty.service │
+ * └────────────────────────────┘
+ *
+ * 3. dml-discard.slice sets DefaultMemoryLow= with no rvalue. As such,
+ * dml-discard-empty.service should end up with a value of 0.
+ * dml-discard-set-ml.service sets MemoryLow=15, and as such should have that override the
+ * reset DefaultMemoryLow value. dml-discard.slice should still have an eventual memory.low of 50.
+ *
+ * ┌───────────┐
+ * │ dml.slice │
+ * └─────┬─────┘
+ * DefaultMemoryLow=
+ * ┌─────────┴─────────┐
+ * │ dml-discard.slice │
+ * └─────────┬─────────┘
+ * ┌──────────────┴───────────────┐
+ * no new settings MemoryLow=15
+ * ┌─────────────┴─────────────┐ ┌─────────────┴──────────────┐
+ * │ dml-discard-empty.service │ │ dml-discard-set-ml.service │
+ * └───────────────────────────┘ └────────────────────────────┘
+ */
+ assert_se(manager_load_startable_unit_or_warn(m, "dml.slice", NULL, &dml) >= 0);
+
+ assert_se(manager_load_startable_unit_or_warn(m, "dml-passthrough.slice", NULL, &dml_passthrough) >= 0);
+ assert_se(UNIT_GET_SLICE(dml_passthrough) == dml);
+ assert_se(manager_load_startable_unit_or_warn(m, "dml-passthrough-empty.service", NULL, &dml_passthrough_empty) >= 0);
+ assert_se(UNIT_GET_SLICE(dml_passthrough_empty) == dml_passthrough);
+ assert_se(manager_load_startable_unit_or_warn(m, "dml-passthrough-set-dml.service", NULL, &dml_passthrough_set_dml) >= 0);
+ assert_se(UNIT_GET_SLICE(dml_passthrough_set_dml) == dml_passthrough);
+ assert_se(manager_load_startable_unit_or_warn(m, "dml-passthrough-set-ml.service", NULL, &dml_passthrough_set_ml) >= 0);
+ assert_se(UNIT_GET_SLICE(dml_passthrough_set_ml) == dml_passthrough);
+
+ assert_se(manager_load_startable_unit_or_warn(m, "dml-override.slice", NULL, &dml_override) >= 0);
+ assert_se(UNIT_GET_SLICE(dml_override) == dml);
+ assert_se(manager_load_startable_unit_or_warn(m, "dml-override-empty.service", NULL, &dml_override_empty) >= 0);
+ assert_se(UNIT_GET_SLICE(dml_override_empty) == dml_override);
+
+ assert_se(manager_load_startable_unit_or_warn(m, "dml-discard.slice", NULL, &dml_discard) >= 0);
+ assert_se(UNIT_GET_SLICE(dml_discard) == dml);
+ assert_se(manager_load_startable_unit_or_warn(m, "dml-discard-empty.service", NULL, &dml_discard_empty) >= 0);
+ assert_se(UNIT_GET_SLICE(dml_discard_empty) == dml_discard);
+ assert_se(manager_load_startable_unit_or_warn(m, "dml-discard-set-ml.service", NULL, &dml_discard_set_ml) >= 0);
+ assert_se(UNIT_GET_SLICE(dml_discard_set_ml) == dml_discard);
+
+ assert_se(root = UNIT_GET_SLICE(dml));
+ assert_se(!UNIT_GET_SLICE(root));
+
+ assert_se(unit_get_ancestor_memory_low(root) == CGROUP_LIMIT_MIN);
+
+ assert_se(unit_get_ancestor_memory_low(dml) == CGROUP_LIMIT_MIN);
+ dml_tree_default = unit_get_cgroup_context(dml)->default_memory_low;
+ assert_se(dml_tree_default == 50);
+
+ assert_se(unit_get_ancestor_memory_low(dml_passthrough) == 100);
+ assert_se(unit_get_ancestor_memory_low(dml_passthrough_empty) == dml_tree_default);
+ assert_se(unit_get_ancestor_memory_low(dml_passthrough_set_dml) == 50);
+ assert_se(unit_get_ancestor_memory_low(dml_passthrough_set_ml) == 0);
+
+ assert_se(unit_get_ancestor_memory_low(dml_override) == dml_tree_default);
+ assert_se(unit_get_ancestor_memory_low(dml_override_empty) == 10);
+
+ assert_se(unit_get_ancestor_memory_low(dml_discard) == dml_tree_default);
+ assert_se(unit_get_ancestor_memory_low(dml_discard_empty) == CGROUP_LIMIT_MIN);
+ assert_se(unit_get_ancestor_memory_low(dml_discard_set_ml) == 15);
+
+ return 0;
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-cgroup-util.c b/src/test/test-cgroup-util.c
new file mode 100644
index 0000000..51f52d9
--- /dev/null
+++ b/src/test/test-cgroup-util.c
@@ -0,0 +1,466 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "cgroup-util.h"
+#include "dirent-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "format-util.h"
+#include "parse-util.h"
+#include "proc-cmdline.h"
+#include "process-util.h"
+#include "special.h"
+#include "stat-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "user-util.h"
+
+static void check_p_d_u(const char *path, int code, const char *result) {
+ _cleanup_free_ char *unit = NULL;
+ int r;
+
+ r = cg_path_decode_unit(path, &unit);
+ printf("%s: %s → %s %d expected %s %d\n", __func__, path, unit, r, strnull(result), code);
+ assert_se(r == code);
+ assert_se(streq_ptr(unit, result));
+}
+
+TEST(path_decode_unit) {
+ check_p_d_u("getty@tty2.service", 0, "getty@tty2.service");
+ check_p_d_u("getty@tty2.service/", 0, "getty@tty2.service");
+ check_p_d_u("getty@tty2.service/xxx", 0, "getty@tty2.service");
+ check_p_d_u("getty@.service/", -ENXIO, NULL);
+ check_p_d_u("getty@.service", -ENXIO, NULL);
+ check_p_d_u("getty.service", 0, "getty.service");
+ check_p_d_u("getty", -ENXIO, NULL);
+ check_p_d_u("getty/waldo", -ENXIO, NULL);
+ check_p_d_u("_cpu.service", 0, "cpu.service");
+}
+
+static void check_p_g_u(const char *path, int code, const char *result) {
+ _cleanup_free_ char *unit = NULL;
+ int r;
+
+ r = cg_path_get_unit(path, &unit);
+ printf("%s: %s → %s %d expected %s %d\n", __func__, path, unit, r, strnull(result), code);
+ assert_se(r == code);
+ assert_se(streq_ptr(unit, result));
+}
+
+TEST(path_get_unit) {
+ check_p_g_u("/system.slice/foobar.service/sdfdsaf", 0, "foobar.service");
+ check_p_g_u("/system.slice/getty@tty5.service", 0, "getty@tty5.service");
+ check_p_g_u("/system.slice/getty@tty5.service/aaa/bbb", 0, "getty@tty5.service");
+ check_p_g_u("/system.slice/getty@tty5.service/", 0, "getty@tty5.service");
+ check_p_g_u("/system.slice/getty@tty6.service/tty5", 0, "getty@tty6.service");
+ check_p_g_u("sadfdsafsda", -ENXIO, NULL);
+ check_p_g_u("/system.slice/getty####@tty6.service/xxx", -ENXIO, NULL);
+ check_p_g_u("/system.slice/system-waldo.slice/foobar.service/sdfdsaf", 0, "foobar.service");
+ check_p_g_u("/system.slice/system-waldo.slice/_cpu.service/sdfdsaf", 0, "cpu.service");
+ check_p_g_u("/user.slice/user-1000.slice/user@1000.service/server.service", 0, "user@1000.service");
+ check_p_g_u("/user.slice/user-1000.slice/user@.service/server.service", -ENXIO, NULL);
+}
+
+static void check_p_g_u_p(const char *path, int code, const char *result) {
+ _cleanup_free_ char *unit_path = NULL;
+ int r;
+
+ r = cg_path_get_unit_path(path, &unit_path);
+ printf("%s: %s → %s %d expected %s %d\n", __func__, path, unit_path, r, strnull(result), code);
+ assert_se(r == code);
+ assert_se(streq_ptr(unit_path, result));
+}
+
+TEST(path_get_unit_path) {
+ check_p_g_u_p("/system.slice/foobar.service/sdfdsaf", 0, "/system.slice/foobar.service");
+ check_p_g_u_p("/system.slice/getty@tty5.service", 0, "/system.slice/getty@tty5.service");
+ check_p_g_u_p("/system.slice/getty@tty5.service/aaa/bbb", 0, "/system.slice/getty@tty5.service");
+ check_p_g_u_p("/system.slice/getty@tty5.service/", 0, "/system.slice/getty@tty5.service");
+ check_p_g_u_p("/system.slice/getty@tty6.service/tty5", 0, "/system.slice/getty@tty6.service");
+ check_p_g_u_p("sadfdsafsda", -ENXIO, NULL);
+ check_p_g_u_p("/system.slice/getty####@tty6.service/xxx", -ENXIO, NULL);
+ check_p_g_u_p("/system.slice/system-waldo.slice/foobar.service/sdfdsaf", 0, "/system.slice/system-waldo.slice/foobar.service");
+ check_p_g_u_p("/system.slice/system-waldo.slice/_cpu.service/sdfdsaf", 0, "/system.slice/system-waldo.slice/_cpu.service");
+ check_p_g_u_p("/system.slice/system-waldo.slice/_cpu.service", 0, "/system.slice/system-waldo.slice/_cpu.service");
+ check_p_g_u_p("/user.slice/user-1000.slice/user@1000.service/server.service", 0, "/user.slice/user-1000.slice/user@1000.service");
+ check_p_g_u_p("/user.slice/user-1000.slice/user@.service/server.service", -ENXIO, NULL);
+ check_p_g_u_p("/user.slice/_user-1000.slice/user@1000.service/foobar.slice/foobar@pie.service", 0, "/user.slice/_user-1000.slice/user@1000.service");
+ check_p_g_u_p("/_session-2.scope/_foobar@pie.service/pa/po", 0, "/_session-2.scope");
+}
+
+static void check_p_g_u_u(const char *path, int code, const char *result) {
+ _cleanup_free_ char *unit = NULL;
+ int r;
+
+ r = cg_path_get_user_unit(path, &unit);
+ printf("%s: %s → %s %d expected %s %d\n", __func__, path, unit, r, strnull(result), code);
+ assert_se(r == code);
+ assert_se(streq_ptr(unit, result));
+}
+
+TEST(path_get_user_unit) {
+ check_p_g_u_u("/user.slice/user-1000.slice/session-2.scope/foobar.service", 0, "foobar.service");
+ check_p_g_u_u("/user.slice/user-1000.slice/session-2.scope/waldo.slice/foobar.service", 0, "foobar.service");
+ check_p_g_u_u("/user.slice/user-1002.slice/session-2.scope/foobar.service/waldo", 0, "foobar.service");
+ check_p_g_u_u("/user.slice/user-1000.slice/session-2.scope/foobar.service/waldo/uuuux", 0, "foobar.service");
+ check_p_g_u_u("/user.slice/user-1000.slice/session-2.scope/waldo/waldo/uuuux", -ENXIO, NULL);
+ check_p_g_u_u("/user.slice/user-1000.slice/session-2.scope/foobar@pie.service/pa/po", 0, "foobar@pie.service");
+ check_p_g_u_u("/session-2.scope/foobar@pie.service/pa/po", 0, "foobar@pie.service");
+ check_p_g_u_u("/xyz.slice/xyz-waldo.slice/session-77.scope/foobar@pie.service/pa/po", 0, "foobar@pie.service");
+ check_p_g_u_u("/meh.service", -ENXIO, NULL);
+ check_p_g_u_u("/session-3.scope/_cpu.service", 0, "cpu.service");
+ check_p_g_u_u("/user.slice/user-1000.slice/user@1000.service/server.service", 0, "server.service");
+ check_p_g_u_u("/user.slice/user-1000.slice/user@1000.service/foobar.slice/foobar@pie.service", 0, "foobar@pie.service");
+ check_p_g_u_u("/user.slice/user-1000.slice/user@.service/server.service", -ENXIO, NULL);
+}
+
+static void check_p_g_s(const char *path, int code, const char *result) {
+ _cleanup_free_ char *s = NULL;
+
+ assert_se(cg_path_get_session(path, &s) == code);
+ assert_se(streq_ptr(s, result));
+}
+
+TEST(path_get_session) {
+ check_p_g_s("/user.slice/user-1000.slice/session-2.scope/foobar.service", 0, "2");
+ check_p_g_s("/session-3.scope", 0, "3");
+ check_p_g_s("/session-.scope", -ENXIO, NULL);
+ check_p_g_s("", -ENXIO, NULL);
+}
+
+static void check_p_g_o_u(const char *path, int code, uid_t result) {
+ uid_t uid = 0;
+
+ assert_se(cg_path_get_owner_uid(path, &uid) == code);
+ assert_se(uid == result);
+}
+
+TEST(path_get_owner_uid) {
+ check_p_g_o_u("/user.slice/user-1000.slice/session-2.scope/foobar.service", 0, 1000);
+ check_p_g_o_u("/user.slice/user-1006.slice", 0, 1006);
+ check_p_g_o_u("", -ENXIO, 0);
+}
+
+static void check_p_g_slice(const char *path, int code, const char *result) {
+ _cleanup_free_ char *s = NULL;
+
+ assert_se(cg_path_get_slice(path, &s) == code);
+ assert_se(streq_ptr(s, result));
+}
+
+TEST(path_get_slice) {
+ check_p_g_slice("/user.slice", 0, "user.slice");
+ check_p_g_slice("/foobar", 0, SPECIAL_ROOT_SLICE);
+ check_p_g_slice("/user.slice/user-waldo.slice", 0, "user-waldo.slice");
+ check_p_g_slice("", 0, SPECIAL_ROOT_SLICE);
+ check_p_g_slice("foobar", 0, SPECIAL_ROOT_SLICE);
+ check_p_g_slice("foobar.slice", 0, "foobar.slice");
+ check_p_g_slice("foo.slice/foo-bar.slice/waldo.service", 0, "foo-bar.slice");
+}
+
+static void check_p_g_u_slice(const char *path, int code, const char *result) {
+ _cleanup_free_ char *s = NULL;
+
+ assert_se(cg_path_get_user_slice(path, &s) == code);
+ assert_se(streq_ptr(s, result));
+}
+
+TEST(path_get_user_slice) {
+ check_p_g_u_slice("/user.slice", -ENXIO, NULL);
+ check_p_g_u_slice("/foobar", -ENXIO, NULL);
+ check_p_g_u_slice("/user.slice/user-waldo.slice", -ENXIO, NULL);
+ check_p_g_u_slice("", -ENXIO, NULL);
+ check_p_g_u_slice("foobar", -ENXIO, NULL);
+ check_p_g_u_slice("foobar.slice", -ENXIO, NULL);
+ check_p_g_u_slice("foo.slice/foo-bar.slice/waldo.service", -ENXIO, NULL);
+
+ check_p_g_u_slice("foo.slice/foo-bar.slice/user@1000.service", 0, SPECIAL_ROOT_SLICE);
+ check_p_g_u_slice("foo.slice/foo-bar.slice/user@1000.service/", 0, SPECIAL_ROOT_SLICE);
+ check_p_g_u_slice("foo.slice/foo-bar.slice/user@1000.service///", 0, SPECIAL_ROOT_SLICE);
+ check_p_g_u_slice("foo.slice/foo-bar.slice/user@1000.service/waldo.service", 0, SPECIAL_ROOT_SLICE);
+ check_p_g_u_slice("foo.slice/foo-bar.slice/user@1000.service/piep.slice/foo.service", 0, "piep.slice");
+ check_p_g_u_slice("/foo.slice//foo-bar.slice/user@1000.service/piep.slice//piep-pap.slice//foo.service", 0, "piep-pap.slice");
+}
+
+TEST(get_paths, .sd_booted = true) {
+ _cleanup_free_ char *a = NULL;
+
+ assert_se(cg_get_root_path(&a) >= 0);
+ log_info("Root = %s", a);
+}
+
+TEST(proc) {
+ _cleanup_closedir_ DIR *d = NULL;
+ int r;
+
+ assert_se(proc_dir_open(&d) >= 0);
+
+ for (;;) {
+ _cleanup_free_ char *path = NULL, *path_shifted = NULL, *session = NULL, *unit = NULL, *user_unit = NULL, *machine = NULL, *slice = NULL;
+ _cleanup_(pidref_done) PidRef pid = PIDREF_NULL;
+ uid_t uid = UID_INVALID;
+
+ r = proc_dir_read_pidref(d, &pid);
+ assert_se(r >= 0);
+
+ if (r == 0)
+ break;
+
+ if (pidref_is_kernel_thread(&pid) != 0)
+ continue;
+
+ cg_pid_get_path(SYSTEMD_CGROUP_CONTROLLER, pid.pid, &path);
+ cg_pid_get_path_shifted(pid.pid, NULL, &path_shifted);
+ cg_pid_get_owner_uid(pid.pid, &uid);
+ cg_pid_get_session(pid.pid, &session);
+ cg_pid_get_unit(pid.pid, &unit);
+ cg_pid_get_user_unit(pid.pid, &user_unit);
+ cg_pid_get_machine_name(pid.pid, &machine);
+ cg_pid_get_slice(pid.pid, &slice);
+
+ printf(PID_FMT"\t%s\t%s\t"UID_FMT"\t%s\t%s\t%s\t%s\t%s\n",
+ pid.pid,
+ path,
+ path_shifted,
+ uid,
+ session,
+ unit,
+ user_unit,
+ machine,
+ slice);
+ }
+}
+
+static void test_escape_one(const char *s, const char *expected) {
+ _cleanup_free_ char *b = NULL;
+
+ assert_se(s);
+ assert_se(expected);
+
+ assert_se(cg_escape(s, &b) >= 0);
+ assert_se(streq(b, expected));
+
+ assert_se(streq(cg_unescape(b), s));
+
+ assert_se(filename_is_valid(b));
+ assert_se(!cg_needs_escape(s) || b[0] == '_');
+}
+
+TEST(escape, .sd_booted = true) {
+ test_escape_one("foobar", "foobar");
+ test_escape_one(".foobar", "_.foobar");
+ test_escape_one("foobar.service", "foobar.service");
+ test_escape_one("cgroup.service", "_cgroup.service");
+ test_escape_one("tasks", "_tasks");
+ if (access("/sys/fs/cgroup/cpu", F_OK) == 0)
+ test_escape_one("cpu.service", "_cpu.service");
+ test_escape_one("_foobar", "__foobar");
+ test_escape_one("", "_");
+ test_escape_one("_", "__");
+ test_escape_one(".", "_.");
+}
+
+TEST(controller_is_valid) {
+ assert_se(cg_controller_is_valid("foobar"));
+ assert_se(cg_controller_is_valid("foo_bar"));
+ assert_se(cg_controller_is_valid("name=foo"));
+ assert_se(!cg_controller_is_valid(""));
+ assert_se(!cg_controller_is_valid("name="));
+ assert_se(!cg_controller_is_valid("="));
+ assert_se(!cg_controller_is_valid("cpu,cpuacct"));
+ assert_se(!cg_controller_is_valid("_"));
+ assert_se(!cg_controller_is_valid("_foobar"));
+ assert_se(!cg_controller_is_valid("tatü"));
+}
+
+static void test_slice_to_path_one(const char *unit, const char *path, int error) {
+ _cleanup_free_ char *ret = NULL;
+ int r;
+
+ log_info("unit: %s", unit);
+
+ r = cg_slice_to_path(unit, &ret);
+ log_info("actual: %s / %d", strnull(ret), r);
+ log_info("expect: %s / %d", strnull(path), error);
+ assert_se(r == error);
+ assert_se(streq_ptr(ret, path));
+}
+
+TEST(slice_to_path) {
+ test_slice_to_path_one("foobar.slice", "foobar.slice", 0);
+ test_slice_to_path_one("foobar-waldo.slice", "foobar.slice/foobar-waldo.slice", 0);
+ test_slice_to_path_one("foobar-waldo.service", NULL, -EINVAL);
+ test_slice_to_path_one(SPECIAL_ROOT_SLICE, "", 0);
+ test_slice_to_path_one("--.slice", NULL, -EINVAL);
+ test_slice_to_path_one("-", NULL, -EINVAL);
+ test_slice_to_path_one("-foo-.slice", NULL, -EINVAL);
+ test_slice_to_path_one("-foo.slice", NULL, -EINVAL);
+ test_slice_to_path_one("foo-.slice", NULL, -EINVAL);
+ test_slice_to_path_one("foo--bar.slice", NULL, -EINVAL);
+ test_slice_to_path_one("foo.slice/foo--bar.slice", NULL, -EINVAL);
+ test_slice_to_path_one("a-b.slice", "a.slice/a-b.slice", 0);
+ test_slice_to_path_one("a-b-c-d-e.slice", "a.slice/a-b.slice/a-b-c.slice/a-b-c-d.slice/a-b-c-d-e.slice", 0);
+
+ test_slice_to_path_one("foobar@.slice", NULL, -EINVAL);
+ test_slice_to_path_one("foobar@waldo.slice", NULL, -EINVAL);
+ test_slice_to_path_one("foobar@waldo.service", NULL, -EINVAL);
+ test_slice_to_path_one("-foo@-.slice", NULL, -EINVAL);
+ test_slice_to_path_one("-foo@.slice", NULL, -EINVAL);
+ test_slice_to_path_one("foo@-.slice", NULL, -EINVAL);
+ test_slice_to_path_one("foo@@bar.slice", NULL, -EINVAL);
+ test_slice_to_path_one("foo.slice/foo@@bar.slice", NULL, -EINVAL);
+}
+
+static void test_shift_path_one(const char *raw, const char *root, const char *shifted) {
+ const char *s = NULL;
+
+ assert_se(cg_shift_path(raw, root, &s) >= 0);
+ assert_se(streq(s, shifted));
+}
+
+TEST(shift_path) {
+ test_shift_path_one("/foobar/waldo", "/", "/foobar/waldo");
+ test_shift_path_one("/foobar/waldo", "", "/foobar/waldo");
+ test_shift_path_one("/foobar/waldo", "/foobar", "/waldo");
+ test_shift_path_one("/foobar/waldo", "/hogehoge", "/foobar/waldo");
+}
+
+TEST(mask_supported, .sd_booted = true) {
+ CGroupMask m;
+ CGroupController c;
+
+ assert_se(cg_mask_supported(&m) >= 0);
+
+ for (c = 0; c < _CGROUP_CONTROLLER_MAX; c++)
+ printf("'%s' is supported: %s\n", cgroup_controller_to_string(c), yes_no(m & CGROUP_CONTROLLER_TO_MASK(c)));
+}
+
+TEST(is_cgroup_fs, .sd_booted = true) {
+ struct statfs sfs;
+ assert_se(statfs("/sys/fs/cgroup", &sfs) == 0);
+ if (is_temporary_fs(&sfs))
+ assert_se(statfs("/sys/fs/cgroup/systemd", &sfs) == 0);
+ assert_se(is_cgroup_fs(&sfs));
+}
+
+TEST(fd_is_cgroup_fs, .sd_booted = true) {
+ int fd;
+
+ fd = open("/sys/fs/cgroup", O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW);
+ assert_se(fd >= 0);
+ if (fd_is_temporary_fs(fd)) {
+ fd = safe_close(fd);
+ fd = open("/sys/fs/cgroup/systemd", O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW);
+ assert_se(fd >= 0);
+ }
+ assert_se(fd_is_cgroup_fs(fd));
+ fd = safe_close(fd);
+}
+
+TEST(cg_tests) {
+ int all, hybrid, systemd, r;
+
+ r = cg_unified();
+ if (r == -ENOMEDIUM) {
+ log_tests_skipped("cgroup not mounted");
+ return;
+ }
+ assert_se(r >= 0);
+
+ all = cg_all_unified();
+ assert_se(IN_SET(all, 0, 1));
+
+ hybrid = cg_hybrid_unified();
+ assert_se(IN_SET(hybrid, 0, 1));
+
+ systemd = cg_unified_controller(SYSTEMD_CGROUP_CONTROLLER);
+ assert_se(IN_SET(systemd, 0, 1));
+
+ if (all) {
+ assert_se(systemd);
+ assert_se(!hybrid);
+
+ } else if (hybrid) {
+ assert_se(systemd);
+ assert_se(!all);
+
+ } else
+ assert_se(!systemd);
+}
+
+TEST(cg_get_keyed_attribute) {
+ _cleanup_free_ char *val = NULL;
+ char *vals3[3] = {}, *vals3a[3] = {};
+ int i, r;
+
+ r = cg_get_keyed_attribute("cpu", "/init.scope", "no_such_file", STRV_MAKE("no_such_attr"), &val);
+ if (r == -ENOMEDIUM || ERRNO_IS_PRIVILEGE(r)) {
+ log_info_errno(r, "Skipping most of %s, /sys/fs/cgroup not accessible: %m", __func__);
+ return;
+ }
+
+ assert_se(r == -ENOENT);
+ assert_se(val == NULL);
+
+ if (access("/sys/fs/cgroup/init.scope/cpu.stat", R_OK) < 0) {
+ log_info_errno(errno, "Skipping most of %s, /init.scope/cpu.stat not accessible: %m", __func__);
+ return;
+ }
+
+ assert_se(cg_get_keyed_attribute("cpu", "/init.scope", "cpu.stat", STRV_MAKE("no_such_attr"), &val) == -ENXIO);
+ assert_se(cg_get_keyed_attribute_graceful("cpu", "/init.scope", "cpu.stat", STRV_MAKE("no_such_attr"), &val) == 0);
+ assert_se(val == NULL);
+
+ assert_se(cg_get_keyed_attribute("cpu", "/init.scope", "cpu.stat", STRV_MAKE("usage_usec"), &val) == 0);
+ val = mfree(val);
+
+ assert_se(cg_get_keyed_attribute_graceful("cpu", "/init.scope", "cpu.stat", STRV_MAKE("usage_usec"), &val) == 1);
+ log_info("cpu /init.scope cpu.stat [usage_usec] → \"%s\"", val);
+
+ assert_se(cg_get_keyed_attribute("cpu", "/init.scope", "cpu.stat", STRV_MAKE("usage_usec", "no_such_attr"), vals3) == -ENXIO);
+ assert_se(cg_get_keyed_attribute_graceful("cpu", "/init.scope", "cpu.stat", STRV_MAKE("usage_usec", "no_such_attr"), vals3) == 1);
+ assert_se(vals3[0] && !vals3[1]);
+ free(vals3[0]);
+
+ assert_se(cg_get_keyed_attribute("cpu", "/init.scope", "cpu.stat", STRV_MAKE("usage_usec", "usage_usec"), vals3) == -ENXIO);
+ assert_se(cg_get_keyed_attribute_graceful("cpu", "/init.scope", "cpu.stat", STRV_MAKE("usage_usec", "usage_usec"), vals3) == 1);
+ assert_se(vals3[0] && !vals3[1]);
+ free(vals3[0]);
+
+ assert_se(cg_get_keyed_attribute("cpu", "/init.scope", "cpu.stat",
+ STRV_MAKE("usage_usec", "user_usec", "system_usec"), vals3) == 0);
+ for (i = 0; i < 3; i++)
+ free(vals3[i]);
+
+ assert_se(cg_get_keyed_attribute_graceful("cpu", "/init.scope", "cpu.stat",
+ STRV_MAKE("usage_usec", "user_usec", "system_usec"), vals3) == 3);
+ log_info("cpu /init.scope cpu.stat [usage_usec user_usec system_usec] → \"%s\", \"%s\", \"%s\"",
+ vals3[0], vals3[1], vals3[2]);
+
+ assert_se(cg_get_keyed_attribute("cpu", "/init.scope", "cpu.stat",
+ STRV_MAKE("system_usec", "user_usec", "usage_usec"), vals3a) == 0);
+ for (i = 0; i < 3; i++)
+ free(vals3a[i]);
+
+ assert_se(cg_get_keyed_attribute_graceful("cpu", "/init.scope", "cpu.stat",
+ STRV_MAKE("system_usec", "user_usec", "usage_usec"), vals3a) == 3);
+ log_info("cpu /init.scope cpu.stat [system_usec user_usec usage_usec] → \"%s\", \"%s\", \"%s\"",
+ vals3a[0], vals3a[1], vals3a[2]);
+
+ for (i = 0; i < 3; i++) {
+ free(vals3[i]);
+ free(vals3a[i]);
+ }
+}
+
+TEST(bfq_weight_conversion) {
+ assert_se(BFQ_WEIGHT(1) == 1);
+ assert_se(BFQ_WEIGHT(50) == 50);
+ assert_se(BFQ_WEIGHT(100) == 100);
+ assert_se(BFQ_WEIGHT(500) == 136);
+ assert_se(BFQ_WEIGHT(5000) == 545);
+ assert_se(BFQ_WEIGHT(10000) == 1000);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-cgroup.c b/src/test/test-cgroup.c
new file mode 100644
index 0000000..0fbd635
--- /dev/null
+++ b/src/test/test-cgroup.c
@@ -0,0 +1,132 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <unistd.h>
+
+#include "cgroup-setup.h"
+#include "cgroup-util.h"
+#include "errno-util.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "string-util.h"
+#include "tests.h"
+
+TEST(cg_split_spec) {
+ char *c, *p;
+
+ assert_se(cg_split_spec("foobar:/", &c, &p) == 0);
+ assert_se(streq(c, "foobar"));
+ assert_se(streq(p, "/"));
+ c = mfree(c);
+ p = mfree(p);
+
+ assert_se(cg_split_spec("foobar:", &c, &p) == 0);
+ c = mfree(c);
+ p = mfree(p);
+
+ assert_se(cg_split_spec("foobar:asdfd", &c, &p) < 0);
+ assert_se(cg_split_spec(":///", &c, &p) < 0);
+ assert_se(cg_split_spec(":", &c, &p) < 0);
+ assert_se(cg_split_spec("", &c, &p) < 0);
+ assert_se(cg_split_spec("fo/obar:/", &c, &p) < 0);
+
+ assert_se(cg_split_spec("/", &c, &p) >= 0);
+ assert_se(c == NULL);
+ assert_se(streq(p, "/"));
+ p = mfree(p);
+
+ assert_se(cg_split_spec("foo", &c, &p) >= 0);
+ assert_se(streq(c, "foo"));
+ assert_se(p == NULL);
+ c = mfree(c);
+}
+
+TEST(cg_create) {
+ int r;
+
+ r = cg_unified_cached(false);
+ if (r == -ENOMEDIUM) {
+ log_tests_skipped("cgroup not mounted");
+ return;
+ }
+ assert_se(r >= 0);
+
+ _cleanup_free_ char *here = NULL;
+ assert_se(cg_pid_get_path_shifted(0, NULL, &here) >= 0);
+
+ const char *test_a = prefix_roota(here, "/test-a"),
+ *test_b = prefix_roota(here, "/test-b"),
+ *test_c = prefix_roota(here, "/test-b/test-c"),
+ *test_d = prefix_roota(here, "/test-b/test-d");
+ char *path;
+
+ log_info("Paths for test:\n%s\n%s", test_a, test_b);
+
+ /* Possibly clean up left-overs from aboted previous runs */
+ (void) cg_trim(SYSTEMD_CGROUP_CONTROLLER, test_a, /* delete_root= */ true);
+ (void) cg_trim(SYSTEMD_CGROUP_CONTROLLER, test_b, /* delete_root= */ true);
+
+ r = cg_create(SYSTEMD_CGROUP_CONTROLLER, test_a);
+ if (IN_SET(r, -EPERM, -EACCES, -EROFS)) {
+ log_info_errno(r, "Skipping %s: %m", __func__);
+ return;
+ }
+
+ assert_se(r == 1);
+ assert_se(cg_create(SYSTEMD_CGROUP_CONTROLLER, test_a) == 0);
+ assert_se(cg_create(SYSTEMD_CGROUP_CONTROLLER, test_b) == 1);
+ assert_se(cg_create(SYSTEMD_CGROUP_CONTROLLER, test_c) == 1);
+ assert_se(cg_create_and_attach(SYSTEMD_CGROUP_CONTROLLER, test_b, 0) == 0);
+
+ assert_se(cg_pid_get_path(SYSTEMD_CGROUP_CONTROLLER, getpid_cached(), &path) == 0);
+ assert_se(streq(path, test_b));
+ free(path);
+
+ assert_se(cg_attach(SYSTEMD_CGROUP_CONTROLLER, test_a, 0) == 0);
+
+ assert_se(cg_pid_get_path(SYSTEMD_CGROUP_CONTROLLER, getpid_cached(), &path) == 0);
+ assert_se(path_equal(path, test_a));
+ free(path);
+
+ assert_se(cg_create_and_attach(SYSTEMD_CGROUP_CONTROLLER, test_d, 0) == 1);
+
+ assert_se(cg_pid_get_path(SYSTEMD_CGROUP_CONTROLLER, getpid_cached(), &path) == 0);
+ assert_se(path_equal(path, test_d));
+ free(path);
+
+ assert_se(cg_get_path(SYSTEMD_CGROUP_CONTROLLER, test_d, NULL, &path) == 0);
+ log_debug("test_d: %s", path);
+ const char *full_d;
+ if (cg_all_unified())
+ full_d = strjoina("/sys/fs/cgroup", test_d);
+ else if (cg_hybrid_unified())
+ full_d = strjoina("/sys/fs/cgroup/unified", test_d);
+ else
+ full_d = strjoina("/sys/fs/cgroup/systemd", test_d);
+ assert_se(path_equal(path, full_d));
+ free(path);
+
+ assert_se(cg_is_empty(SYSTEMD_CGROUP_CONTROLLER, test_a) > 0);
+ assert_se(cg_is_empty(SYSTEMD_CGROUP_CONTROLLER, test_b) > 0);
+ assert_se(cg_is_empty_recursive(SYSTEMD_CGROUP_CONTROLLER, test_a) > 0);
+ assert_se(cg_is_empty_recursive(SYSTEMD_CGROUP_CONTROLLER, test_b) == 0);
+
+ assert_se(cg_kill_recursive(test_a, 0, 0, NULL, NULL, NULL) == 0);
+ assert_se(cg_kill_recursive(test_b, 0, 0, NULL, NULL, NULL) > 0);
+
+ assert_se(cg_migrate_recursive(SYSTEMD_CGROUP_CONTROLLER, test_b, SYSTEMD_CGROUP_CONTROLLER, test_a, 0) > 0);
+
+ assert_se(cg_is_empty_recursive(SYSTEMD_CGROUP_CONTROLLER, test_a) == 0);
+ assert_se(cg_is_empty_recursive(SYSTEMD_CGROUP_CONTROLLER, test_b) > 0);
+
+ assert_se(cg_kill_recursive(test_a, 0, 0, NULL, NULL, NULL) > 0);
+ assert_se(cg_kill_recursive(test_b, 0, 0, NULL, NULL, NULL) == 0);
+
+ (void) cg_trim(SYSTEMD_CGROUP_CONTROLLER, test_b, false);
+
+ assert_se(cg_rmdir(SYSTEMD_CGROUP_CONTROLLER, test_b) == 0);
+ assert_se(cg_rmdir(SYSTEMD_CGROUP_CONTROLLER, test_a) < 0);
+ assert_se(cg_migrate_recursive(SYSTEMD_CGROUP_CONTROLLER, test_a, SYSTEMD_CGROUP_CONTROLLER, here, 0) > 0);
+ assert_se(cg_rmdir(SYSTEMD_CGROUP_CONTROLLER, test_a) == 0);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-chase-manual.c b/src/test/test-chase-manual.c
new file mode 100644
index 0000000..475f089
--- /dev/null
+++ b/src/test/test-chase-manual.c
@@ -0,0 +1,116 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#include <getopt.h>
+
+#include "chase.h"
+#include "fd-util.h"
+#include "log.h"
+#include "main-func.h"
+#include "tests.h"
+
+static char *arg_root = NULL;
+static int arg_flags = 0;
+static bool arg_open = false;
+
+static int parse_argv(int argc, char *argv[]) {
+ enum {
+ ARG_ROOT = 0x1000,
+ ARG_OPEN,
+ };
+
+ static const struct option options[] = {
+ { "help", no_argument, NULL, 'h' },
+ { "root", required_argument, NULL, ARG_ROOT },
+ { "open", no_argument, NULL, ARG_OPEN },
+
+ { "prefix-root", no_argument, NULL, CHASE_PREFIX_ROOT },
+ { "nonexistent", no_argument, NULL, CHASE_NONEXISTENT },
+ { "no_autofs", no_argument, NULL, CHASE_NO_AUTOFS },
+ { "safe", no_argument, NULL, CHASE_SAFE },
+ { "trail-slash", no_argument, NULL, CHASE_TRAIL_SLASH },
+ { "step", no_argument, NULL, CHASE_STEP },
+ { "nofollow", no_argument, NULL, CHASE_NOFOLLOW },
+ { "warn", no_argument, NULL, CHASE_WARN },
+ {}
+ };
+
+ int c;
+
+ assert_se(argc >= 0);
+ assert_se(argv);
+
+ while ((c = getopt_long(argc, argv, "", options, NULL)) >= 0)
+ switch (c) {
+
+ case 'h':
+ printf("Syntax:\n"
+ " %s [OPTION...] path...\n"
+ "Options:\n"
+ , argv[0]);
+ for (size_t i = 0; i < ELEMENTSOF(options) - 1; i++)
+ printf(" --%s\n", options[i].name);
+ return 0;
+
+ case ARG_ROOT:
+ arg_root = optarg;
+ break;
+
+ case ARG_OPEN:
+ arg_open = true;
+ break;
+
+ case CHASE_PREFIX_ROOT:
+ case CHASE_NONEXISTENT:
+ case CHASE_NO_AUTOFS:
+ case CHASE_SAFE:
+ case CHASE_TRAIL_SLASH:
+ case CHASE_STEP:
+ case CHASE_NOFOLLOW:
+ case CHASE_WARN:
+ arg_flags |= c;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached();
+ }
+
+ if (optind == argc)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "At least one argument is required.");
+
+ return 1;
+}
+
+static int run(int argc, char **argv) {
+ int r;
+
+ test_setup_logging(LOG_DEBUG);
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ for (int i = optind; i < argc; i++) {
+ _cleanup_free_ char *p = NULL;
+ _cleanup_close_ int fd = -EBADF;
+
+ printf("%s ", argv[i]);
+ fflush(stdout);
+
+ r = chase(argv[i], arg_root, arg_flags, &p, arg_open ? &fd : NULL);
+ if (r < 0)
+ log_error_errno(r, "failed: %m");
+ else {
+ log_info("→ %s", p);
+ if (arg_open)
+ assert_se(fd >= 0);
+ else
+ assert_se(fd == -EBADF);
+ }
+ }
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/test/test-chase.c b/src/test/test-chase.c
new file mode 100644
index 0000000..dbbc99b
--- /dev/null
+++ b/src/test/test-chase.c
@@ -0,0 +1,756 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "chase.h"
+#include "dirent-util.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "id128-util.h"
+#include "mkdir.h"
+#include "path-util.h"
+#include "rm-rf.h"
+#include "string-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+static const char *arg_test_dir = NULL;
+
+static void test_chase_extract_filename_one(const char *path, const char *root, const char *expected) {
+ _cleanup_free_ char *ret1 = NULL, *ret2 = NULL, *fname = NULL;
+
+ log_debug("/* %s(path=%s, root=%s) */", __func__, path, strnull(root));
+
+ assert_se(chase(path, root, CHASE_EXTRACT_FILENAME, &ret1, NULL) > 0);
+ assert_se(streq(ret1, expected));
+
+ assert_se(chase(path, root, 0, &ret2, NULL) > 0);
+ assert_se(chase_extract_filename(ret2, root, &fname) >= 0);
+ assert_se(streq(fname, expected));
+}
+
+TEST(chase) {
+ _cleanup_free_ char *result = NULL, *pwd = NULL;
+ _cleanup_close_ int pfd = -EBADF;
+ char *temp;
+ const char *top, *p, *pslash, *q, *qslash;
+ struct stat st;
+ int r;
+
+ temp = strjoina(arg_test_dir ?: "/tmp", "/test-chase.XXXXXX");
+ assert_se(mkdtemp(temp));
+
+ top = strjoina(temp, "/top");
+ assert_se(mkdir(top, 0700) >= 0);
+
+ p = strjoina(top, "/dot");
+ if (symlink(".", p) < 0) {
+ assert_se(IN_SET(errno, EINVAL, ENOSYS, ENOTTY, EPERM));
+ log_tests_skipped_errno(errno, "symlink() not possible");
+ goto cleanup;
+ };
+
+ p = strjoina(top, "/dotdot");
+ assert_se(symlink("..", p) >= 0);
+
+ p = strjoina(top, "/dotdota");
+ assert_se(symlink("../a", p) >= 0);
+
+ p = strjoina(temp, "/a");
+ assert_se(symlink("b", p) >= 0);
+
+ p = strjoina(temp, "/b");
+ assert_se(symlink("/usr", p) >= 0);
+
+ p = strjoina(temp, "/start");
+ assert_se(symlink("top/dot/dotdota", p) >= 0);
+
+ /* Paths that use symlinks underneath the "root" */
+
+ r = chase(p, NULL, 0, &result, NULL);
+ assert_se(r > 0);
+ assert_se(path_equal(result, "/usr"));
+ result = mfree(result);
+
+ r = chase(p, "/.//../../../", 0, &result, NULL);
+ assert_se(r > 0);
+ assert_se(path_equal(result, "/usr"));
+ result = mfree(result);
+
+ pslash = strjoina(p, "/");
+ r = chase(pslash, NULL, 0, &result, NULL);
+ assert_se(r > 0);
+ assert_se(path_equal(result, "/usr/"));
+ result = mfree(result);
+
+ r = chase(p, temp, 0, &result, NULL);
+ assert_se(r == -ENOENT);
+
+ r = chase(pslash, temp, 0, &result, NULL);
+ assert_se(r == -ENOENT);
+
+ q = strjoina(temp, "/usr");
+
+ r = chase(p, temp, CHASE_NONEXISTENT, &result, NULL);
+ assert_se(r == 0);
+ assert_se(path_equal(result, q));
+ result = mfree(result);
+
+ qslash = strjoina(q, "/");
+
+ r = chase(pslash, temp, CHASE_NONEXISTENT, &result, NULL);
+ assert_se(r == 0);
+ assert_se(path_equal(result, qslash));
+ result = mfree(result);
+
+ assert_se(mkdir(q, 0700) >= 0);
+
+ r = chase(p, temp, 0, &result, NULL);
+ assert_se(r > 0);
+ assert_se(path_equal(result, q));
+ result = mfree(result);
+
+ r = chase(pslash, temp, 0, &result, NULL);
+ assert_se(r > 0);
+ assert_se(path_equal(result, qslash));
+ result = mfree(result);
+
+ p = strjoina(temp, "/slash");
+ assert_se(symlink("/", p) >= 0);
+
+ r = chase(p, NULL, 0, &result, NULL);
+ assert_se(r > 0);
+ assert_se(path_equal(result, "/"));
+ result = mfree(result);
+
+ r = chase(p, temp, 0, &result, NULL);
+ assert_se(r > 0);
+ assert_se(path_equal(result, temp));
+ result = mfree(result);
+
+ /* Tests for CHASE_EXTRACT_FILENAME and chase_extract_filename() */
+
+ p = strjoina(temp, "/start");
+ pslash = strjoina(p, "/");
+ test_chase_extract_filename_one(p, NULL, "usr");
+ test_chase_extract_filename_one(pslash, NULL, "usr");
+ test_chase_extract_filename_one(p, temp, "usr");
+ test_chase_extract_filename_one(pslash, temp, "usr");
+
+ p = strjoina(temp, "/slash");
+ test_chase_extract_filename_one(p, NULL, ".");
+ test_chase_extract_filename_one(p, temp, ".");
+
+ /* Paths that would "escape" outside of the "root" */
+
+ p = strjoina(temp, "/6dots");
+ assert_se(symlink("../../..", p) >= 0);
+
+ r = chase(p, temp, 0, &result, NULL);
+ assert_se(r > 0 && path_equal(result, temp));
+ result = mfree(result);
+
+ p = strjoina(temp, "/6dotsusr");
+ assert_se(symlink("../../../usr", p) >= 0);
+
+ r = chase(p, temp, 0, &result, NULL);
+ assert_se(r > 0 && path_equal(result, q));
+ result = mfree(result);
+
+ p = strjoina(temp, "/top/8dotsusr");
+ assert_se(symlink("../../../../usr", p) >= 0);
+
+ r = chase(p, temp, 0, &result, NULL);
+ assert_se(r > 0 && path_equal(result, q));
+ result = mfree(result);
+
+ /* Paths that contain repeated slashes */
+
+ p = strjoina(temp, "/slashslash");
+ assert_se(symlink("///usr///", p) >= 0);
+
+ r = chase(p, NULL, 0, &result, NULL);
+ assert_se(r > 0);
+ assert_se(path_equal(result, "/usr"));
+ assert_se(streq(result, "/usr")); /* we guarantee that we drop redundant slashes */
+ result = mfree(result);
+
+ r = chase(p, temp, 0, &result, NULL);
+ assert_se(r > 0);
+ assert_se(path_equal(result, q));
+ result = mfree(result);
+
+ /* Paths underneath the "root" with different UIDs while using CHASE_SAFE */
+
+ if (geteuid() == 0) {
+ p = strjoina(temp, "/user");
+ assert_se(mkdir(p, 0755) >= 0);
+ assert_se(chown(p, UID_NOBODY, GID_NOBODY) >= 0);
+
+ q = strjoina(temp, "/user/root");
+ assert_se(mkdir(q, 0755) >= 0);
+
+ p = strjoina(q, "/link");
+ assert_se(symlink("/", p) >= 0);
+
+ /* Fail when user-owned directories contain root-owned subdirectories. */
+ r = chase(p, temp, CHASE_SAFE, &result, NULL);
+ assert_se(r == -ENOLINK);
+ result = mfree(result);
+
+ /* Allow this when the user-owned directories are all in the "root". */
+ r = chase(p, q, CHASE_SAFE, &result, NULL);
+ assert_se(r > 0);
+ result = mfree(result);
+ }
+
+ /* Paths using . */
+
+ r = chase("/etc/./.././", NULL, 0, &result, NULL);
+ assert_se(r > 0);
+ assert_se(path_equal(result, "/"));
+ result = mfree(result);
+
+ r = chase("/etc/./.././", "/etc", 0, &result, NULL);
+ assert_se(r > 0 && path_equal(result, "/etc"));
+ result = mfree(result);
+
+ r = chase("/../.././//../../etc", NULL, 0, &result, NULL);
+ assert_se(r > 0);
+ assert_se(streq(result, "/etc"));
+ result = mfree(result);
+
+ r = chase("/../.././//../../test-chase.fsldajfl", NULL, CHASE_NONEXISTENT, &result, NULL);
+ assert_se(r == 0);
+ assert_se(streq(result, "/test-chase.fsldajfl"));
+ result = mfree(result);
+
+ r = chase("/../.././//../../etc", "/", CHASE_PREFIX_ROOT, &result, NULL);
+ assert_se(r > 0);
+ assert_se(streq(result, "/etc"));
+ result = mfree(result);
+
+ r = chase("/../.././//../../test-chase.fsldajfl", "/", CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &result, NULL);
+ assert_se(r == 0);
+ assert_se(streq(result, "/test-chase.fsldajfl"));
+ result = mfree(result);
+
+ r = chase("/etc/machine-id/foo", NULL, 0, &result, NULL);
+ assert_se(IN_SET(r, -ENOTDIR, -ENOENT));
+ result = mfree(result);
+
+ /* Path that loops back to self */
+
+ p = strjoina(temp, "/recursive-symlink");
+ assert_se(symlink("recursive-symlink", p) >= 0);
+ r = chase(p, NULL, 0, &result, NULL);
+ assert_se(r == -ELOOP);
+
+ /* Path which doesn't exist */
+
+ p = strjoina(temp, "/idontexist");
+ r = chase(p, NULL, 0, &result, NULL);
+ assert_se(r == -ENOENT);
+
+ r = chase(p, NULL, CHASE_NONEXISTENT, &result, NULL);
+ assert_se(r == 0);
+ assert_se(path_equal(result, p));
+ result = mfree(result);
+
+ p = strjoina(temp, "/idontexist/meneither");
+ r = chase(p, NULL, 0, &result, NULL);
+ assert_se(r == -ENOENT);
+
+ r = chase(p, NULL, CHASE_NONEXISTENT, &result, NULL);
+ assert_se(r == 0);
+ assert_se(path_equal(result, p));
+ result = mfree(result);
+
+ /* Relative paths */
+
+ assert_se(safe_getcwd(&pwd) >= 0);
+
+ assert_se(chdir(temp) >= 0);
+
+ p = "this/is/a/relative/path";
+ r = chase(p, NULL, CHASE_NONEXISTENT, &result, NULL);
+ assert_se(r == 0);
+
+ p = strjoina(temp, "/", p);
+ assert_se(path_equal(result, p));
+ result = mfree(result);
+
+ p = "this/is/a/relative/path";
+ r = chase(p, temp, CHASE_NONEXISTENT, &result, NULL);
+ assert_se(r == 0);
+
+ p = strjoina(temp, "/", p);
+ assert_se(path_equal(result, p));
+ result = mfree(result);
+
+ assert_se(chdir(pwd) >= 0);
+
+ /* Path which doesn't exist, but contains weird stuff */
+
+ p = strjoina(temp, "/idontexist/..");
+ r = chase(p, NULL, 0, &result, NULL);
+ assert_se(r == -ENOENT);
+
+ r = chase(p, NULL, CHASE_NONEXISTENT, &result, NULL);
+ assert_se(r == -ENOENT);
+
+ p = strjoina(temp, "/target");
+ q = strjoina(temp, "/top");
+ assert_se(symlink(q, p) >= 0);
+ p = strjoina(temp, "/target/idontexist");
+ r = chase(p, NULL, 0, &result, NULL);
+ assert_se(r == -ENOENT);
+
+ if (geteuid() == 0) {
+ p = strjoina(temp, "/priv1");
+ assert_se(mkdir(p, 0755) >= 0);
+
+ q = strjoina(p, "/priv2");
+ assert_se(mkdir(q, 0755) >= 0);
+
+ assert_se(chase(q, NULL, CHASE_SAFE, NULL, NULL) >= 0);
+
+ assert_se(chown(q, UID_NOBODY, GID_NOBODY) >= 0);
+ assert_se(chase(q, NULL, CHASE_SAFE, NULL, NULL) >= 0);
+
+ assert_se(chown(p, UID_NOBODY, GID_NOBODY) >= 0);
+ assert_se(chase(q, NULL, CHASE_SAFE, NULL, NULL) >= 0);
+
+ assert_se(chown(q, 0, 0) >= 0);
+ assert_se(chase(q, NULL, CHASE_SAFE, NULL, NULL) == -ENOLINK);
+
+ assert_se(rmdir(q) >= 0);
+ assert_se(symlink("/etc/passwd", q) >= 0);
+ assert_se(chase(q, NULL, CHASE_SAFE, NULL, NULL) == -ENOLINK);
+
+ assert_se(chown(p, 0, 0) >= 0);
+ assert_se(chase(q, NULL, CHASE_SAFE, NULL, NULL) >= 0);
+ }
+
+ p = strjoina(temp, "/machine-id-test");
+ assert_se(symlink("/usr/../etc/./machine-id", p) >= 0);
+
+ r = chase(p, NULL, 0, NULL, &pfd);
+ if (r != -ENOENT && sd_id128_get_machine(NULL) >= 0) {
+ _cleanup_close_ int fd = -EBADF;
+ sd_id128_t a, b;
+
+ assert_se(pfd >= 0);
+
+ fd = fd_reopen(pfd, O_RDONLY|O_CLOEXEC);
+ assert_se(fd >= 0);
+ safe_close(pfd);
+
+ assert_se(id128_read_fd(fd, ID128_FORMAT_PLAIN, &a) >= 0);
+ assert_se(sd_id128_get_machine(&b) >= 0);
+ assert_se(sd_id128_equal(a, b));
+ }
+
+ assert_se(lstat(p, &st) >= 0);
+ r = chase_and_unlink(p, NULL, 0, 0, &result);
+ assert_se(r == 0);
+ assert_se(path_equal(result, p));
+ result = mfree(result);
+ assert_se(lstat(p, &st) == -1 && errno == ENOENT);
+
+ /* Test CHASE_NOFOLLOW */
+
+ p = strjoina(temp, "/target");
+ q = strjoina(temp, "/symlink");
+ assert_se(symlink(p, q) >= 0);
+ r = chase(q, NULL, CHASE_NOFOLLOW, &result, &pfd);
+ assert_se(r >= 0);
+ assert_se(pfd >= 0);
+ assert_se(path_equal(result, q));
+ assert_se(fstat(pfd, &st) >= 0);
+ assert_se(S_ISLNK(st.st_mode));
+ result = mfree(result);
+ pfd = safe_close(pfd);
+
+ /* s1 -> s2 -> nonexistent */
+ q = strjoina(temp, "/s1");
+ assert_se(symlink("s2", q) >= 0);
+ p = strjoina(temp, "/s2");
+ assert_se(symlink("nonexistent", p) >= 0);
+ r = chase(q, NULL, CHASE_NOFOLLOW, &result, &pfd);
+ assert_se(r >= 0);
+ assert_se(pfd >= 0);
+ assert_se(path_equal(result, q));
+ assert_se(fstat(pfd, &st) >= 0);
+ assert_se(S_ISLNK(st.st_mode));
+ result = mfree(result);
+ pfd = safe_close(pfd);
+
+ /* Test CHASE_STEP */
+
+ p = strjoina(temp, "/start");
+ r = chase(p, NULL, CHASE_STEP, &result, NULL);
+ assert_se(r == 0);
+ p = strjoina(temp, "/top/dot/dotdota");
+ assert_se(streq(p, result));
+ result = mfree(result);
+
+ r = chase(p, NULL, CHASE_STEP, &result, NULL);
+ assert_se(r == 0);
+ p = strjoina(temp, "/top/dotdota");
+ assert_se(streq(p, result));
+ result = mfree(result);
+
+ r = chase(p, NULL, CHASE_STEP, &result, NULL);
+ assert_se(r == 0);
+ p = strjoina(temp, "/top/../a");
+ assert_se(streq(p, result));
+ result = mfree(result);
+
+ r = chase(p, NULL, CHASE_STEP, &result, NULL);
+ assert_se(r == 0);
+ p = strjoina(temp, "/a");
+ assert_se(streq(p, result));
+ result = mfree(result);
+
+ r = chase(p, NULL, CHASE_STEP, &result, NULL);
+ assert_se(r == 0);
+ p = strjoina(temp, "/b");
+ assert_se(streq(p, result));
+ result = mfree(result);
+
+ r = chase(p, NULL, CHASE_STEP, &result, NULL);
+ assert_se(r == 0);
+ assert_se(streq("/usr", result));
+ result = mfree(result);
+
+ r = chase("/usr", NULL, CHASE_STEP, &result, NULL);
+ assert_se(r > 0);
+ assert_se(streq("/usr", result));
+ result = mfree(result);
+
+ /* Make sure that symlinks in the "root" path are not resolved, but those below are */
+ p = strjoina("/etc/..", temp, "/self");
+ assert_se(symlink(".", p) >= 0);
+ q = strjoina(p, "/top/dot/dotdota");
+ r = chase(q, p, 0, &result, NULL);
+ assert_se(r > 0);
+ assert_se(path_equal(path_startswith(result, p), "usr"));
+ result = mfree(result);
+
+ /* Test CHASE_PROHIBIT_SYMLINKS */
+
+ assert_se(chase("top/dot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, NULL, NULL) == -EREMCHG);
+ assert_se(chase("top/dot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_WARN, NULL, NULL) == -EREMCHG);
+ assert_se(chase("top/dotdot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, NULL, NULL) == -EREMCHG);
+ assert_se(chase("top/dotdot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_WARN, NULL, NULL) == -EREMCHG);
+ assert_se(chase("top/dot/dot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, NULL, NULL) == -EREMCHG);
+ assert_se(chase("top/dot/dot", temp, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS|CHASE_WARN, NULL, NULL) == -EREMCHG);
+
+ cleanup:
+ assert_se(rm_rf(temp, REMOVE_ROOT|REMOVE_PHYSICAL) >= 0);
+}
+
+TEST(chaseat) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ _cleanup_close_ int tfd = -EBADF, fd = -EBADF;
+ _cleanup_free_ char *result = NULL;
+ _cleanup_closedir_ DIR *dir = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ struct stat st;
+ const char *p;
+
+ assert_se((tfd = mkdtemp_open(NULL, 0, &t)) >= 0);
+
+ /* Test that AT_FDCWD with CHASE_AT_RESOLVE_IN_ROOT resolves against / and not the current working
+ * directory. */
+
+ assert_se(symlinkat("/usr", tfd, "abc") >= 0);
+
+ p = strjoina(t, "/abc");
+ assert_se(chaseat(AT_FDCWD, p, CHASE_AT_RESOLVE_IN_ROOT, &result, NULL) >= 0);
+ assert_se(streq(result, "/usr"));
+ result = mfree(result);
+
+ /* If the file descriptor points to the root directory, the result will be absolute. */
+
+ fd = open("/", O_CLOEXEC | O_DIRECTORY | O_PATH);
+ assert_se(fd >= 0);
+
+ assert_se(chaseat(fd, p, 0, &result, NULL) >= 0);
+ assert_se(streq(result, "/usr"));
+ result = mfree(result);
+
+ assert_se(chaseat(fd, p, CHASE_AT_RESOLVE_IN_ROOT, &result, NULL) >= 0);
+ assert_se(streq(result, "/usr"));
+ result = mfree(result);
+
+ fd = safe_close(fd);
+
+ /* If the file descriptor does not point to the root directory, the result will be relative
+ * unless the result is outside of the specified file descriptor. */
+
+ assert_se(chaseat(tfd, "abc", 0, &result, NULL) >= 0);
+ assert_se(streq(result, "/usr"));
+ result = mfree(result);
+
+ assert_se(chaseat(tfd, "/abc", 0, &result, NULL) >= 0);
+ assert_se(streq(result, "/usr"));
+ result = mfree(result);
+
+ assert_se(chaseat(tfd, "abc", CHASE_AT_RESOLVE_IN_ROOT, NULL, NULL) == -ENOENT);
+ assert_se(chaseat(tfd, "/abc", CHASE_AT_RESOLVE_IN_ROOT, NULL, NULL) == -ENOENT);
+
+ assert_se(chaseat(tfd, "abc", CHASE_AT_RESOLVE_IN_ROOT | CHASE_NONEXISTENT, &result, NULL) >= 0);
+ assert_se(streq(result, "usr"));
+ result = mfree(result);
+
+ assert_se(chaseat(tfd, "/abc", CHASE_AT_RESOLVE_IN_ROOT | CHASE_NONEXISTENT, &result, NULL) >= 0);
+ assert_se(streq(result, "usr"));
+ result = mfree(result);
+
+ /* Test that absolute path or not are the same when resolving relative to a directory file
+ * descriptor and that we always get a relative path back. */
+
+ assert_se(fd = openat(tfd, "def", O_CREAT|O_CLOEXEC, 0700) >= 0);
+ fd = safe_close(fd);
+ assert_se(symlinkat("/def", tfd, "qed") >= 0);
+ assert_se(chaseat(tfd, "qed", CHASE_AT_RESOLVE_IN_ROOT, &result, NULL) >= 0);
+ assert_se(streq(result, "def"));
+ result = mfree(result);
+ assert_se(chaseat(tfd, "/qed", CHASE_AT_RESOLVE_IN_ROOT, &result, NULL) >= 0);
+ assert_se(streq(result, "def"));
+ result = mfree(result);
+
+ /* Valid directory file descriptor without CHASE_AT_RESOLVE_IN_ROOT should resolve symlinks against
+ * host's root. */
+ assert_se(chaseat(tfd, "/qed", 0, NULL, NULL) == -ENOENT);
+
+ /* Test CHASE_PARENT */
+
+ assert_se((fd = open_mkdir_at(tfd, "chase", O_CLOEXEC, 0755)) >= 0);
+ assert_se(symlinkat("/def", fd, "parent") >= 0);
+ fd = safe_close(fd);
+
+ /* Make sure that when we chase a symlink parent directory, that we chase the parent directory of the
+ * symlink target and not the symlink itself. But if we add CHASE_NOFOLLOW, we get the parent
+ * directory of the symlink itself. */
+
+ assert_se(chaseat(tfd, "chase/parent", CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT, &result, &fd) >= 0);
+ assert_se(faccessat(fd, "def", F_OK, 0) >= 0);
+ assert_se(streq(result, "def"));
+ fd = safe_close(fd);
+ result = mfree(result);
+
+ assert_se(chaseat(tfd, "chase/parent", CHASE_AT_RESOLVE_IN_ROOT|CHASE_PARENT|CHASE_NOFOLLOW, &result, &fd) >= 0);
+ assert_se(faccessat(fd, "parent", F_OK, AT_SYMLINK_NOFOLLOW) >= 0);
+ assert_se(streq(result, "chase/parent"));
+ fd = safe_close(fd);
+ result = mfree(result);
+
+ assert_se(chaseat(tfd, "chase", CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT, &result, &fd) >= 0);
+ assert_se(faccessat(fd, "chase", F_OK, 0) >= 0);
+ assert_se(streq(result, "chase"));
+ fd = safe_close(fd);
+ result = mfree(result);
+
+ assert_se(chaseat(tfd, "/", CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT, &result, NULL) >= 0);
+ assert_se(streq(result, "."));
+ result = mfree(result);
+
+ assert_se(chaseat(tfd, ".", CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT, &result, NULL) >= 0);
+ assert_se(streq(result, "."));
+ result = mfree(result);
+
+ /* Test CHASE_MKDIR_0755 */
+
+ assert_se(chaseat(tfd, "m/k/d/i/r", CHASE_MKDIR_0755|CHASE_NONEXISTENT, &result, NULL) >= 0);
+ assert_se(faccessat(tfd, "m/k/d/i", F_OK, 0) >= 0);
+ assert_se(RET_NERRNO(faccessat(tfd, "m/k/d/i/r", F_OK, 0)) == -ENOENT);
+ assert_se(streq(result, "m/k/d/i/r"));
+ result = mfree(result);
+
+ assert_se(chaseat(tfd, "m/../q", CHASE_MKDIR_0755|CHASE_NONEXISTENT, &result, NULL) >= 0);
+ assert_se(faccessat(tfd, "m", F_OK, 0) >= 0);
+ assert_se(RET_NERRNO(faccessat(tfd, "q", F_OK, 0)) == -ENOENT);
+ assert_se(streq(result, "q"));
+ result = mfree(result);
+
+ assert_se(chaseat(tfd, "i/../p", CHASE_MKDIR_0755|CHASE_NONEXISTENT, NULL, NULL) == -ENOENT);
+
+ /* Test CHASE_EXTRACT_FILENAME */
+
+ assert_se(chaseat(tfd, "chase/parent", CHASE_AT_RESOLVE_IN_ROOT|CHASE_PARENT|CHASE_NOFOLLOW|CHASE_EXTRACT_FILENAME, &result, &fd) >= 0);
+ assert_se(faccessat(fd, result, F_OK, AT_SYMLINK_NOFOLLOW) >= 0);
+ assert_se(streq(result, "parent"));
+ fd = safe_close(fd);
+ result = mfree(result);
+
+ assert_se(chaseat(tfd, "chase", CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT|CHASE_EXTRACT_FILENAME, &result, &fd) >= 0);
+ assert_se(faccessat(fd, result, F_OK, 0) >= 0);
+ assert_se(streq(result, "chase"));
+ fd = safe_close(fd);
+ result = mfree(result);
+
+ assert_se(chaseat(tfd, "/", CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT|CHASE_EXTRACT_FILENAME, &result, NULL) >= 0);
+ assert_se(streq(result, "."));
+ result = mfree(result);
+
+ assert_se(chaseat(tfd, ".", CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT|CHASE_EXTRACT_FILENAME, &result, NULL) >= 0);
+ assert_se(streq(result, "."));
+ result = mfree(result);
+
+ assert_se(chaseat(tfd, NULL, CHASE_PARENT|CHASE_AT_RESOLVE_IN_ROOT|CHASE_EXTRACT_FILENAME, &result, NULL) >= 0);
+ assert_se(streq(result, "."));
+ result = mfree(result);
+
+ /* Test chase_and_openat() */
+
+ fd = chase_and_openat(tfd, "o/p/e/n/f/i/l/e", CHASE_MKDIR_0755, O_CREAT|O_EXCL|O_CLOEXEC, NULL);
+ assert_se(fd >= 0);
+ assert_se(fd_verify_regular(fd) >= 0);
+ fd = safe_close(fd);
+
+ fd = chase_and_openat(tfd, "o/p/e/n/d/i/r", CHASE_MKDIR_0755, O_DIRECTORY|O_CREAT|O_EXCL|O_CLOEXEC, NULL);
+ assert_se(fd >= 0);
+ assert_se(fd_verify_directory(fd) >= 0);
+ fd = safe_close(fd);
+
+ fd = chase_and_openat(tfd, NULL, CHASE_PARENT|CHASE_EXTRACT_FILENAME, O_PATH|O_DIRECTORY|O_CLOEXEC, &result);
+ assert_se(fd >= 0);
+ assert_se(streq(result, "."));
+ fd = safe_close(fd);
+ result = mfree(result);
+
+ /* Test chase_and_openatdir() */
+
+ assert_se(chase_and_opendirat(tfd, "o/p/e/n/d/i", 0, &result, &dir) >= 0);
+ FOREACH_DIRENT(de, dir, assert_not_reached())
+ assert_se(streq(de->d_name, "r"));
+ assert_se(streq(result, "o/p/e/n/d/i"));
+ result = mfree(result);
+
+ /* Test chase_and_statat() */
+
+ assert_se(chase_and_statat(tfd, "o/p", 0, &result, &st) >= 0);
+ assert_se(stat_verify_directory(&st) >= 0);
+ assert_se(streq(result, "o/p"));
+ result = mfree(result);
+
+ /* Test chase_and_accessat() */
+
+ assert_se(chase_and_accessat(tfd, "o/p/e", 0, F_OK, &result) >= 0);
+ assert_se(streq(result, "o/p/e"));
+ result = mfree(result);
+
+ /* Test chase_and_fopenat_unlocked() */
+
+ assert_se(chase_and_fopenat_unlocked(tfd, "o/p/e/n/f/i/l/e", 0, "re", &result, &f) >= 0);
+ assert_se(fread(&(char[1]) {}, 1, 1, f) == 0);
+ assert_se(feof(f));
+ f = safe_fclose(f);
+ assert_se(streq(result, "o/p/e/n/f/i/l/e"));
+ result = mfree(result);
+
+ /* Test chase_and_unlinkat() */
+
+ assert_se(chase_and_unlinkat(tfd, "o/p/e/n/f/i/l/e", 0, 0, &result) >= 0);
+ assert_se(streq(result, "o/p/e/n/f/i/l/e"));
+ result = mfree(result);
+
+ /* Test chase_and_open_parent_at() */
+
+ assert_se((fd = chase_and_open_parent_at(tfd, "chase/parent", CHASE_AT_RESOLVE_IN_ROOT|CHASE_NOFOLLOW, &result)) >= 0);
+ assert_se(faccessat(fd, result, F_OK, AT_SYMLINK_NOFOLLOW) >= 0);
+ assert_se(streq(result, "parent"));
+ fd = safe_close(fd);
+ result = mfree(result);
+
+ assert_se((fd = chase_and_open_parent_at(tfd, "chase", CHASE_AT_RESOLVE_IN_ROOT, &result)) >= 0);
+ assert_se(faccessat(fd, result, F_OK, 0) >= 0);
+ assert_se(streq(result, "chase"));
+ fd = safe_close(fd);
+ result = mfree(result);
+
+ assert_se((fd = chase_and_open_parent_at(tfd, "/", CHASE_AT_RESOLVE_IN_ROOT, &result)) >= 0);
+ assert_se(streq(result, "."));
+ fd = safe_close(fd);
+ result = mfree(result);
+
+ assert_se((fd = chase_and_open_parent_at(tfd, ".", CHASE_AT_RESOLVE_IN_ROOT, &result)) >= 0);
+ assert_se(streq(result, "."));
+ fd = safe_close(fd);
+ result = mfree(result);
+}
+
+TEST(chaseat_prefix_root) {
+ _cleanup_free_ char *cwd = NULL, *ret = NULL, *expected = NULL;
+
+ assert_se(safe_getcwd(&cwd) >= 0);
+
+ assert_se(chaseat_prefix_root("/hoge", NULL, &ret) >= 0);
+ assert_se(streq(ret, "/hoge"));
+
+ ret = mfree(ret);
+
+ assert_se(chaseat_prefix_root("/hoge", "a/b/c", &ret) >= 0);
+ assert_se(streq(ret, "/hoge"));
+
+ ret = mfree(ret);
+
+ assert_se(chaseat_prefix_root("hoge", "/a/b//./c///", &ret) >= 0);
+ assert_se(streq(ret, "/a/b/c/hoge"));
+
+ ret = mfree(ret);
+
+ assert_se(chaseat_prefix_root("hoge", "a/b//./c///", &ret) >= 0);
+ assert_se(expected = path_join(cwd, "a/b/c/hoge"));
+ assert_se(streq(ret, expected));
+
+ ret = mfree(ret);
+ expected = mfree(expected);
+
+ assert_se(chaseat_prefix_root("./hoge/aaa/../././b", "/a/b//./c///", &ret) >= 0);
+ assert_se(streq(ret, "/a/b/c/hoge/aaa/../././b"));
+
+ ret = mfree(ret);
+
+ assert_se(chaseat_prefix_root("./hoge/aaa/../././b", "a/b//./c///", &ret) >= 0);
+ assert_se(expected = path_join(cwd, "a/b/c/hoge/aaa/../././b"));
+ assert_se(streq(ret, expected));
+}
+
+TEST(trailing_dot_dot) {
+ _cleanup_free_ char *path = NULL, *fdpath = NULL;
+ _cleanup_close_ int fd = -EBADF;
+
+ assert_se(chase("/usr/..", NULL, CHASE_PARENT, &path, &fd) >= 0);
+ assert_se(path_equal(path, "/"));
+ assert_se(fd_get_path(fd, &fdpath) >= 0);
+ assert_se(path_equal(fdpath, "/"));
+
+ path = mfree(path);
+ fdpath = mfree(fdpath);
+ fd = safe_close(fd);
+
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ assert_se(mkdtemp_malloc(NULL, &t) >= 0);
+ _cleanup_free_ char *sub = ASSERT_PTR(path_join(t, "a/b/c/d"));
+ assert_se(mkdir_p(sub, 0700) >= 0);
+ _cleanup_free_ char *suffixed = ASSERT_PTR(path_join(sub, ".."));
+ assert_se(chase(suffixed, NULL, CHASE_PARENT, &path, &fd) >= 0);
+ _cleanup_free_ char *expected1 = ASSERT_PTR(path_join(t, "a/b/c"));
+ _cleanup_free_ char *expected2 = ASSERT_PTR(path_join(t, "a/b"));
+
+ assert_se(path_equal(path, expected1));
+ assert_se(fd_get_path(fd, &fdpath) >= 0);
+ assert_se(path_equal(fdpath, expected2));
+}
+
+static int intro(void) {
+ arg_test_dir = saved_argv[1];
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_INFO, intro);
diff --git a/src/test/test-chown-rec.c b/src/test/test-chown-rec.c
new file mode 100644
index 0000000..5d83f59
--- /dev/null
+++ b/src/test/test-chown-rec.c
@@ -0,0 +1,162 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/xattr.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "chown-recursive.h"
+#include "log.h"
+#include "rm-rf.h"
+#include "string-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+static const uint8_t acl[] = {
+ 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x07, 0x00,
+ 0xff, 0xff, 0xff, 0xff, 0x02, 0x00, 0x07, 0x00,
+ 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x07, 0x00,
+ 0xff, 0xff, 0xff, 0xff, 0x10, 0x00, 0x07, 0x00,
+ 0xff, 0xff, 0xff, 0xff, 0x20, 0x00, 0x05, 0x00,
+ 0xff, 0xff, 0xff, 0xff,
+};
+
+static const uint8_t default_acl[] = {
+ 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x07, 0x00,
+ 0xff, 0xff, 0xff, 0xff, 0x04, 0x00, 0x07, 0x00,
+ 0xff, 0xff, 0xff, 0xff, 0x08, 0x00, 0x07, 0x00,
+ 0x04, 0x00, 0x00, 0x00, 0x10, 0x00, 0x07, 0x00,
+ 0xff, 0xff, 0xff, 0xff, 0x20, 0x00, 0x05, 0x00,
+ 0xff, 0xff, 0xff, 0xff,
+};
+
+static bool has_xattr(const char *p) {
+ char buffer[sizeof(acl) * 4];
+
+ if (lgetxattr(p, "system.posix_acl_access", buffer, sizeof(buffer)) < 0)
+ return !ERRNO_IS_XATTR_ABSENT(errno);
+
+ return true;
+}
+
+TEST(chown_recursive) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ struct stat st;
+ const char *p;
+ const uid_t uid = getuid();
+ const gid_t gid = getgid();
+ int r;
+
+ umask(022);
+ assert_se(mkdtemp_malloc(NULL, &t) >= 0);
+
+ p = strjoina(t, "/dir");
+ assert_se(mkdir(p, 0777) >= 0);
+ assert_se(lstat(p, &st) >= 0);
+ assert_se(S_ISDIR(st.st_mode));
+ assert_se((st.st_mode & 07777) == 0755);
+ assert_se(st.st_uid == uid);
+ assert_se(st.st_gid == gid);
+ assert_se(!has_xattr(p));
+
+ p = strjoina(t, "/dir/symlink");
+ assert_se(symlink("../../", p) >= 0);
+ assert_se(lstat(p, &st) >= 0);
+ assert_se(S_ISLNK(st.st_mode));
+ assert_se((st.st_mode & 07777) == 0777);
+ assert_se(st.st_uid == uid);
+ assert_se(st.st_gid == gid);
+ assert_se(!has_xattr(p));
+
+ p = strjoina(t, "/dir/reg");
+ assert_se(mknod(p, S_IFREG|0777, 0) >= 0);
+ assert_se(lstat(p, &st) >= 0);
+ assert_se(S_ISREG(st.st_mode));
+ assert_se((st.st_mode & 07777) == 0755);
+ assert_se(st.st_uid == uid);
+ assert_se(st.st_gid == gid);
+ assert_se(!has_xattr(p));
+
+ p = strjoina(t, "/dir/sock");
+ assert_se(mknod(p, S_IFSOCK|0777, 0) >= 0);
+ assert_se(lstat(p, &st) >= 0);
+ assert_se(S_ISSOCK(st.st_mode));
+ assert_se((st.st_mode & 07777) == 0755);
+ assert_se(st.st_uid == uid);
+ assert_se(st.st_gid == gid);
+ assert_se(!has_xattr(p));
+
+ p = strjoina(t, "/dir/fifo");
+ assert_se(mknod(p, S_IFIFO|0777, 0) >= 0);
+ assert_se(lstat(p, &st) >= 0);
+ assert_se(S_ISFIFO(st.st_mode));
+ assert_se((st.st_mode & 07777) == 0755);
+ assert_se(st.st_uid == uid);
+ assert_se(st.st_gid == gid);
+ assert_se(!has_xattr(p));
+
+ /* We now apply an xattr to the dir, and check it again */
+ p = strjoina(t, "/dir");
+ r = RET_NERRNO(setxattr(p, "system.posix_acl_access", acl, sizeof(acl), 0));
+ if (ERRNO_IS_NEG_NOT_SUPPORTED(r))
+ return (void) log_tests_skipped_errno(r, "no acl supported on /tmp");
+
+ assert_se(r >= 0);
+ assert_se(setxattr(p, "system.posix_acl_default", default_acl, sizeof(default_acl), 0) >= 0);
+ assert_se(lstat(p, &st) >= 0);
+ assert_se(S_ISDIR(st.st_mode));
+ assert_se((st.st_mode & 07777) == 0775); /* acl change changed the mode too */
+ assert_se(st.st_uid == uid);
+ assert_se(st.st_gid == gid);
+ assert_se(has_xattr(p));
+
+ assert_se(path_chown_recursive(t, 1, 2, 07777, 0) >= 0);
+
+ p = strjoina(t, "/dir");
+ assert_se(lstat(p, &st) >= 0);
+ assert_se(S_ISDIR(st.st_mode));
+ assert_se((st.st_mode & 07777) == 0775);
+ assert_se(st.st_uid == 1);
+ assert_se(st.st_gid == 2);
+ assert_se(!has_xattr(p));
+
+ p = strjoina(t, "/dir/symlink");
+ assert_se(lstat(p, &st) >= 0);
+ assert_se(S_ISLNK(st.st_mode));
+ assert_se((st.st_mode & 07777) == 0777);
+ assert_se(st.st_uid == 1);
+ assert_se(st.st_gid == 2);
+ assert_se(!has_xattr(p));
+
+ p = strjoina(t, "/dir/reg");
+ assert_se(lstat(p, &st) >= 0);
+ assert_se(S_ISREG(st.st_mode));
+ assert_se((st.st_mode & 07777) == 0755);
+ assert_se(st.st_uid == 1);
+ assert_se(st.st_gid == 2);
+ assert_se(!has_xattr(p));
+
+ p = strjoina(t, "/dir/sock");
+ assert_se(lstat(p, &st) >= 0);
+ assert_se(S_ISSOCK(st.st_mode));
+ assert_se((st.st_mode & 07777) == 0755);
+ assert_se(st.st_uid == 1);
+ assert_se(st.st_gid == 2);
+ assert_se(!has_xattr(p));
+
+ p = strjoina(t, "/dir/fifo");
+ assert_se(lstat(p, &st) >= 0);
+ assert_se(S_ISFIFO(st.st_mode));
+ assert_se((st.st_mode & 07777) == 0755);
+ assert_se(st.st_uid == 1);
+ assert_se(st.st_gid == 2);
+ assert_se(!has_xattr(p));
+}
+
+static int intro(void) {
+ if (geteuid() != 0)
+ return log_tests_skipped("not running as root");
+
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_DEBUG, intro);
diff --git a/src/test/test-clock.c b/src/test/test-clock.c
new file mode 100644
index 0000000..123831a
--- /dev/null
+++ b/src/test/test-clock.c
@@ -0,0 +1,74 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/***
+ Copyright © 2016 Canonical Ltd.
+***/
+
+#include <unistd.h>
+#include <fcntl.h>
+
+#include "clock-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "log.h"
+#include "macro.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+TEST(clock_is_localtime) {
+ _cleanup_(unlink_tempfilep) char adjtime[] = "/tmp/test-adjtime.XXXXXX";
+ _cleanup_fclose_ FILE* f = NULL;
+
+ static const struct scenario {
+ const char* contents;
+ int expected_result;
+ } scenarios[] = {
+ /* adjtime configures UTC */
+ {"0.0 0 0\n0\nUTC\n", 0},
+ /* adjtime configures local time */
+ {"0.0 0 0\n0\nLOCAL\n", 1},
+ /* no final EOL */
+ {"0.0 0 0\n0\nUTC", 0},
+ {"0.0 0 0\n0\nLOCAL", 1},
+ /* empty value -> defaults to UTC */
+ {"0.0 0 0\n0\n", 0},
+ /* unknown value -> defaults to UTC */
+ {"0.0 0 0\n0\nFOO\n", 0},
+ /* no third line */
+ {"0.0 0 0", 0},
+ {"0.0 0 0\n", 0},
+ {"0.0 0 0\n0", 0},
+ };
+
+ /* without an adjtime file we default to UTC */
+ assert_se(clock_is_localtime("/nonexisting/adjtime") == 0);
+
+ assert_se(fmkostemp_safe(adjtime, "w", &f) == 0);
+ log_info("adjtime test file: %s", adjtime);
+
+ for (size_t i = 0; i < ELEMENTSOF(scenarios); ++i) {
+ log_info("scenario #%zu:, expected result %i", i, scenarios[i].expected_result);
+ log_info("%s", scenarios[i].contents);
+ rewind(f);
+ assert_se(ftruncate(fileno(f), 0) == 0);
+ assert_se(write_string_stream(f, scenarios[i].contents, WRITE_STRING_FILE_AVOID_NEWLINE) == 0);
+ assert_se(clock_is_localtime(adjtime) == scenarios[i].expected_result);
+ }
+}
+
+/* Test with the real /etc/adjtime */
+TEST(clock_is_localtime_system) {
+ int r;
+ r = clock_is_localtime(NULL);
+
+ if (access("/etc/adjtime", R_OK) == 0) {
+ log_info("/etc/adjtime is readable, clock_is_localtime() == %i", r);
+ /* if /etc/adjtime exists we expect some answer, no error or
+ * crash */
+ assert_se(IN_SET(r, 0, 1));
+ } else
+ /* default is UTC if there is no /etc/adjtime */
+ assert_se(r == 0 || ERRNO_IS_PRIVILEGE(r));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-compare-operator.c b/src/test/test-compare-operator.c
new file mode 100644
index 0000000..3d8f46f
--- /dev/null
+++ b/src/test/test-compare-operator.c
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "compare-operator.h"
+#include "tests.h"
+
+TEST(parse_compare_operator) {
+ const char *str_a = "<>version";
+ assert_se(parse_compare_operator(&str_a, 0) == COMPARE_UNEQUAL);
+ const char *str_b = "$=version";
+ assert_se(parse_compare_operator(&str_b, 0) == _COMPARE_OPERATOR_INVALID);
+ assert_se(parse_compare_operator(&str_b, COMPARE_ALLOW_FNMATCH) == COMPARE_FNMATCH_EQUAL);
+ const char *str_c = "eq oranges";
+ assert_se(parse_compare_operator(&str_c, 0) == _COMPARE_OPERATOR_INVALID);
+ assert_se(parse_compare_operator(&str_c, COMPARE_ALLOW_TEXTUAL) == COMPARE_EQUAL);
+ const char *str_d = "";
+ assert_se(parse_compare_operator(&str_d, 0) == _COMPARE_OPERATOR_INVALID);
+ const char *str_e = "!=!="; /* parse_compare_operator() moves the pointer */
+ assert_se(parse_compare_operator(&str_e, COMPARE_EQUAL_BY_STRING) == COMPARE_STRING_UNEQUAL);
+ assert_se(parse_compare_operator(&str_e, 0) == COMPARE_UNEQUAL);
+ assert_se(parse_compare_operator(&str_e, 0) == _COMPARE_OPERATOR_INVALID);
+}
+
+TEST(test_order) {
+ assert_se(!test_order(5, COMPARE_LOWER));
+ assert_se(!test_order(5, COMPARE_LOWER_OR_EQUAL));
+ assert_se(!test_order(5, COMPARE_EQUAL));
+ assert_se(test_order(5, COMPARE_UNEQUAL));
+ assert_se(test_order(5, COMPARE_GREATER_OR_EQUAL));
+ assert_se(test_order(5, COMPARE_GREATER));
+ assert_se(test_order(5, COMPARE_STRING_EQUAL) == -EINVAL);
+}
+
+TEST(version_or_fnmatch_compare) {
+ assert_se(version_or_fnmatch_compare(COMPARE_STRING_EQUAL, "locale", "locale"));
+ assert_se(version_or_fnmatch_compare(COMPARE_STRING_UNEQUAL, "locale", "LOCALE"));
+ assert_se(version_or_fnmatch_compare(COMPARE_FNMATCH_EQUAL, "locaale", "loc*le"));
+ assert_se(version_or_fnmatch_compare(COMPARE_FNMATCH_UNEQUAL, "locaale", "loc?le"));
+ assert_se(version_or_fnmatch_compare(COMPARE_GREATER, "local512", "local256"));
+ assert_se(version_or_fnmatch_compare(COMPARE_LOWER, "local52", "local256"));
+ assert_se(version_or_fnmatch_compare(_COMPARE_OPERATOR_MAX, "local512", "local256") == -EINVAL);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-compress-benchmark.c b/src/test/test-compress-benchmark.c
new file mode 100644
index 0000000..1727db8
--- /dev/null
+++ b/src/test/test-compress-benchmark.c
@@ -0,0 +1,176 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "compress.h"
+#include "env-util.h"
+#include "macro.h"
+#include "memory-util.h"
+#include "nulstr-util.h"
+#include "parse-util.h"
+#include "process-util.h"
+#include "random-util.h"
+#include "string-util.h"
+#include "tests.h"
+
+typedef int (compress_t)(const void *src, uint64_t src_size, void *dst,
+ size_t dst_alloc_size, size_t *dst_size);
+typedef int (decompress_t)(const void *src, uint64_t src_size,
+ void **dst, size_t* dst_size, size_t dst_max);
+
+#if HAVE_COMPRESSION
+
+static usec_t arg_duration;
+static size_t arg_start;
+
+#define MAX_SIZE (1024*1024LU)
+#define PRIME 1048571 /* A prime close enough to one megabyte that mod 4 == 3 */
+
+static size_t _permute(size_t x) {
+ size_t residue;
+
+ if (x >= PRIME)
+ return x;
+
+ residue = x*x % PRIME;
+ if (x <= PRIME / 2)
+ return residue;
+ else
+ return PRIME - residue;
+}
+
+static size_t permute(size_t x) {
+ return _permute((_permute(x) + arg_start) % MAX_SIZE ^ 0xFF345);
+}
+
+static char* make_buf(size_t count, const char *type) {
+ char *buf;
+ size_t i;
+
+ buf = malloc(count);
+ assert_se(buf);
+
+ if (streq(type, "zeros"))
+ memzero(buf, count);
+ else if (streq(type, "simple"))
+ for (i = 0; i < count; i++)
+ buf[i] = 'a' + i % ('z' - 'a' + 1);
+ else if (streq(type, "random")) {
+ size_t step = count / 10;
+
+ random_bytes(buf, step);
+ memzero(buf + 1*step, step);
+ random_bytes(buf + 2*step, step);
+ memzero(buf + 3*step, step);
+ random_bytes(buf + 4*step, step);
+ memzero(buf + 5*step, step);
+ random_bytes(buf + 6*step, step);
+ memzero(buf + 7*step, step);
+ random_bytes(buf + 8*step, step);
+ memzero(buf + 9*step, step);
+ } else
+ assert_not_reached();
+
+ return buf;
+}
+
+static void test_compress_decompress(const char* label, const char* type,
+ compress_t compress, decompress_t decompress) {
+ usec_t n, n2 = 0;
+ float dt;
+
+ _cleanup_free_ char *text = NULL, *buf = NULL;
+ _cleanup_free_ void *buf2 = NULL;
+ size_t skipped = 0, compressed = 0, total = 0;
+
+ text = make_buf(MAX_SIZE, type);
+ buf = calloc(MAX_SIZE + 1, 1);
+ assert_se(text && buf);
+
+ n = now(CLOCK_MONOTONIC);
+
+ for (size_t i = 0; i <= MAX_SIZE; i++) {
+ size_t j = 0, k = 0, size;
+ int r;
+
+ size = permute(i);
+ if (size == 0)
+ continue;
+
+ log_debug("%s %zu %zu", type, i, size);
+
+ memzero(buf, MIN(size + 1000, MAX_SIZE));
+
+ r = compress(text, size, buf, size, &j);
+ /* assume compression must be successful except for small or random inputs */
+ assert_se(r >= 0 || (size < 2048 && r == -ENOBUFS) || streq(type, "random"));
+
+ /* check for overwrites */
+ assert_se(buf[size] == 0);
+ if (r < 0) {
+ skipped += size;
+ continue;
+ }
+
+ assert_se(j > 0);
+ if (j >= size)
+ log_error("%s \"compressed\" %zu -> %zu", label, size, j);
+
+ r = decompress(buf, j, &buf2, &k, 0);
+ assert_se(r == 0);
+ assert_se(k == size);
+
+ assert_se(memcmp(text, buf2, size) == 0);
+
+ total += size;
+ compressed += j;
+
+ n2 = now(CLOCK_MONOTONIC);
+ if (n2 - n > arg_duration)
+ break;
+ }
+
+ dt = (n2-n) / 1e6;
+
+ log_info("%s/%s: compressed & decompressed %zu bytes in %.2fs (%.2fMiB/s), "
+ "mean compression %.2f%%, skipped %zu bytes",
+ label, type, total, dt,
+ total / 1024. / 1024 / dt,
+ 100 - compressed * 100. / total,
+ skipped);
+}
+#endif
+
+int main(int argc, char *argv[]) {
+#if HAVE_COMPRESSION
+ test_setup_logging(LOG_INFO);
+
+ if (argc >= 2) {
+ unsigned x;
+
+ assert_se(safe_atou(argv[1], &x) >= 0);
+ arg_duration = x * USEC_PER_SEC;
+ } else
+ arg_duration = slow_tests_enabled() ?
+ 2 * USEC_PER_SEC : USEC_PER_SEC / 50;
+
+ if (argc == 3)
+ (void) safe_atozu(argv[2], &arg_start);
+ else
+ arg_start = getpid_cached();
+
+ NULSTR_FOREACH(i, "zeros\0simple\0random\0") {
+#if HAVE_XZ
+ test_compress_decompress("XZ", i, compress_blob_xz, decompress_blob_xz);
+#endif
+#if HAVE_LZ4
+ test_compress_decompress("LZ4", i, compress_blob_lz4, decompress_blob_lz4);
+#endif
+#if HAVE_ZSTD
+ test_compress_decompress("ZSTD", i, compress_blob_zstd, decompress_blob_zstd);
+#endif
+ }
+ return 0;
+#else
+ return log_tests_skipped("No compression feature is enabled");
+#endif
+}
diff --git a/src/test/test-compress.c b/src/test/test-compress.c
new file mode 100644
index 0000000..2f20d00
--- /dev/null
+++ b/src/test/test-compress.c
@@ -0,0 +1,373 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/stat.h>
+
+#if HAVE_LZ4
+#include <lz4.h>
+#endif
+
+#include "alloc-util.h"
+#include "compress.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "macro.h"
+#include "memory-util.h"
+#include "path-util.h"
+#include "random-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+#if HAVE_XZ
+# define XZ_OK 0
+#else
+# define XZ_OK -EPROTONOSUPPORT
+#endif
+
+#if HAVE_LZ4
+# define LZ4_OK 0
+#else
+# define LZ4_OK -EPROTONOSUPPORT
+#endif
+
+#define HUGE_SIZE (4096*1024)
+
+typedef int (compress_blob_t)(const void *src, uint64_t src_size,
+ void *dst, size_t dst_alloc_size, size_t *dst_size);
+typedef int (decompress_blob_t)(const void *src, uint64_t src_size,
+ void **dst,
+ size_t* dst_size, size_t dst_max);
+typedef int (decompress_sw_t)(const void *src, uint64_t src_size,
+ void **buffer,
+ const void *prefix, size_t prefix_len,
+ uint8_t extra);
+
+typedef int (compress_stream_t)(int fdf, int fdt, uint64_t max_bytes, uint64_t *uncompressed_size);
+typedef int (decompress_stream_t)(int fdf, int fdt, uint64_t max_size);
+
+#if HAVE_COMPRESSION
+_unused_ static void test_compress_decompress(
+ const char *compression,
+ compress_blob_t compress,
+ decompress_blob_t decompress,
+ const char *data,
+ size_t data_len,
+ bool may_fail) {
+
+ char compressed[512];
+ size_t csize;
+ _cleanup_free_ char *decompressed = NULL;
+ int r;
+
+ log_info("/* testing %s %s blob compression/decompression */",
+ compression, data);
+
+ r = compress(data, data_len, compressed, sizeof(compressed), &csize);
+ if (r == -ENOBUFS) {
+ log_info_errno(r, "compression failed: %m");
+ assert_se(may_fail);
+ } else {
+ assert_se(r >= 0);
+ r = decompress(compressed, csize,
+ (void **) &decompressed, &csize, 0);
+ assert_se(r == 0);
+ assert_se(decompressed);
+ assert_se(memcmp(decompressed, data, data_len) == 0);
+ }
+
+ r = decompress("garbage", 7,
+ (void **) &decompressed, &csize, 0);
+ assert_se(r < 0);
+
+ /* make sure to have the minimal lz4 compressed size */
+ r = decompress("00000000\1g", 9,
+ (void **) &decompressed, &csize, 0);
+ assert_se(r < 0);
+
+ r = decompress("\100000000g", 9,
+ (void **) &decompressed, &csize, 0);
+ assert_se(r < 0);
+
+ explicit_bzero_safe(decompressed, MALLOC_SIZEOF_SAFE(decompressed));
+}
+
+_unused_ static void test_decompress_startswith(const char *compression,
+ compress_blob_t compress,
+ decompress_sw_t decompress_sw,
+ const char *data,
+ size_t data_len,
+ bool may_fail) {
+
+ char *compressed;
+ _cleanup_free_ char *compressed1 = NULL, *compressed2 = NULL, *decompressed = NULL;
+ size_t csize, len;
+ int r;
+
+ log_info("/* testing decompress_startswith with %s on %.20s text */",
+ compression, data);
+
+#define BUFSIZE_1 512
+#define BUFSIZE_2 20000
+
+ compressed = compressed1 = malloc(BUFSIZE_1);
+ assert_se(compressed1);
+ r = compress(data, data_len, compressed, BUFSIZE_1, &csize);
+ if (r == -ENOBUFS) {
+ log_info_errno(r, "compression failed: %m");
+ assert_se(may_fail);
+
+ compressed = compressed2 = malloc(BUFSIZE_2);
+ assert_se(compressed2);
+ r = compress(data, data_len, compressed, BUFSIZE_2, &csize);
+ }
+ assert_se(r >= 0);
+
+ len = strlen(data);
+
+ r = decompress_sw(compressed, csize, (void **) &decompressed, data, len, '\0');
+ assert_se(r > 0);
+ r = decompress_sw(compressed, csize, (void **) &decompressed, data, len, 'w');
+ assert_se(r == 0);
+ r = decompress_sw(compressed, csize, (void **) &decompressed, "barbarbar", 9, ' ');
+ assert_se(r == 0);
+ r = decompress_sw(compressed, csize, (void **) &decompressed, data, len - 1, data[len-1]);
+ assert_se(r > 0);
+ r = decompress_sw(compressed, csize, (void **) &decompressed, data, len - 1, 'w');
+ assert_se(r == 0);
+ r = decompress_sw(compressed, csize, (void **) &decompressed, data, len, '\0');
+ assert_se(r > 0);
+}
+
+_unused_ static void test_decompress_startswith_short(const char *compression,
+ compress_blob_t compress,
+ decompress_sw_t decompress_sw) {
+
+#define TEXT "HUGE=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+
+ char buf[1024];
+ size_t csize;
+ int r;
+
+ log_info("/* %s with %s */", __func__, compression);
+
+ r = compress(TEXT, sizeof TEXT, buf, sizeof buf, &csize);
+ assert_se(r >= 0);
+
+ for (size_t i = 1; i < strlen(TEXT); i++) {
+ _cleanup_free_ void *buf2 = NULL;
+
+ assert_se(buf2 = malloc(i));
+
+ assert_se(decompress_sw(buf, csize, &buf2, TEXT, i, TEXT[i]) == 1);
+ assert_se(decompress_sw(buf, csize, &buf2, TEXT, i, 'y') == 0);
+ }
+}
+
+_unused_ static void test_compress_stream(const char *compression,
+ const char *cat,
+ compress_stream_t compress,
+ decompress_stream_t decompress,
+ const char *srcfile) {
+
+ _cleanup_close_ int src = -EBADF, dst = -EBADF, dst2 = -EBADF;
+ _cleanup_(unlink_tempfilep) char
+ pattern[] = "/tmp/systemd-test.compressed.XXXXXX",
+ pattern2[] = "/tmp/systemd-test.compressed.XXXXXX";
+ int r;
+ _cleanup_free_ char *cmd = NULL, *cmd2 = NULL;
+ struct stat st = {};
+ uint64_t uncompressed_size;
+
+ r = find_executable(cat, NULL);
+ if (r < 0) {
+ log_error_errno(r, "Skipping %s, could not find %s binary: %m", __func__, cat);
+ return;
+ }
+
+ log_debug("/* testing %s compression */", compression);
+
+ log_debug("/* create source from %s */", srcfile);
+
+ assert_se((src = open(srcfile, O_RDONLY|O_CLOEXEC)) >= 0);
+
+ log_debug("/* test compression */");
+
+ assert_se((dst = mkostemp_safe(pattern)) >= 0);
+
+ assert_se(compress(src, dst, -1, &uncompressed_size) >= 0);
+
+ if (cat) {
+ assert_se(asprintf(&cmd, "%s %s | diff %s -", cat, pattern, srcfile) > 0);
+ assert_se(system(cmd) == 0);
+ }
+
+ log_debug("/* test decompression */");
+
+ assert_se((dst2 = mkostemp_safe(pattern2)) >= 0);
+
+ assert_se(stat(srcfile, &st) == 0);
+ assert_se((uint64_t)st.st_size == uncompressed_size);
+
+ assert_se(lseek(dst, 0, SEEK_SET) == 0);
+ r = decompress(dst, dst2, st.st_size);
+ assert_se(r == 0);
+
+ assert_se(asprintf(&cmd2, "diff %s %s", srcfile, pattern2) > 0);
+ assert_se(system(cmd2) == 0);
+
+ log_debug("/* test faulty decompression */");
+
+ assert_se(lseek(dst, 1, SEEK_SET) == 1);
+ r = decompress(dst, dst2, st.st_size);
+ assert_se(IN_SET(r, 0, -EBADMSG));
+
+ assert_se(lseek(dst, 0, SEEK_SET) == 0);
+ assert_se(lseek(dst2, 0, SEEK_SET) == 0);
+ r = decompress(dst, dst2, st.st_size - 1);
+ assert_se(r == -EFBIG);
+}
+#endif
+
+#if HAVE_LZ4
+static void test_lz4_decompress_partial(void) {
+ char buf[20000], buf2[100];
+ size_t buf_size = sizeof(buf), compressed;
+ int r;
+ _cleanup_free_ char *huge = NULL;
+
+ log_debug("/* %s */", __func__);
+
+ assert_se(huge = malloc(HUGE_SIZE));
+ memcpy(huge, "HUGE=", STRLEN("HUGE="));
+ memset(&huge[STRLEN("HUGE=")], 'x', HUGE_SIZE - STRLEN("HUGE=") - 1);
+ huge[HUGE_SIZE - 1] = '\0';
+
+ r = LZ4_compress_default(huge, buf, HUGE_SIZE, buf_size);
+ assert_se(r >= 0);
+ compressed = r;
+ log_info("Compressed %i → %zu", HUGE_SIZE, compressed);
+
+ r = LZ4_decompress_safe(buf, huge, r, HUGE_SIZE);
+ assert_se(r >= 0);
+ log_info("Decompressed → %i", r);
+
+ r = LZ4_decompress_safe_partial(buf, huge,
+ compressed,
+ 12, HUGE_SIZE);
+ assert_se(r >= 0);
+ log_info("Decompressed partial %i/%i → %i", 12, HUGE_SIZE, r);
+
+ for (size_t size = 1; size < sizeof(buf2); size++) {
+ /* This failed in older lz4s but works in newer ones. */
+ r = LZ4_decompress_safe_partial(buf, buf2, compressed, size, size);
+ log_info("Decompressed partial %zu/%zu → %i (%s)", size, size, r,
+ r < 0 ? "bad" : "good");
+ if (r >= 0 && LZ4_versionNumber() >= 10803)
+ /* lz4 <= 1.8.2 should fail that test, let's only check for newer ones */
+ assert_se(memcmp(buf2, huge, r) == 0);
+ }
+}
+#endif
+
+int main(int argc, char *argv[]) {
+#if HAVE_COMPRESSION
+ _unused_ const char text[] =
+ "text\0foofoofoofoo AAAA aaaaaaaaa ghost busters barbarbar FFF"
+ "foofoofoofoo AAAA aaaaaaaaa ghost busters barbarbar FFF";
+
+ /* The file to test compression on can be specified as the first argument */
+ const char *srcfile = argc > 1 ? argv[1] : argv[0];
+
+ char data[512] = "random\0";
+
+ _cleanup_free_ char *huge = NULL;
+
+ assert_se(huge = malloc(HUGE_SIZE));
+ memcpy(huge, "HUGE=", STRLEN("HUGE="));
+ memset(&huge[STRLEN("HUGE=")], 'x', HUGE_SIZE - STRLEN("HUGE=") - 1);
+ huge[HUGE_SIZE - 1] = '\0';
+
+ test_setup_logging(LOG_DEBUG);
+
+ random_bytes(data + 7, sizeof(data) - 7);
+
+#if HAVE_XZ
+ test_compress_decompress("XZ", compress_blob_xz, decompress_blob_xz,
+ text, sizeof(text), false);
+ test_compress_decompress("XZ", compress_blob_xz, decompress_blob_xz,
+ data, sizeof(data), true);
+
+ test_decompress_startswith("XZ",
+ compress_blob_xz, decompress_startswith_xz,
+ text, sizeof(text), false);
+ test_decompress_startswith("XZ",
+ compress_blob_xz, decompress_startswith_xz,
+ data, sizeof(data), true);
+ test_decompress_startswith("XZ",
+ compress_blob_xz, decompress_startswith_xz,
+ huge, HUGE_SIZE, true);
+
+ test_compress_stream("XZ", "xzcat",
+ compress_stream_xz, decompress_stream_xz, srcfile);
+
+ test_decompress_startswith_short("XZ", compress_blob_xz, decompress_startswith_xz);
+
+#else
+ log_info("/* XZ test skipped */");
+#endif
+
+#if HAVE_LZ4
+ test_compress_decompress("LZ4", compress_blob_lz4, decompress_blob_lz4,
+ text, sizeof(text), false);
+ test_compress_decompress("LZ4", compress_blob_lz4, decompress_blob_lz4,
+ data, sizeof(data), true);
+
+ test_decompress_startswith("LZ4",
+ compress_blob_lz4, decompress_startswith_lz4,
+ text, sizeof(text), false);
+ test_decompress_startswith("LZ4",
+ compress_blob_lz4, decompress_startswith_lz4,
+ data, sizeof(data), true);
+ test_decompress_startswith("LZ4",
+ compress_blob_lz4, decompress_startswith_lz4,
+ huge, HUGE_SIZE, true);
+
+ test_compress_stream("LZ4", "lz4cat",
+ compress_stream_lz4, decompress_stream_lz4, srcfile);
+
+ test_lz4_decompress_partial();
+
+ test_decompress_startswith_short("LZ4", compress_blob_lz4, decompress_startswith_lz4);
+
+#else
+ log_info("/* LZ4 test skipped */");
+#endif
+
+#if HAVE_ZSTD
+ test_compress_decompress("ZSTD", compress_blob_zstd, decompress_blob_zstd,
+ text, sizeof(text), false);
+ test_compress_decompress("ZSTD", compress_blob_zstd, decompress_blob_zstd,
+ data, sizeof(data), true);
+
+ test_decompress_startswith("ZSTD",
+ compress_blob_zstd, decompress_startswith_zstd,
+ text, sizeof(text), false);
+ test_decompress_startswith("ZSTD",
+ compress_blob_zstd, decompress_startswith_zstd,
+ data, sizeof(data), true);
+ test_decompress_startswith("ZSTD",
+ compress_blob_zstd, decompress_startswith_zstd,
+ huge, HUGE_SIZE, true);
+
+ test_compress_stream("ZSTD", "zstdcat",
+ compress_stream_zstd, decompress_stream_zstd, srcfile);
+
+ test_decompress_startswith_short("ZSTD", compress_blob_zstd, decompress_startswith_zstd);
+#else
+ log_info("/* ZSTD test skipped */");
+#endif
+
+ return 0;
+#else
+ return log_tests_skipped("no compression algorithm supported");
+#endif
+}
diff --git a/src/test/test-condition.c b/src/test/test-condition.c
new file mode 100644
index 0000000..bb98761
--- /dev/null
+++ b/src/test/test-condition.c
@@ -0,0 +1,1496 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/utsname.h>
+#include <unistd.h>
+
+#include "sd-id128.h"
+
+#include "alloc-util.h"
+#include "apparmor-util.h"
+#include "architecture.h"
+#include "audit-util.h"
+#include "battery-util.h"
+#include "cgroup-util.h"
+#include "condition.h"
+#include "confidential-virt.h"
+#include "cpu-set-util.h"
+#include "efi-loader.h"
+#include "env-util.h"
+#include "errno-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "hostname-util.h"
+#include "id128-util.h"
+#include "ima-util.h"
+#include "limits-util.h"
+#include "log.h"
+#include "macro.h"
+#include "nulstr-util.h"
+#include "os-util.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "psi-util.h"
+#include "rm-rf.h"
+#include "selinux-util.h"
+#include "set.h"
+#include "smack-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+#include "tomoyo-util.h"
+#include "uid-alloc-range.h"
+#include "user-util.h"
+#include "virt.h"
+
+TEST(condition_test_path) {
+ Condition *condition;
+
+ condition = condition_new(CONDITION_PATH_EXISTS, "/bin/sh", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_PATH_EXISTS, "/bin/s?", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_PATH_EXISTS_GLOB, "/bin/s?", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_PATH_EXISTS_GLOB, "/bin/s?", false, true);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_PATH_EXISTS, "/thiscertainlywontexist", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_PATH_EXISTS, "/thiscertainlywontexist", false, true);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_PATH_IS_DIRECTORY, "/bin", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_DIRECTORY_NOT_EMPTY, "/bin", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_FILE_NOT_EMPTY, "/bin/sh", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_FILE_IS_EXECUTABLE, "/bin/sh", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_FILE_IS_EXECUTABLE, "/etc/passwd", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_PATH_IS_MOUNT_POINT, "/proc", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_PATH_IS_MOUNT_POINT, "/", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_PATH_IS_MOUNT_POINT, "/bin", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_PATH_IS_READ_WRITE, "/tmp", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_PATH_IS_ENCRYPTED, "/sys", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_PATH_IS_SYMBOLIC_LINK, "/dev/stdout", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+}
+
+TEST(condition_test_control_group_hierarchy) {
+ Condition *condition;
+ int r;
+
+ r = cg_unified();
+ if (r == -ENOMEDIUM) {
+ log_tests_skipped("cgroup not mounted");
+ return;
+ }
+ assert_se(r >= 0);
+
+ condition = condition_new(CONDITION_CONTROL_GROUP_CONTROLLER, "v1", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == (r < CGROUP_UNIFIED_ALL));
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CONTROL_GROUP_CONTROLLER, "v2", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == (r >= CGROUP_UNIFIED_ALL));
+ condition_free(condition);
+}
+
+TEST(condition_test_control_group_controller) {
+ Condition *condition;
+ CGroupMask system_mask;
+ _cleanup_free_ char *controller_name = NULL;
+ int r;
+
+ r = cg_unified();
+ if (r == -ENOMEDIUM) {
+ log_tests_skipped("cgroup not mounted");
+ return;
+ }
+ assert_se(r >= 0);
+
+ /* Invalid controllers are ignored */
+ condition = condition_new(CONDITION_CONTROL_GROUP_CONTROLLER, "thisisnotarealcontroller", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CONTROL_GROUP_CONTROLLER, "thisisnotarealcontroller", false, true);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ assert_se(cg_mask_supported(&system_mask) >= 0);
+
+ /* Individual valid controllers one by one */
+ for (CGroupController controller = 0; controller < _CGROUP_CONTROLLER_MAX; controller++) {
+ const char *local_controller_name = cgroup_controller_to_string(controller);
+ log_info("chosen controller is '%s'", local_controller_name);
+ if (system_mask & CGROUP_CONTROLLER_TO_MASK(controller)) {
+ log_info("this controller is available");
+ condition = condition_new(CONDITION_CONTROL_GROUP_CONTROLLER, local_controller_name, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CONTROL_GROUP_CONTROLLER, local_controller_name, false, true);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+ } else {
+ log_info("this controller is unavailable");
+ condition = condition_new(CONDITION_CONTROL_GROUP_CONTROLLER, local_controller_name, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CONTROL_GROUP_CONTROLLER, local_controller_name, false, true);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+ }
+ }
+
+ /* Multiple valid controllers at the same time */
+ assert_se(cg_mask_to_string(system_mask, &controller_name) >= 0);
+
+ condition = condition_new(CONDITION_CONTROL_GROUP_CONTROLLER, strempty(controller_name), false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CONTROL_GROUP_CONTROLLER, strempty(controller_name), false, true);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+}
+
+TEST(condition_test_ac_power) {
+ Condition *condition;
+
+ condition = condition_new(CONDITION_AC_POWER, "true", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == on_ac_power());
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_AC_POWER, "false", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) != on_ac_power());
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_AC_POWER, "false", false, true);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == on_ac_power());
+ condition_free(condition);
+}
+
+TEST(condition_test_host) {
+ _cleanup_free_ char *hostname = NULL;
+ Condition *condition;
+ sd_id128_t id;
+ int r;
+
+ r = sd_id128_get_machine(&id);
+ if (ERRNO_IS_NEG_MACHINE_ID_UNSET(r))
+ return (void) log_tests_skipped("/etc/machine-id missing");
+ assert_se(r >= 0);
+
+ condition = condition_new(CONDITION_HOST, SD_ID128_TO_STRING(id), false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_HOST, "garbage value jjjjjjjjjjjjjj", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_HOST, SD_ID128_TO_STRING(id), false, true);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ hostname = gethostname_malloc();
+ assert_se(hostname);
+
+ /* if hostname looks like an id128 then skip testing it */
+ if (id128_is_valid(hostname))
+ log_notice("hostname is an id128, skipping test");
+ else {
+ condition = condition_new(CONDITION_HOST, hostname, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+ }
+}
+
+TEST(condition_test_architecture) {
+ Condition *condition;
+ const char *sa;
+ Architecture a;
+
+ a = uname_architecture();
+ assert_se(a >= 0);
+
+ sa = architecture_to_string(a);
+ assert_se(sa);
+
+ condition = condition_new(CONDITION_ARCHITECTURE, sa, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_ARCHITECTURE, "garbage value", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_ARCHITECTURE, sa, false, true);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+}
+
+TEST(condition_test_firmware) {
+ Condition *condition;
+
+ /* Empty parameter */
+ condition = condition_new(CONDITION_FIRMWARE, "", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ /* uefi parameter */
+ condition = condition_new(CONDITION_FIRMWARE, "uefi", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == is_efi_boot());
+ condition_free(condition);
+}
+
+TEST(condition_test_firmware_device_tree) {
+ Condition *condition;
+ bool is_device_tree_system;
+
+ /* device-tree parameter */
+ is_device_tree_system = (access("/sys/firmware/devicetree/", F_OK) == 0);
+
+ condition = condition_new(CONDITION_FIRMWARE, "device-tree", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == is_device_tree_system);
+ condition_free(condition);
+
+ /* device-tree-compatible parameter */
+ if (!is_device_tree_system) {
+ condition = condition_new(CONDITION_FIRMWARE, "device-tree-compatible()", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+ } else {
+ _cleanup_free_ char *dtcompat = NULL;
+ _cleanup_strv_free_ char **dtcompatlist = NULL;
+ size_t dtcompat_size;
+ int r;
+
+ r = read_full_virtual_file("/proc/device-tree/compatible", &dtcompat, &dtcompat_size);
+ if (r < 0) {
+ condition = condition_new(CONDITION_FIRMWARE, "device-tree-compatible()", false, false);
+ assert_se(condition);
+ if (r == -ENOENT)
+ assert_se(condition_test(condition, environ) == 0);
+ else
+ assert_se(condition_test(condition, environ) < 0);
+ condition_free(condition);
+ return;
+ }
+
+ dtcompatlist = strv_parse_nulstr(dtcompat, dtcompat_size);
+
+ STRV_FOREACH(c, dtcompatlist) {
+ _cleanup_free_ char *expression = NULL;
+
+ assert_se(expression = strjoin("device-tree-compatible(", *c, ")"));
+ condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+ }
+ }
+}
+
+TEST(condition_test_firmware_smbios) {
+ Condition *condition;
+ _cleanup_free_ char *bios_vendor = NULL, *bios_version = NULL;
+ const char *expression;
+
+ /* smbios-field parameter */
+ /* Test some malformed smbios-field arguments */
+ condition = condition_new(CONDITION_FIRMWARE, "smbios-field()", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == -EINVAL);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed)", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == -EINVAL);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == -EINVAL);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed=)", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == -EINVAL);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_FIRMWARE, "smbios-field(malformed=)", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == -EINVAL);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_FIRMWARE, "smbios-field(not_existing=nothing garbage)", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == -EINVAL);
+ condition_free(condition);
+
+ /* Test not existing SMBIOS field */
+ condition = condition_new(CONDITION_FIRMWARE, "smbios-field(not_existing=nothing)", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ /* Test with bios_vendor, if available */
+ if (read_virtual_file("/sys/class/dmi/id/bios_vendor", SIZE_MAX, &bios_vendor, NULL) <= 0)
+ return;
+
+ /* remove trailing newline */
+ strstrip(bios_vendor);
+
+ /* Check if the bios_vendor contains any spaces we should quote */
+ const char *quote = strchr(bios_vendor, ' ') ? "\"" : "";
+
+ /* Test equality / inequality using fnmatch() */
+ expression = strjoina("smbios-field(bios_vendor $= ", quote, bios_vendor, quote, ")");
+ condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ expression = strjoina("smbios-field(bios_vendor$=", quote, bios_vendor, quote, ")");
+ condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ expression = strjoina("smbios-field(bios_vendor !$= ", quote, bios_vendor, quote, ")");
+ condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ expression = strjoina("smbios-field(bios_vendor!$=", quote, bios_vendor, quote, ")");
+ condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ expression = strjoina("smbios-field(bios_vendor $= ", quote, bios_vendor, "*", quote, ")");
+ condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ /* Test version comparison with bios_version, if available */
+ if (read_virtual_file("/sys/class/dmi/id/bios_version", SIZE_MAX, &bios_version, NULL) <= 0)
+ return;
+
+ /* remove trailing newline */
+ strstrip(bios_version);
+
+ /* Check if the bios_version contains any spaces we should quote */
+ quote = strchr(bios_version, ' ') ? "\"" : "";
+
+ expression = strjoina("smbios-field(bios_version = ", quote, bios_version, quote, ")");
+ condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ expression = strjoina("smbios-field(bios_version != ", quote, bios_version, quote, ")");
+ condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ expression = strjoina("smbios-field(bios_version <= ", quote, bios_version, quote, ")");
+ condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ expression = strjoina("smbios-field(bios_version >= ", quote, bios_version, quote, ")");
+ condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ expression = strjoina("smbios-field(bios_version < ", quote, bios_version, ".1", quote, ")");
+ condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ expression = strjoina("smbios-field(bios_version > ", quote, bios_version, ".1", quote, ")");
+ condition = condition_new(CONDITION_FIRMWARE, expression, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+}
+
+TEST(condition_test_kernel_command_line) {
+ Condition *condition;
+ int r;
+
+ condition = condition_new(CONDITION_KERNEL_COMMAND_LINE, "thisreallyshouldntbeonthekernelcommandline", false, false);
+ assert_se(condition);
+ r = condition_test(condition, environ);
+ if (ERRNO_IS_PRIVILEGE(r))
+ return;
+ assert_se(r == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_KERNEL_COMMAND_LINE, "andthis=neither", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+}
+
+TEST(condition_test_kernel_version) {
+ Condition *condition;
+ struct utsname u;
+ const char *v;
+
+ condition = condition_new(CONDITION_KERNEL_VERSION, "*thisreallyshouldntbeinthekernelversion*", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_KERNEL_VERSION, "*", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ /* An artificially empty condition. It evaluates to true, but normally
+ * such condition cannot be created, because the condition list is reset instead. */
+ condition = condition_new(CONDITION_KERNEL_VERSION, "", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ assert_se(uname(&u) >= 0);
+
+ condition = condition_new(CONDITION_KERNEL_VERSION, u.release, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ strshorten(u.release, 4);
+ strcpy(strchr(u.release, 0), "*");
+
+ condition = condition_new(CONDITION_KERNEL_VERSION, u.release, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ /* 0.1.2 would be a very very very old kernel */
+ condition = condition_new(CONDITION_KERNEL_VERSION, "> 0.1.2", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_KERNEL_VERSION, ">0.1.2", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_KERNEL_VERSION, "'>0.1.2' '<9.0.0'", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_KERNEL_VERSION, "> 0.1.2 < 9.0.0", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == -EINVAL);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_KERNEL_VERSION, ">", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == -EINVAL);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_KERNEL_VERSION, ">= 0.1.2", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_KERNEL_VERSION, "< 0.1.2", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_KERNEL_VERSION, "<= 0.1.2", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_KERNEL_VERSION, "= 0.1.2", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ /* 4711.8.15 is a very very very future kernel */
+ condition = condition_new(CONDITION_KERNEL_VERSION, "< 4711.8.15", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_KERNEL_VERSION, "<= 4711.8.15", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_KERNEL_VERSION, "= 4711.8.15", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_KERNEL_VERSION, "> 4711.8.15", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_KERNEL_VERSION, " >= 4711.8.15", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ assert_se(uname(&u) >= 0);
+
+ v = strjoina(">=", u.release);
+ condition = condition_new(CONDITION_KERNEL_VERSION, v, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ v = strjoina("= ", u.release);
+ condition = condition_new(CONDITION_KERNEL_VERSION, v, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ v = strjoina("<=", u.release);
+ condition = condition_new(CONDITION_KERNEL_VERSION, v, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ v = strjoina("> ", u.release);
+ condition = condition_new(CONDITION_KERNEL_VERSION, v, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ v = strjoina("< ", u.release);
+ condition = condition_new(CONDITION_KERNEL_VERSION, v, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+}
+
+TEST(condition_test_credential) {
+ _cleanup_(rm_rf_physical_and_freep) char *n1 = NULL, *n2 = NULL;
+ _cleanup_free_ char *d1 = NULL, *d2 = NULL, *j = NULL;
+ Condition *condition;
+
+ assert_se(free_and_strdup(&d1, getenv("CREDENTIALS_DIRECTORY")) >= 0);
+ assert_se(free_and_strdup(&d2, getenv("ENCRYPTED_CREDENTIALS_DIRECTORY")) >= 0);
+
+ assert_se(unsetenv("CREDENTIALS_DIRECTORY") >= 0);
+ assert_se(unsetenv("ENCRYPTED_CREDENTIALS_DIRECTORY") >= 0);
+
+ condition = condition_new(CONDITION_CREDENTIAL, "definitelymissing", /* trigger= */ false, /* negate= */ false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ /* invalid */
+ condition = condition_new(CONDITION_CREDENTIAL, "..", /* trigger= */ false, /* negate= */ false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ assert_se(mkdtemp_malloc(NULL, &n1) >= 0);
+ assert_se(mkdtemp_malloc(NULL, &n2) >= 0);
+
+ assert_se(setenv("CREDENTIALS_DIRECTORY", n1, /* overwrite= */ true) >= 0);
+ assert_se(setenv("ENCRYPTED_CREDENTIALS_DIRECTORY", n2, /* overwrite= */ true) >= 0);
+
+ condition = condition_new(CONDITION_CREDENTIAL, "stillmissing", /* trigger= */ false, /* negate= */ false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ assert_se(j = path_join(n1, "existing"));
+ assert_se(touch(j) >= 0);
+ assert_se(j);
+ condition = condition_new(CONDITION_CREDENTIAL, "existing", /* trigger= */ false, /* negate= */ false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+ free(j);
+
+ assert_se(j = path_join(n2, "existing-encrypted"));
+ assert_se(touch(j) >= 0);
+ assert_se(j);
+ condition = condition_new(CONDITION_CREDENTIAL, "existing-encrypted", /* trigger= */ false, /* negate= */ false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ assert_se(set_unset_env("CREDENTIALS_DIRECTORY", d1, /* overwrite= */ true) >= 0);
+ assert_se(set_unset_env("ENCRYPTED_CREDENTIALS_DIRECTORY", d2, /* overwrite= */ true) >= 0);
+}
+
+#if defined(__i386__) || defined(__x86_64__)
+TEST(condition_test_cpufeature) {
+ Condition *condition;
+
+ condition = condition_new(CONDITION_CPU_FEATURE, "fpu", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CPU_FEATURE, "somecpufeaturethatreallydoesntmakesense", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CPU_FEATURE, "a", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+}
+#endif
+
+TEST(condition_test_security) {
+ Condition *condition;
+
+ condition = condition_new(CONDITION_SECURITY, "garbage oifdsjfoidsjoj", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_SECURITY, "selinux", false, true);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) != mac_selinux_use());
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_SECURITY, "apparmor", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == mac_apparmor_use());
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_SECURITY, "tomoyo", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == mac_tomoyo_use());
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_SECURITY, "ima", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == use_ima());
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_SECURITY, "smack", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == mac_smack_use());
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_SECURITY, "audit", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == use_audit());
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_SECURITY, "uefi-secureboot", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == is_efi_secure_boot());
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_SECURITY, "cvm", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) ==
+ (detect_confidential_virtualization() != CONFIDENTIAL_VIRTUALIZATION_NONE));
+ condition_free(condition);
+}
+
+TEST(print_securities) {
+ log_info("------ enabled security technologies ------");
+ log_info("SELinux: %s", yes_no(mac_selinux_use()));
+ log_info("AppArmor: %s", yes_no(mac_apparmor_use()));
+ log_info("Tomoyo: %s", yes_no(mac_tomoyo_use()));
+ log_info("IMA: %s", yes_no(use_ima()));
+ log_info("SMACK: %s", yes_no(mac_smack_use()));
+ log_info("Audit: %s", yes_no(use_audit()));
+ log_info("UEFI secure boot: %s", yes_no(is_efi_secure_boot()));
+ log_info("Confidential VM: %s", yes_no
+ (detect_confidential_virtualization() != CONFIDENTIAL_VIRTUALIZATION_NONE));
+ log_info("-------------------------------------------");
+}
+
+TEST(condition_test_virtualization) {
+ Condition *condition;
+ int r;
+
+ condition = condition_new(CONDITION_VIRTUALIZATION, "garbage oifdsjfoidsjoj", false, false);
+ assert_se(condition);
+ r = condition_test(condition, environ);
+ if (ERRNO_IS_PRIVILEGE(r))
+ return;
+ log_info("ConditionVirtualization=garbage → %i", r);
+ assert_se(r == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_VIRTUALIZATION, "container", false, false);
+ assert_se(condition);
+ r = condition_test(condition, environ);
+ log_info("ConditionVirtualization=container → %i", r);
+ assert_se(r == !!detect_container());
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_VIRTUALIZATION, "vm", false, false);
+ assert_se(condition);
+ r = condition_test(condition, environ);
+ log_info("ConditionVirtualization=vm → %i", r);
+ assert_se(r == (detect_vm() && !detect_container()));
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_VIRTUALIZATION, "private-users", false, false);
+ assert_se(condition);
+ r = condition_test(condition, environ);
+ log_info("ConditionVirtualization=private-users → %i", r);
+ assert_se(r == !!running_in_userns());
+ condition_free(condition);
+
+ NULSTR_FOREACH(virt,
+ "kvm\0"
+ "amazon\0"
+ "qemu\0"
+ "bochs\0"
+ "xen\0"
+ "uml\0"
+ "vmware\0"
+ "oracle\0"
+ "microsoft\0"
+ "zvm\0"
+ "parallels\0"
+ "bhyve\0"
+ "vm_other\0") {
+
+ condition = condition_new(CONDITION_VIRTUALIZATION, virt, false, false);
+ assert_se(condition);
+ r = condition_test(condition, environ);
+ log_info("ConditionVirtualization=%s → %i", virt, r);
+ assert_se(r >= 0);
+ condition_free(condition);
+ }
+}
+
+TEST(condition_test_user) {
+ Condition *condition;
+ char* uid;
+ char* username;
+ int r;
+
+ condition = condition_new(CONDITION_USER, "garbage oifdsjfoidsjoj", false, false);
+ assert_se(condition);
+ r = condition_test(condition, environ);
+ log_info("ConditionUser=garbage → %i", r);
+ assert_se(r == 0);
+ condition_free(condition);
+
+ assert_se(asprintf(&uid, "%"PRIu32, UINT32_C(0xFFFF)) > 0);
+ condition = condition_new(CONDITION_USER, uid, false, false);
+ assert_se(condition);
+ r = condition_test(condition, environ);
+ log_info("ConditionUser=%s → %i", uid, r);
+ assert_se(r == 0);
+ condition_free(condition);
+ free(uid);
+
+ assert_se(asprintf(&uid, "%u", (unsigned)getuid()) > 0);
+ condition = condition_new(CONDITION_USER, uid, false, false);
+ assert_se(condition);
+ r = condition_test(condition, environ);
+ log_info("ConditionUser=%s → %i", uid, r);
+ assert_se(r > 0);
+ condition_free(condition);
+ free(uid);
+
+ assert_se(asprintf(&uid, "%u", (unsigned)getuid()+1) > 0);
+ condition = condition_new(CONDITION_USER, uid, false, false);
+ assert_se(condition);
+ r = condition_test(condition, environ);
+ log_info("ConditionUser=%s → %i", uid, r);
+ assert_se(r == 0);
+ condition_free(condition);
+ free(uid);
+
+ username = getusername_malloc();
+ assert_se(username);
+ condition = condition_new(CONDITION_USER, username, false, false);
+ assert_se(condition);
+ r = condition_test(condition, environ);
+ log_info("ConditionUser=%s → %i", username, r);
+ assert_se(r > 0);
+ condition_free(condition);
+ free(username);
+
+ username = (char*)(geteuid() == 0 ? NOBODY_USER_NAME : "root");
+ condition = condition_new(CONDITION_USER, username, false, false);
+ assert_se(condition);
+ r = condition_test(condition, environ);
+ log_info("ConditionUser=%s → %i", username, r);
+ assert_se(r == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_USER, "@system", false, false);
+ assert_se(condition);
+ r = condition_test(condition, environ);
+ log_info("ConditionUser=@system → %i", r);
+ if (uid_is_system(getuid()) || uid_is_system(geteuid()))
+ assert_se(r > 0);
+ else
+ assert_se(r == 0);
+ condition_free(condition);
+}
+
+TEST(condition_test_group) {
+ Condition *condition;
+ char* gid;
+ char* groupname;
+ gid_t *gids, max_gid;
+ int ngroups_max, ngroups, r, i;
+
+ assert_se(0 < asprintf(&gid, "%u", UINT32_C(0xFFFF)));
+ condition = condition_new(CONDITION_GROUP, gid, false, false);
+ assert_se(condition);
+ r = condition_test(condition, environ);
+ log_info("ConditionGroup=%s → %i", gid, r);
+ assert_se(r == 0);
+ condition_free(condition);
+ free(gid);
+
+ assert_se(0 < asprintf(&gid, "%u", getgid()));
+ condition = condition_new(CONDITION_GROUP, gid, false, false);
+ assert_se(condition);
+ r = condition_test(condition, environ);
+ log_info("ConditionGroup=%s → %i", gid, r);
+ assert_se(r > 0);
+ condition_free(condition);
+ free(gid);
+
+ ngroups_max = sysconf(_SC_NGROUPS_MAX);
+ assert_se(ngroups_max > 0);
+
+ gids = newa(gid_t, ngroups_max);
+
+ ngroups = getgroups(ngroups_max, gids);
+ assert_se(ngroups >= 0);
+
+ max_gid = getgid();
+ for (i = 0; i < ngroups; i++) {
+ _cleanup_free_ char *name = NULL;
+
+ assert_se(0 < asprintf(&gid, "%u", gids[i]));
+ condition = condition_new(CONDITION_GROUP, gid, false, false);
+ assert_se(condition);
+ r = condition_test(condition, environ);
+ log_info("ConditionGroup=%s → %i", gid, r);
+ assert_se(r > 0);
+ condition_free(condition);
+ free(gid);
+ max_gid = gids[i] > max_gid ? gids[i] : max_gid;
+
+ name = gid_to_name(gids[i]);
+ assert_se(name);
+ if (STR_IN_SET(name, "sbuild", "buildd"))
+ return; /* Debian package build in chroot, groupnames won't match, skip */
+ condition = condition_new(CONDITION_GROUP, name, false, false);
+ assert_se(condition);
+ r = condition_test(condition, environ);
+ log_info("ConditionGroup=%s → %i", name, r);
+ assert_se(r > 0);
+ condition_free(condition);
+ max_gid = gids[i] > max_gid ? gids[i] : max_gid;
+ }
+
+ assert_se(0 < asprintf(&gid, "%u", max_gid+1));
+ condition = condition_new(CONDITION_GROUP, gid, false, false);
+ assert_se(condition);
+ r = condition_test(condition, environ);
+ log_info("ConditionGroup=%s → %i", gid, r);
+ assert_se(r == 0);
+ condition_free(condition);
+ free(gid);
+
+ groupname = (char*)(getegid() == 0 ? NOBODY_GROUP_NAME : "root");
+ condition = condition_new(CONDITION_GROUP, groupname, false, false);
+ assert_se(condition);
+ r = condition_test(condition, environ);
+ log_info("ConditionGroup=%s → %i", groupname, r);
+ assert_se(r == 0);
+ condition_free(condition);
+}
+
+static void test_condition_test_cpus_one(const char *s, bool result) {
+ Condition *condition;
+ int r;
+
+ log_debug("%s=%s", condition_type_to_string(CONDITION_CPUS), s);
+
+ condition = condition_new(CONDITION_CPUS, s, false, false);
+ assert_se(condition);
+
+ r = condition_test(condition, environ);
+ assert_se(r >= 0);
+ assert_se(r == result);
+ condition_free(condition);
+}
+
+TEST(condition_test_cpus) {
+ _cleanup_free_ char *t = NULL;
+ int cpus;
+
+ cpus = cpus_in_affinity_mask();
+ assert_se(cpus >= 0);
+
+ test_condition_test_cpus_one("> 0", true);
+ test_condition_test_cpus_one(">= 0", true);
+ test_condition_test_cpus_one("!= 0", true);
+ test_condition_test_cpus_one("<= 0", false);
+ test_condition_test_cpus_one("< 0", false);
+ test_condition_test_cpus_one("= 0", false);
+
+ test_condition_test_cpus_one("> 100000", false);
+ test_condition_test_cpus_one("= 100000", false);
+ test_condition_test_cpus_one(">= 100000", false);
+ test_condition_test_cpus_one("< 100000", true);
+ test_condition_test_cpus_one("!= 100000", true);
+ test_condition_test_cpus_one("<= 100000", true);
+
+ assert_se(asprintf(&t, "= %i", cpus) >= 0);
+ test_condition_test_cpus_one(t, true);
+ t = mfree(t);
+
+ assert_se(asprintf(&t, "<= %i", cpus) >= 0);
+ test_condition_test_cpus_one(t, true);
+ t = mfree(t);
+
+ assert_se(asprintf(&t, ">= %i", cpus) >= 0);
+ test_condition_test_cpus_one(t, true);
+ t = mfree(t);
+
+ assert_se(asprintf(&t, "!= %i", cpus) >= 0);
+ test_condition_test_cpus_one(t, false);
+ t = mfree(t);
+
+ assert_se(asprintf(&t, "< %i", cpus) >= 0);
+ test_condition_test_cpus_one(t, false);
+ t = mfree(t);
+
+ assert_se(asprintf(&t, "> %i", cpus) >= 0);
+ test_condition_test_cpus_one(t, false);
+ t = mfree(t);
+}
+
+static void test_condition_test_memory_one(const char *s, bool result) {
+ Condition *condition;
+ int r;
+
+ log_debug("%s=%s", condition_type_to_string(CONDITION_MEMORY), s);
+
+ condition = condition_new(CONDITION_MEMORY, s, false, false);
+ assert_se(condition);
+
+ r = condition_test(condition, environ);
+ assert_se(r >= 0);
+ assert_se(r == result);
+ condition_free(condition);
+}
+
+TEST(condition_test_memory) {
+ _cleanup_free_ char *t = NULL;
+ uint64_t memory;
+
+ memory = physical_memory();
+
+ test_condition_test_memory_one("> 0", true);
+ test_condition_test_memory_one(">= 0", true);
+ test_condition_test_memory_one("!= 0", true);
+ test_condition_test_memory_one("<= 0", false);
+ test_condition_test_memory_one("< 0", false);
+ test_condition_test_memory_one("= 0", false);
+
+ test_condition_test_memory_one("> 18446744073709547520", false);
+ test_condition_test_memory_one("= 18446744073709547520", false);
+ test_condition_test_memory_one(">= 18446744073709547520", false);
+ test_condition_test_memory_one("< 18446744073709547520", true);
+ test_condition_test_memory_one("!= 18446744073709547520", true);
+ test_condition_test_memory_one("<= 18446744073709547520", true);
+
+ test_condition_test_memory_one("> 100T", false);
+ test_condition_test_memory_one("= 100T", false);
+ test_condition_test_memory_one(">= 100T", false);
+ test_condition_test_memory_one("< 100T", true);
+ test_condition_test_memory_one("!= 100T", true);
+ test_condition_test_memory_one("<= 100T", true);
+
+ test_condition_test_memory_one("> 100 T", false);
+ test_condition_test_memory_one("= 100 T", false);
+ test_condition_test_memory_one(">= 100 T", false);
+ test_condition_test_memory_one("< 100 T", true);
+ test_condition_test_memory_one("!= 100 T", true);
+ test_condition_test_memory_one("<= 100 T", true);
+
+ test_condition_test_memory_one("> 100 T 1 G", false);
+ test_condition_test_memory_one("= 100 T 1 G", false);
+ test_condition_test_memory_one(">= 100 T 1 G", false);
+ test_condition_test_memory_one("< 100 T 1 G", true);
+ test_condition_test_memory_one("!= 100 T 1 G", true);
+ test_condition_test_memory_one("<= 100 T 1 G", true);
+
+ assert_se(asprintf(&t, "= %" PRIu64, memory) >= 0);
+ test_condition_test_memory_one(t, true);
+ t = mfree(t);
+
+ assert_se(asprintf(&t, "<= %" PRIu64, memory) >= 0);
+ test_condition_test_memory_one(t, true);
+ t = mfree(t);
+
+ assert_se(asprintf(&t, ">= %" PRIu64, memory) >= 0);
+ test_condition_test_memory_one(t, true);
+ t = mfree(t);
+
+ assert_se(asprintf(&t, "!= %" PRIu64, memory) >= 0);
+ test_condition_test_memory_one(t, false);
+ t = mfree(t);
+
+ assert_se(asprintf(&t, "< %" PRIu64, memory) >= 0);
+ test_condition_test_memory_one(t, false);
+ t = mfree(t);
+
+ assert_se(asprintf(&t, "> %" PRIu64, memory) >= 0);
+ test_condition_test_memory_one(t, false);
+ t = mfree(t);
+}
+
+static void test_condition_test_environment_one(const char *s, bool result) {
+ Condition *condition;
+ int r;
+
+ log_debug("%s=%s", condition_type_to_string(CONDITION_ENVIRONMENT), s);
+
+ condition = condition_new(CONDITION_ENVIRONMENT, s, false, false);
+ assert_se(condition);
+
+ r = condition_test(condition, environ);
+ assert_se(r >= 0);
+ assert_se(r == result);
+ condition_free(condition);
+}
+
+TEST(condition_test_environment) {
+ assert_se(setenv("EXISTINGENVVAR", "foo", false) >= 0);
+
+ test_condition_test_environment_one("MISSINGENVVAR", false);
+ test_condition_test_environment_one("MISSINGENVVAR=foo", false);
+ test_condition_test_environment_one("MISSINGENVVAR=", false);
+
+ test_condition_test_environment_one("EXISTINGENVVAR", true);
+ test_condition_test_environment_one("EXISTINGENVVAR=foo", true);
+ test_condition_test_environment_one("EXISTINGENVVAR=bar", false);
+ test_condition_test_environment_one("EXISTINGENVVAR=", false);
+}
+
+TEST(condition_test_os_release) {
+ _cleanup_strv_free_ char **os_release_pairs = NULL;
+ _cleanup_free_ char *version_id = NULL;
+ const char *key_value_pair;
+ Condition *condition;
+
+ /* Should not happen, but it's a test so we don't know the environment. */
+ if (load_os_release_pairs(NULL, &os_release_pairs) < 0)
+ return;
+ if (strv_length(os_release_pairs) < 2)
+ return;
+
+ condition = condition_new(CONDITION_OS_RELEASE, "_THISHOPEFULLYWONTEXIST=01234 56789", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_OS_RELEASE, "WRONG FORMAT", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == -EINVAL);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_OS_RELEASE, "WRONG!<>=FORMAT", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == -EINVAL);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_OS_RELEASE, "WRONG FORMAT=", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == -EINVAL);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_OS_RELEASE, "WRONG =FORMAT", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == -EINVAL);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_OS_RELEASE, "WRONG = FORMAT", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == -EINVAL);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_OS_RELEASE, "WRONGFORMAT= ", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == -EINVAL);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_OS_RELEASE, "WRO NG=FORMAT", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == -EINVAL);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_OS_RELEASE, "", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ /* load_os_release_pairs() removes quotes, we have to add them back,
+ * otherwise we get a string: "PRETTY_NAME=Debian GNU/Linux 10 (buster)"
+ * which is wrong, as the value is not quoted anymore. */
+ const char *quote = strchr(os_release_pairs[1], ' ') ? "\"" : "";
+ key_value_pair = strjoina(os_release_pairs[0], "=", quote, os_release_pairs[1], quote);
+ condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ key_value_pair = strjoina(os_release_pairs[0], "!=", quote, os_release_pairs[1], quote);
+ condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ /* Test fnmatch() operators */
+ key_value_pair = strjoina(os_release_pairs[0], "$=", quote, os_release_pairs[1], quote);
+ condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ key_value_pair = strjoina(os_release_pairs[0], "!$=", quote, os_release_pairs[1], quote);
+ condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ /* Some distros (eg: Arch) do not set VERSION_ID */
+ if (parse_os_release(NULL, "VERSION_ID", &version_id) <= 0)
+ return;
+
+ key_value_pair = strjoina("VERSION_ID", "=", version_id);
+ condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ key_value_pair = strjoina("VERSION_ID", "!=", version_id);
+ condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ key_value_pair = strjoina("VERSION_ID", "<=", version_id);
+ condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ key_value_pair = strjoina("VERSION_ID", ">=", version_id);
+ condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ key_value_pair = strjoina("VERSION_ID", "<", version_id, ".1");
+ condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ key_value_pair = strjoina("VERSION_ID", ">", version_id, ".1");
+ condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ key_value_pair = strjoina("VERSION_ID", "=", version_id, " ", os_release_pairs[0], "=", quote, os_release_pairs[1], quote);
+ condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ key_value_pair = strjoina("VERSION_ID", "!=", version_id, " ", os_release_pairs[0], "=", quote, os_release_pairs[1], quote);
+ condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ key_value_pair = strjoina("VERSION_ID", "=", version_id, " ", os_release_pairs[0], "!=", quote, os_release_pairs[1], quote);
+ condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ key_value_pair = strjoina("VERSION_ID", "!=", version_id, " ", os_release_pairs[0], "!=", quote, os_release_pairs[1], quote);
+ condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) == 0);
+ condition_free(condition);
+
+ key_value_pair = strjoina("VERSION_ID", "<", version_id, ".1", " ", os_release_pairs[0], "=", quote, os_release_pairs[1], quote);
+ condition = condition_new(CONDITION_OS_RELEASE, key_value_pair, false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+}
+
+TEST(condition_test_psi) {
+ Condition *condition;
+ CGroupMask mask;
+ int r;
+
+ if (!is_pressure_supported())
+ return (void) log_notice("Pressure Stall Information (PSI) is not supported, skipping %s", __func__);
+
+ condition = condition_new(CONDITION_MEMORY_PRESSURE, "", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) < 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CPU_PRESSURE, "sbarabau", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) < 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_MEMORY_PRESSURE, "10%sbarabau", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) < 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CPU_PRESSURE, "10% sbarabau", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) < 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CPU_PRESSURE, "-10", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) < 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CPU_PRESSURE, "10%/10min", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) < 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CPU_PRESSURE, "10min/10%", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) < 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CPU_PRESSURE, "10% 5min", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) < 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CPU_PRESSURE, "/5min", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) < 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_IO_PRESSURE, "10s / ", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) < 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_MEMORY_PRESSURE, "100%", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) >= 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_MEMORY_PRESSURE, "0%", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) >= 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_MEMORY_PRESSURE, "0.0%", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) >= 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CPU_PRESSURE, "100%", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) >= 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CPU_PRESSURE, "0%", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) >= 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CPU_PRESSURE, "0.0%", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) >= 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CPU_PRESSURE, "0.01%", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) >= 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CPU_PRESSURE, "0.0%/10sec", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) >= 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CPU_PRESSURE, "100.0% / 1min", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) >= 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_IO_PRESSURE, "50.0% / 1min", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) >= 0);
+ condition_free(condition);
+
+ r = cg_all_unified();
+ if (r < 0)
+ return (void) log_notice("Failed to determine whether the unified cgroups hierarchy is used, skipping %s", __func__);
+ if (r == 0)
+ return (void) log_notice("Requires the unified cgroups hierarchy, skipping %s", __func__);
+
+ if (cg_mask_supported(&mask) < 0)
+ return (void) log_notice("Failed to get supported cgroup controllers, skipping %s", __func__);
+
+ if (!FLAGS_SET(mask, CGROUP_MASK_MEMORY))
+ return (void) log_notice("Requires the cgroup memory controller, skipping %s", __func__);
+
+ if (!FLAGS_SET(mask, CGROUP_MASK_CPU))
+ return (void) log_notice("Requires the cgroup CPU controller, skipping %s", __func__);
+
+ condition = condition_new(CONDITION_MEMORY_PRESSURE, " : / ", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) < 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CPU_PRESSURE, "hopefullythisisnotarealone.slice:100% / 10sec", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) > 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_CPU_PRESSURE, "-.slice:100.0% / 1min", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) >= 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_MEMORY_PRESSURE, "-.slice:0.0%/5min", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) >= 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_MEMORY_PRESSURE, "-.slice:100.0%", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) >= 0);
+ condition_free(condition);
+
+ condition = condition_new(CONDITION_IO_PRESSURE, "-.slice:0.0%", false, false);
+ assert_se(condition);
+ assert_se(condition_test(condition, environ) >= 0);
+ condition_free(condition);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-conf-files.c b/src/test/test-conf-files.c
new file mode 100644
index 0000000..4253490
--- /dev/null
+++ b/src/test/test-conf-files.c
@@ -0,0 +1,218 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/***
+ Copyright © 2014 Michael Marineau
+***/
+
+#include <stdarg.h>
+#include <stdio.h>
+
+#include "alloc-util.h"
+#include "conf-files.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "macro.h"
+#include "mkdir.h"
+#include "path-util.h"
+#include "rm-rf.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+TEST(conf_files_list) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ _cleanup_close_ int tfd = -EBADF;
+ _cleanup_strv_free_ char **result = NULL;
+ const char *search1, *search2, *search1_a, *search1_b, *search1_c, *search2_aa;
+
+ tfd = mkdtemp_open("/tmp/test-conf-files-XXXXXX", O_PATH, &t);
+ assert(tfd >= 0);
+
+ assert_se(mkdirat(tfd, "dir1", 0755) >= 0);
+ assert_se(mkdirat(tfd, "dir2", 0755) >= 0);
+
+ search1 = strjoina(t, "/dir1/");
+ search2 = strjoina(t, "/dir2/");
+
+ FOREACH_STRING(p, "a.conf", "b.conf", "c.foo") {
+ _cleanup_free_ char *path = NULL;
+
+ assert_se(path = path_join(search1, p));
+ assert_se(write_string_file(path, "foobar", WRITE_STRING_FILE_CREATE) >= 0);
+ }
+
+ assert_se(symlinkat("/dev/null", tfd, "dir1/m.conf") >= 0);
+
+ FOREACH_STRING(p, "a.conf", "aa.conf", "m.conf") {
+ _cleanup_free_ char *path = NULL;
+
+ assert_se(path = path_join(search2, p));
+ assert_se(write_string_file(path, "hogehoge", WRITE_STRING_FILE_CREATE) >= 0);
+ }
+
+ search1_a = strjoina(search1, "a.conf");
+ search1_b = strjoina(search1, "b.conf");
+ search1_c = strjoina(search1, "c.foo");
+ search2_aa = strjoina(search2, "aa.conf");
+
+ /* search dir1 without suffix */
+ assert_se(conf_files_list(&result, NULL, NULL, CONF_FILES_FILTER_MASKED, search1) >= 0);
+ strv_print(result);
+ assert_se(strv_equal(result, STRV_MAKE(search1_a, search1_b, search1_c)));
+
+ result = strv_free(result);
+
+ assert_se(conf_files_list(&result, NULL, t, CONF_FILES_FILTER_MASKED, "/dir1/") >= 0);
+ strv_print(result);
+ assert_se(strv_equal(result, STRV_MAKE(search1_a, search1_b, search1_c)));
+
+ result = strv_free(result);
+
+ assert_se(conf_files_list_at(&result, NULL, AT_FDCWD, CONF_FILES_FILTER_MASKED, search1) >= 0);
+ strv_print(result);
+ assert_se(strv_equal(result, STRV_MAKE(search1_a, search1_b, search1_c)));
+
+ result = strv_free(result);
+
+ assert_se(conf_files_list_at(&result, NULL, tfd, CONF_FILES_FILTER_MASKED, "/dir1/") >= 0);
+ strv_print(result);
+ assert_se(strv_equal(result, STRV_MAKE("dir1/a.conf", "dir1/b.conf", "dir1/c.foo")));
+
+ result = strv_free(result);
+
+ /* search dir1 with suffix */
+ assert_se(conf_files_list(&result, ".conf", NULL, CONF_FILES_FILTER_MASKED, search1) >= 0);
+ strv_print(result);
+ assert_se(strv_equal(result, STRV_MAKE(search1_a, search1_b)));
+
+ result = strv_free(result);
+
+ assert_se(conf_files_list(&result, ".conf", t, CONF_FILES_FILTER_MASKED, "/dir1/") >= 0);
+ strv_print(result);
+ assert_se(strv_equal(result, STRV_MAKE(search1_a, search1_b)));
+
+ result = strv_free(result);
+
+ assert_se(conf_files_list_at(&result, ".conf", AT_FDCWD, CONF_FILES_FILTER_MASKED, search1) >= 0);
+ strv_print(result);
+ assert_se(strv_equal(result, STRV_MAKE(search1_a, search1_b)));
+
+ result = strv_free(result);
+
+ assert_se(conf_files_list_at(&result, ".conf", tfd, CONF_FILES_FILTER_MASKED, "/dir1/") >= 0);
+ strv_print(result);
+ assert_se(strv_equal(result, STRV_MAKE("dir1/a.conf", "dir1/b.conf")));
+
+ result = strv_free(result);
+
+ /* search two dirs */
+ assert_se(conf_files_list_strv(&result, ".conf", NULL, CONF_FILES_FILTER_MASKED, STRV_MAKE_CONST(search1, search2)) >= 0);
+ strv_print(result);
+ assert_se(strv_equal(result, STRV_MAKE(search1_a, search2_aa, search1_b)));
+
+ result = strv_free(result);
+
+ assert_se(conf_files_list_strv(&result, ".conf", t, CONF_FILES_FILTER_MASKED, STRV_MAKE_CONST("/dir1/", "/dir2/")) >= 0);
+ strv_print(result);
+ assert_se(strv_equal(result, STRV_MAKE(search1_a, search2_aa, search1_b)));
+
+ result = strv_free(result);
+
+ assert_se(conf_files_list_strv_at(&result, ".conf", AT_FDCWD, CONF_FILES_FILTER_MASKED, STRV_MAKE_CONST(search1, search2)) >= 0);
+ strv_print(result);
+ assert_se(strv_equal(result, STRV_MAKE(search1_a, search2_aa, search1_b)));
+
+ result = strv_free(result);
+
+ assert_se(conf_files_list_strv_at(&result, ".conf", tfd, CONF_FILES_FILTER_MASKED, STRV_MAKE_CONST("/dir1/", "/dir2/")) >= 0);
+ strv_print(result);
+ assert_se(strv_equal(result, STRV_MAKE("dir1/a.conf", "dir2/aa.conf", "dir1/b.conf")));
+
+ result = strv_free(result);
+
+ /* filename only */
+ assert_se(conf_files_list_strv(&result, ".conf", NULL, CONF_FILES_FILTER_MASKED | CONF_FILES_BASENAME, STRV_MAKE_CONST(search1, search2)) >= 0);
+ strv_print(result);
+ assert_se(strv_equal(result, STRV_MAKE("a.conf", "aa.conf", "b.conf")));
+
+ result = strv_free(result);
+
+ assert_se(conf_files_list_strv(&result, ".conf", t, CONF_FILES_FILTER_MASKED | CONF_FILES_BASENAME, STRV_MAKE_CONST("/dir1/", "/dir2/")) >= 0);
+ strv_print(result);
+ assert_se(strv_equal(result, STRV_MAKE("a.conf", "aa.conf", "b.conf")));
+
+ result = strv_free(result);
+
+ assert_se(conf_files_list_strv_at(&result, ".conf", AT_FDCWD, CONF_FILES_FILTER_MASKED | CONF_FILES_BASENAME, STRV_MAKE_CONST(search1, search2)) >= 0);
+ strv_print(result);
+ assert_se(strv_equal(result, STRV_MAKE("a.conf", "aa.conf", "b.conf")));
+
+ result = strv_free(result);
+
+ assert_se(conf_files_list_strv_at(&result, ".conf", tfd, CONF_FILES_FILTER_MASKED | CONF_FILES_BASENAME, STRV_MAKE_CONST("/dir1/", "/dir2/")) >= 0);
+ strv_print(result);
+ assert_se(strv_equal(result, STRV_MAKE("a.conf", "aa.conf", "b.conf")));
+}
+
+static void test_conf_files_insert_one(const char *root) {
+ _cleanup_strv_free_ char **s = NULL;
+
+ log_info("/* %s root=%s */", __func__, strempty(root));
+
+ char **dirs = STRV_MAKE("/dir1", "/dir2", "/dir3");
+
+ _cleanup_free_ const char
+ *foo1 = path_join(root, "/dir1/foo.conf"),
+ *foo2 = path_join(root, "/dir2/foo.conf"),
+ *bar2 = path_join(root, "/dir2/bar.conf"),
+ *zzz3 = path_join(root, "/dir3/zzz.conf"),
+ *whatever = path_join(root, "/whatever.conf");
+
+ assert_se(conf_files_insert(&s, root, dirs, "/dir2/foo.conf") == 0);
+ assert_se(strv_equal(s, STRV_MAKE(foo2)));
+
+ /* The same file again, https://github.com/systemd/systemd/issues/11124 */
+ assert_se(conf_files_insert(&s, root, dirs, "/dir2/foo.conf") == 0);
+ assert_se(strv_equal(s, STRV_MAKE(foo2)));
+
+ /* Lower priority → new entry is ignored */
+ assert_se(conf_files_insert(&s, root, dirs, "/dir3/foo.conf") == 0);
+ assert_se(strv_equal(s, STRV_MAKE(foo2)));
+
+ /* Higher priority → new entry replaces */
+ assert_se(conf_files_insert(&s, root, dirs, "/dir1/foo.conf") == 0);
+ assert_se(strv_equal(s, STRV_MAKE(foo1)));
+
+ /* Earlier basename */
+ assert_se(conf_files_insert(&s, root, dirs, "/dir2/bar.conf") == 0);
+ assert_se(strv_equal(s, STRV_MAKE(bar2, foo1)));
+
+ /* Later basename */
+ assert_se(conf_files_insert(&s, root, dirs, "/dir3/zzz.conf") == 0);
+ assert_se(strv_equal(s, STRV_MAKE(bar2, foo1, zzz3)));
+
+ /* All lower priority → all ignored */
+ assert_se(conf_files_insert(&s, root, dirs, "/dir3/zzz.conf") == 0);
+ assert_se(conf_files_insert(&s, root, dirs, "/dir2/bar.conf") == 0);
+ assert_se(conf_files_insert(&s, root, dirs, "/dir3/bar.conf") == 0);
+ assert_se(conf_files_insert(&s, root, dirs, "/dir2/foo.conf") == 0);
+ assert_se(strv_equal(s, STRV_MAKE(bar2, foo1, zzz3)));
+
+ /* Two entries that don't match any of the directories, but match basename */
+ assert_se(conf_files_insert(&s, root, dirs, "/dir4/zzz.conf") == 0);
+ assert_se(conf_files_insert(&s, root, dirs, "/zzz.conf") == 0);
+ assert_se(strv_equal(s, STRV_MAKE(bar2, foo1, zzz3)));
+
+ /* An entry that doesn't match any of the directories, no match at all */
+ assert_se(conf_files_insert(&s, root, dirs, "/whatever.conf") == 0);
+ assert_se(strv_equal(s, STRV_MAKE(bar2, foo1, whatever, zzz3)));
+}
+
+TEST(conf_files_insert) {
+ test_conf_files_insert_one(NULL);
+ test_conf_files_insert_one("/root");
+ test_conf_files_insert_one("/root/");
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-conf-parser.c b/src/test/test-conf-parser.c
new file mode 100644
index 0000000..0acb413
--- /dev/null
+++ b/src/test/test-conf-parser.c
@@ -0,0 +1,393 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "conf-parser.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "log.h"
+#include "macro.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+static void test_config_parse_path_one(const char *rvalue, const char *expected) {
+ _cleanup_free_ char *path = NULL;
+
+ assert_se(config_parse_path("unit", "filename", 1, "section", 1, "lvalue", 0, rvalue, &path, NULL) >= 0);
+ assert_se(streq_ptr(expected, path));
+}
+
+static void test_config_parse_log_level_one(const char *rvalue, int expected) {
+ int log_level = 0;
+
+ assert_se(config_parse_log_level("unit", "filename", 1, "section", 1, "lvalue", 0, rvalue, &log_level, NULL) >= 0);
+ assert_se(expected == log_level);
+}
+
+static void test_config_parse_log_facility_one(const char *rvalue, int expected) {
+ int log_facility = 0;
+
+ assert_se(config_parse_log_facility("unit", "filename", 1, "section", 1, "lvalue", 0, rvalue, &log_facility, NULL) >= 0);
+ assert_se(expected == log_facility);
+}
+
+static void test_config_parse_iec_size_one(const char *rvalue, size_t expected) {
+ size_t iec_size = 0;
+
+ assert_se(config_parse_iec_size("unit", "filename", 1, "section", 1, "lvalue", 0, rvalue, &iec_size, NULL) >= 0);
+ assert_se(expected == iec_size);
+}
+
+static void test_config_parse_si_uint64_one(const char *rvalue, uint64_t expected) {
+ uint64_t si_uint64 = 0;
+
+ assert_se(config_parse_si_uint64("unit", "filename", 1, "section", 1, "lvalue", 0, rvalue, &si_uint64, NULL) >= 0);
+ assert_se(expected == si_uint64);
+}
+
+static void test_config_parse_int_one(const char *rvalue, int expected) {
+ int v = -1;
+
+ assert_se(config_parse_int("unit", "filename", 1, "section", 1, "lvalue", 0, rvalue, &v, NULL) >= 0);
+ assert_se(expected == v);
+}
+
+static void test_config_parse_unsigned_one(const char *rvalue, unsigned expected) {
+ unsigned v = 0;
+
+ assert_se(config_parse_unsigned("unit", "filename", 1, "section", 1, "lvalue", 0, rvalue, &v, NULL) >= 0);
+ assert_se(expected == v);
+}
+
+static void test_config_parse_strv_one(const char *rvalue, char **expected) {
+ _cleanup_strv_free_ char **strv = NULL;
+
+ assert_se(config_parse_strv("unit", "filename", 1, "section", 1, "lvalue", 0, rvalue, &strv, NULL) >= 0);
+ assert_se(strv_equal(expected, strv));
+}
+
+static void test_config_parse_mode_one(const char *rvalue, mode_t expected) {
+ mode_t v = 0;
+
+ assert_se(config_parse_mode("unit", "filename", 1, "section", 1, "lvalue", 0, rvalue, &v, NULL) >= 0);
+ assert_se(expected == v);
+}
+
+static void test_config_parse_sec_one(const char *rvalue, usec_t expected) {
+ usec_t v = 0;
+
+ assert_se(config_parse_sec("unit", "filename", 1, "section", 1, "lvalue", 0, rvalue, &v, NULL) >= 0);
+ assert_se(expected == v);
+}
+
+static void test_config_parse_nsec_one(const char *rvalue, nsec_t expected) {
+ nsec_t v = 0;
+
+ assert_se(config_parse_nsec("unit", "filename", 1, "nsection", 1, "lvalue", 0, rvalue, &v, NULL) >= 0);
+ assert_se(expected == v);
+}
+
+TEST(config_parse_path) {
+ test_config_parse_path_one("/path", "/path");
+ test_config_parse_path_one("/path//////////", "/path");
+ test_config_parse_path_one("///path/foo///bar////bar//", "/path/foo/bar/bar");
+ test_config_parse_path_one("/path//./////hogehoge///.", "/path/hogehoge");
+ test_config_parse_path_one("/path/\xc3\x80", "/path/\xc3\x80");
+
+ test_config_parse_path_one("not_absolute/path", NULL);
+ test_config_parse_path_one("/path/\xc3\x7f", NULL);
+}
+
+TEST(config_parse_log_level) {
+ test_config_parse_log_level_one("debug", LOG_DEBUG);
+ test_config_parse_log_level_one("info", LOG_INFO);
+
+ test_config_parse_log_level_one("garbage", 0);
+}
+
+TEST(config_parse_log_facility) {
+ test_config_parse_log_facility_one("mail", LOG_MAIL);
+ test_config_parse_log_facility_one("user", LOG_USER);
+
+ test_config_parse_log_facility_one("garbage", 0);
+}
+
+TEST(config_parse_iec_size) {
+ test_config_parse_iec_size_one("1024", 1024);
+ test_config_parse_iec_size_one("2K", 2048);
+ test_config_parse_iec_size_one("10M", 10 * 1024 * 1024);
+ test_config_parse_iec_size_one("1G", 1 * 1024 * 1024 * 1024);
+ test_config_parse_iec_size_one("0G", 0);
+ test_config_parse_iec_size_one("0", 0);
+
+ test_config_parse_iec_size_one("-982", 0);
+ test_config_parse_iec_size_one("49874444198739873000000G", 0);
+ test_config_parse_iec_size_one("garbage", 0);
+}
+
+TEST(config_parse_si_uint64) {
+ test_config_parse_si_uint64_one("1024", 1024);
+ test_config_parse_si_uint64_one("2K", 2000);
+ test_config_parse_si_uint64_one("10M", 10 * 1000 * 1000);
+ test_config_parse_si_uint64_one("1G", 1 * 1000 * 1000 * 1000);
+ test_config_parse_si_uint64_one("0G", 0);
+ test_config_parse_si_uint64_one("0", 0);
+
+ test_config_parse_si_uint64_one("-982", 0);
+ test_config_parse_si_uint64_one("49874444198739873000000G", 0);
+ test_config_parse_si_uint64_one("garbage", 0);
+}
+
+TEST(config_parse_int) {
+ test_config_parse_int_one("1024", 1024);
+ test_config_parse_int_one("-1024", -1024);
+ test_config_parse_int_one("0", 0);
+
+ test_config_parse_int_one("99999999999999999999999999999999999999999999999999999999", -1);
+ test_config_parse_int_one("-99999999999999999999999999999999999999999999999999999999", -1);
+ test_config_parse_int_one("1G", -1);
+ test_config_parse_int_one("garbage", -1);
+}
+
+TEST(config_parse_unsigned) {
+ test_config_parse_unsigned_one("10241024", 10241024);
+ test_config_parse_unsigned_one("1024", 1024);
+ test_config_parse_unsigned_one("0", 0);
+
+ test_config_parse_unsigned_one("99999999999999999999999999999999999999999999999999999999", 0);
+ test_config_parse_unsigned_one("1G", 0);
+ test_config_parse_unsigned_one("garbage", 0);
+ test_config_parse_unsigned_one("1000garbage", 0);
+}
+
+TEST(config_parse_strv) {
+ test_config_parse_strv_one("", STRV_MAKE_EMPTY);
+ test_config_parse_strv_one("foo", STRV_MAKE("foo"));
+ test_config_parse_strv_one("foo bar foo", STRV_MAKE("foo", "bar", "foo"));
+ test_config_parse_strv_one("\"foo bar\" foo", STRV_MAKE("foo bar", "foo"));
+ test_config_parse_strv_one("\xc3\x80", STRV_MAKE("\xc3\x80"));
+ test_config_parse_strv_one("\xc3\x7f", STRV_MAKE("\xc3\x7f"));
+}
+
+TEST(config_parse_mode) {
+ test_config_parse_mode_one("777", 0777);
+ test_config_parse_mode_one("644", 0644);
+
+ test_config_parse_mode_one("-777", 0);
+ test_config_parse_mode_one("999", 0);
+ test_config_parse_mode_one("garbage", 0);
+ test_config_parse_mode_one("777garbage", 0);
+ test_config_parse_mode_one("777 garbage", 0);
+}
+
+TEST(config_parse_sec) {
+ test_config_parse_sec_one("1", 1 * USEC_PER_SEC);
+ test_config_parse_sec_one("1s", 1 * USEC_PER_SEC);
+ test_config_parse_sec_one("100ms", 100 * USEC_PER_MSEC);
+ test_config_parse_sec_one("5min 20s", 5 * 60 * USEC_PER_SEC + 20 * USEC_PER_SEC);
+
+ test_config_parse_sec_one("-1", 0);
+ test_config_parse_sec_one("10foo", 0);
+ test_config_parse_sec_one("garbage", 0);
+}
+
+TEST(config_parse_nsec) {
+ test_config_parse_nsec_one("1", 1);
+ test_config_parse_nsec_one("1s", 1 * NSEC_PER_SEC);
+ test_config_parse_nsec_one("100ms", 100 * NSEC_PER_MSEC);
+ test_config_parse_nsec_one("5min 20s", 5 * 60 * NSEC_PER_SEC + 20 * NSEC_PER_SEC);
+
+ test_config_parse_nsec_one("-1", 0);
+ test_config_parse_nsec_one("10foo", 0);
+ test_config_parse_nsec_one("garbage", 0);
+}
+
+TEST(config_parse_iec_uint64) {
+ uint64_t offset = 0;
+ assert_se(config_parse_iec_uint64(NULL, "/this/file", 11, "Section", 22, "Size", 0, "4M", &offset, NULL) == 0);
+ assert_se(offset == 4 * 1024 * 1024);
+
+ assert_se(config_parse_iec_uint64(NULL, "/this/file", 11, "Section", 22, "Size", 0, "4.5M", &offset, NULL) == 0);
+}
+
+#define x10(x) x x x x x x x x x x
+#define x100(x) x10(x10(x))
+#define x1000(x) x10(x100(x))
+
+static const char* const config_file[] = {
+ "[Section]\n"
+ "setting1=1\n",
+
+ "[Section]\n"
+ "setting1=1", /* no terminating newline */
+
+ "\n\n\n\n[Section]\n\n\n"
+ "setting1=1", /* some whitespace, no terminating newline */
+
+ "[Section]\n"
+ "[Section]\n"
+ "setting1=1\n"
+ "setting1= 2 \t\n"
+ "setting1= 1\n", /* repeated settings */
+
+ "[Section]\n"
+ "[Section]\n"
+ "setting1=1\n"
+ "setting1=2\\\n"
+ " \n" /* empty line breaks continuation */
+ "setting1=1\n", /* repeated settings */
+
+ "[Section]\n"
+ "setting1=1\\\n" /* normal continuation */
+ "2\\\n"
+ "3\n",
+
+ "[Section]\n"
+ "#hogehoge\\\n" /* continuation is ignored in comment */
+ "setting1=1\\\n" /* normal continuation */
+ "2\\\n"
+ "3\n",
+
+ "[Section]\n"
+ "setting1=1\\\n" /* normal continuation */
+ "#hogehoge\\\n" /* commented out line in continuation is ignored */
+ "2\\\n"
+ "3\n",
+
+ "[Section]\n"
+ " #hogehoge\\\n" /* whitespaces before comments */
+ " setting1=1\\\n" /* whitespaces before key */
+ "2\\\n"
+ "3\n",
+
+ "[Section]\n"
+ " setting1=1\\\n" /* whitespaces before key */
+ " #hogehoge\\\n" /* commented out line prefixed with whitespaces in continuation */
+ "2\\\n"
+ "3\n",
+
+ "[Section]\n"
+ "setting1=1\\\n" /* continuation with extra trailing backslash at the end */
+ "2\\\n"
+ "3\\\n",
+
+ "[Section]\n"
+ "setting1=1\\\\\\\n" /* continuation with trailing escape symbols */
+ "\\\\2\n", /* note that C requires one level of escaping, so the
+ * parser gets "…1 BS BS BS NL BS BS 2 NL", which
+ * it translates into "…1 BS BS SP BS BS 2" */
+
+ "\n[Section]\n\n"
+ "setting1=" /* a line above LINE_MAX length */
+ x1000("ABCD")
+ "\n",
+
+ "[Section]\n"
+ "setting1=" /* a line above LINE_MAX length, with continuation */
+ x1000("ABCD") "\\\n"
+ "foobar",
+
+ "[Section]\n"
+ "setting1=" /* a line above LINE_MAX length, with continuation */
+ x1000("ABCD") "\\\n" /* and an extra trailing backslash */
+ "foobar\\\n",
+
+ "[Section]\n"
+ "setting1=" /* a line above the allowed limit: 9 + 1050000 + 1 */
+ x1000(x1000("x") x10("abcde")) "\n",
+
+ "[Section]\n"
+ "setting1=" /* many continuation lines, together above the limit */
+ x1000(x1000("x") x10("abcde") "\\\n") "xxx",
+
+ "[Section]\n"
+ "setting1=2\n"
+ "[NoWarnSection]\n"
+ "setting1=3\n"
+ "[WarnSection]\n"
+ "setting1=3\n"
+ "[X-Section]\n"
+ "setting1=3\n",
+};
+
+static void test_config_parse_one(unsigned i, const char *s) {
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-conf-parser.XXXXXX";
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *setting1 = NULL;
+ int r;
+
+ const ConfigTableItem items[] = {
+ { "Section", "setting1", config_parse_string, 0, &setting1},
+ {}
+ };
+
+ log_info("== %s[%u] ==", __func__, i);
+
+ assert_se(fmkostemp_safe(name, "r+", &f) == 0);
+ assert_se(fwrite(s, strlen(s), 1, f) == 1);
+ rewind(f);
+
+ /*
+ int config_parse(const char *unit,
+ const char *filename,
+ FILE *f,
+ const char *sections,
+ ConfigItemLookup lookup,
+ const void *table,
+ ConfigParseFlags flags,
+ void *userdata,
+ struct stat *ret_stat);
+ */
+
+ r = config_parse(NULL, name, f,
+ "Section\0"
+ "-NoWarnSection\0",
+ config_item_table_lookup, items,
+ CONFIG_PARSE_WARN,
+ NULL,
+ NULL);
+
+ switch (i) {
+ case 0 ... 4:
+ assert_se(r == 1);
+ assert_se(streq(setting1, "1"));
+ break;
+
+ case 5 ... 10:
+ assert_se(r == 1);
+ assert_se(streq(setting1, "1 2 3"));
+ break;
+
+ case 11:
+ assert_se(r == 1);
+ assert_se(streq(setting1, "1\\\\ \\\\2"));
+ break;
+
+ case 12:
+ assert_se(r == 1);
+ assert_se(streq(setting1, x1000("ABCD")));
+ break;
+
+ case 13 ... 14:
+ assert_se(r == 1);
+ assert_se(streq(setting1, x1000("ABCD") " foobar"));
+ break;
+
+ case 15 ... 16:
+ assert_se(r == -ENOBUFS);
+ assert_se(setting1 == NULL);
+ break;
+
+ case 17:
+ assert_se(r == 1);
+ assert_se(streq(setting1, "2"));
+ break;
+ }
+}
+
+TEST(config_parse) {
+ for (unsigned i = 0; i < ELEMENTSOF(config_file); i++)
+ test_config_parse_one(i, config_file[i]);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-copy.c b/src/test/test-copy.c
new file mode 100644
index 0000000..f3144f0
--- /dev/null
+++ b/src/test/test-copy.c
@@ -0,0 +1,532 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/file.h>
+#include <sys/xattr.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "chase.h"
+#include "copy.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "hexdecoct.h"
+#include "io-util.h"
+#include "log.h"
+#include "macro.h"
+#include "mkdir.h"
+#include "path-util.h"
+#include "random-util.h"
+#include "rm-rf.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+#include "user-util.h"
+#include "xattr-util.h"
+
+TEST(copy_file) {
+ _cleanup_free_ char *buf = NULL;
+ _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-copy_file.XXXXXX";
+ _cleanup_(unlink_tempfilep) char fn_copy[] = "/tmp/test-copy_file.XXXXXX";
+ size_t sz = 0;
+ int fd;
+
+ fd = mkostemp_safe(fn);
+ assert_se(fd >= 0);
+ close(fd);
+
+ fd = mkostemp_safe(fn_copy);
+ assert_se(fd >= 0);
+ close(fd);
+
+ assert_se(write_string_file(fn, "foo bar bar bar foo", WRITE_STRING_FILE_CREATE) == 0);
+
+ assert_se(copy_file(fn, fn_copy, 0, 0644, COPY_REFLINK) == 0);
+
+ assert_se(read_full_file(fn_copy, &buf, &sz) == 0);
+ assert_se(streq(buf, "foo bar bar bar foo\n"));
+ assert_se(sz == 20);
+}
+
+static bool read_file_at_and_streq(int dir_fd, const char *path, const char *expected) {
+ _cleanup_free_ char *buf = NULL;
+
+ assert_se(read_full_file_at(dir_fd, path, &buf, NULL) == 0);
+ return streq(buf, expected);
+}
+
+TEST(copy_tree_replace_file) {
+ _cleanup_free_ char *src = NULL, *dst = NULL;
+
+ assert_se(tempfn_random("/tmp/test-copy_file.XXXXXX", NULL, &src) >= 0);
+ assert_se(tempfn_random("/tmp/test-copy_file.XXXXXX", NULL, &dst) >= 0);
+
+ assert_se(write_string_file(src, "bar bar", WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(write_string_file(dst, "foo foo foo", WRITE_STRING_FILE_CREATE) == 0);
+
+ /* The file exists- now overwrite original contents, and test the COPY_REPLACE flag. */
+
+ assert_se(copy_tree(src, dst, UID_INVALID, GID_INVALID, COPY_REFLINK, NULL, NULL) == -EEXIST);
+
+ assert_se(read_file_at_and_streq(AT_FDCWD, dst, "foo foo foo\n"));
+
+ assert_se(copy_tree(src, dst, UID_INVALID, GID_INVALID, COPY_REFLINK|COPY_REPLACE, NULL, NULL) == 0);
+
+ assert_se(read_file_at_and_streq(AT_FDCWD, dst, "bar bar\n"));
+}
+
+TEST(copy_tree_replace_dirs) {
+ _cleanup_(rm_rf_physical_and_freep) char *srcp = NULL, *dstp = NULL;
+ _cleanup_close_ int src = -EBADF, dst = -EBADF;
+
+ /* Create the random source/destination directories */
+ assert_se((src = mkdtemp_open(NULL, 0, &srcp)) >= 0);
+ assert_se((dst = mkdtemp_open(NULL, 0, &dstp)) >= 0);
+
+ /* Populate some data to differentiate the files. */
+ assert_se(write_string_file_at(src, "foo", "src file 1", WRITE_STRING_FILE_CREATE) >= 0);
+ assert_se(write_string_file_at(src, "bar", "src file 2", WRITE_STRING_FILE_CREATE) == 0);
+
+ assert_se(write_string_file_at(dst, "foo", "dest file 1", WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(write_string_file_at(dst, "bar", "dest file 2", WRITE_STRING_FILE_CREATE) == 0);
+
+ /* Copying without COPY_REPLACE should fail because the destination file already exists. */
+ assert_se(copy_tree_at(src, ".", dst, ".", UID_INVALID, GID_INVALID, COPY_REFLINK, NULL, NULL) == -EEXIST);
+
+ assert_se(read_file_at_and_streq(src, "foo", "src file 1\n"));
+ assert_se(read_file_at_and_streq(src, "bar", "src file 2\n"));
+ assert_se(read_file_at_and_streq(dst, "foo", "dest file 1\n"));
+ assert_se(read_file_at_and_streq(dst, "bar", "dest file 2\n"));
+
+ assert_se(copy_tree_at(src, ".", dst, ".", UID_INVALID, GID_INVALID, COPY_REFLINK|COPY_REPLACE|COPY_MERGE, NULL, NULL) == 0);
+
+ assert_se(read_file_at_and_streq(src, "foo", "src file 1\n"));
+ assert_se(read_file_at_and_streq(src, "bar", "src file 2\n"));
+ assert_se(read_file_at_and_streq(dst, "foo", "src file 1\n"));
+ assert_se(read_file_at_and_streq(dst, "bar", "src file 2\n"));
+}
+
+TEST(copy_file_fd) {
+ _cleanup_(unlink_tempfilep) char in_fn[] = "/tmp/test-copy-file-fd-XXXXXX";
+ _cleanup_(unlink_tempfilep) char out_fn[] = "/tmp/test-copy-file-fd-XXXXXX";
+ _cleanup_close_ int in_fd = -EBADF, out_fd = -EBADF;
+ const char *text = "boohoo\nfoo\n\tbar\n";
+ char buf[64] = {};
+
+ in_fd = mkostemp_safe(in_fn);
+ assert_se(in_fd >= 0);
+ out_fd = mkostemp_safe(out_fn);
+ assert_se(out_fd >= 0);
+
+ assert_se(write_string_file(in_fn, text, WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(copy_file_fd("/a/file/which/does/not/exist/i/guess", out_fd, COPY_REFLINK) < 0);
+ assert_se(copy_file_fd(in_fn, out_fd, COPY_REFLINK) >= 0);
+ assert_se(lseek(out_fd, SEEK_SET, 0) == 0);
+
+ assert_se(read(out_fd, buf, sizeof buf) == (ssize_t) strlen(text));
+ assert_se(streq(buf, text));
+}
+
+TEST(copy_tree) {
+ _cleanup_hashmap_free_ Hashmap *denylist = NULL;
+ _cleanup_free_ char *cp = NULL;
+ char original_dir[] = "/tmp/test-copy_tree/";
+ char copy_dir[] = "/tmp/test-copy_tree-copy/";
+ char **files = STRV_MAKE("file", "dir1/file", "dir1/dir2/file", "dir1/dir2/dir3/dir4/dir5/file");
+ char **symlinks = STRV_MAKE("link", "file",
+ "link2", "dir1/file");
+ char **hardlinks = STRV_MAKE("hlink", "file",
+ "hlink2", "dir1/file");
+ const char *unixsockp, *ignorep;
+ struct stat st;
+ int xattr_worked = -1; /* xattr support is optional in temporary directories, hence use it if we can,
+ * but don't fail if we can't */
+
+ (void) rm_rf(copy_dir, REMOVE_ROOT|REMOVE_PHYSICAL);
+ (void) rm_rf(original_dir, REMOVE_ROOT|REMOVE_PHYSICAL);
+
+ STRV_FOREACH(p, files) {
+ _cleanup_free_ char *f = NULL, *c = NULL;
+ int k;
+
+ assert_se(f = path_join(original_dir, *p));
+
+ assert_se(write_string_file(f, "file", WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755) == 0);
+
+ assert_se(base64mem(*p, strlen(*p), &c) >= 0);
+
+ k = setxattr(f, "user.testxattr", c, strlen(c), 0);
+ assert_se(xattr_worked < 0 || ((k >= 0) == !!xattr_worked));
+ xattr_worked = k >= 0;
+ }
+
+ STRV_FOREACH_PAIR(ll, p, symlinks) {
+ _cleanup_free_ char *f = NULL, *l = NULL;
+
+ assert_se(f = path_join(original_dir, *p));
+ assert_se(l = path_join(original_dir, *ll));
+
+ assert_se(mkdir_parents(l, 0755) >= 0);
+ assert_se(symlink(f, l) == 0);
+ }
+
+ STRV_FOREACH_PAIR(ll, p, hardlinks) {
+ _cleanup_free_ char *f = NULL, *l = NULL;
+
+ assert_se(f = path_join(original_dir, *p));
+ assert_se(l = path_join(original_dir, *ll));
+
+ assert_se(mkdir_parents(l, 0755) >= 0);
+ assert_se(link(f, l) == 0);
+ }
+
+ unixsockp = strjoina(original_dir, "unixsock");
+ assert_se(mknod(unixsockp, S_IFSOCK|0644, 0) >= 0);
+
+ ignorep = strjoina(original_dir, "ignore/file");
+ assert_se(write_string_file(ignorep, "ignore", WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755) == 0);
+ assert_se(RET_NERRNO(stat(ignorep, &st)) >= 0);
+ assert_se(cp = memdup(&st, sizeof(st)));
+ assert_se(hashmap_ensure_put(&denylist, &inode_hash_ops, cp, INT_TO_PTR(DENY_INODE)) >= 0);
+ TAKE_PTR(cp);
+
+ assert_se(copy_tree(original_dir, copy_dir, UID_INVALID, GID_INVALID, COPY_REFLINK|COPY_MERGE|COPY_HARDLINKS, denylist, NULL) == 0);
+
+ STRV_FOREACH(p, files) {
+ _cleanup_free_ char *buf = NULL, *f = NULL, *c = NULL;
+ size_t sz;
+ int k;
+
+ assert_se(f = path_join(copy_dir, *p));
+
+ assert_se(access(f, F_OK) == 0);
+ assert_se(read_full_file(f, &buf, &sz) == 0);
+ assert_se(streq(buf, "file\n"));
+
+ k = lgetxattr_malloc(f, "user.testxattr", &c);
+ assert_se(xattr_worked < 0 || ((k >= 0) == !!xattr_worked));
+
+ if (k >= 0) {
+ _cleanup_free_ char *d = NULL;
+
+ assert_se(base64mem(*p, strlen(*p), &d) >= 0);
+ assert_se(streq(d, c));
+ }
+ }
+
+ STRV_FOREACH_PAIR(ll, p, symlinks) {
+ _cleanup_free_ char *target = NULL, *f = NULL, *l = NULL;
+
+ assert_se(f = strjoin(original_dir, *p));
+ assert_se(l = strjoin(copy_dir, *ll));
+
+ assert_se(chase(l, NULL, 0, &target, NULL) == 1);
+ assert_se(path_equal(f, target));
+ }
+
+ STRV_FOREACH_PAIR(ll, p, hardlinks) {
+ _cleanup_free_ char *f = NULL, *l = NULL;
+ struct stat a, b;
+
+ assert_se(f = strjoin(copy_dir, *p));
+ assert_se(l = strjoin(copy_dir, *ll));
+
+ assert_se(lstat(f, &a) >= 0);
+ assert_se(lstat(l, &b) >= 0);
+
+ assert_se(a.st_ino == b.st_ino);
+ assert_se(a.st_dev == b.st_dev);
+ }
+
+ unixsockp = strjoina(copy_dir, "unixsock");
+ assert_se(stat(unixsockp, &st) >= 0);
+ assert_se(S_ISSOCK(st.st_mode));
+
+ assert_se(copy_tree(original_dir, copy_dir, UID_INVALID, GID_INVALID, COPY_REFLINK, denylist, NULL) < 0);
+ assert_se(copy_tree("/tmp/inexistent/foo/bar/fsdoi", copy_dir, UID_INVALID, GID_INVALID, COPY_REFLINK, denylist, NULL) < 0);
+
+ ignorep = strjoina(copy_dir, "ignore/file");
+ assert_se(RET_NERRNO(access(ignorep, F_OK)) == -ENOENT);
+
+ (void) rm_rf(copy_dir, REMOVE_ROOT|REMOVE_PHYSICAL);
+ (void) rm_rf(original_dir, REMOVE_ROOT|REMOVE_PHYSICAL);
+}
+
+TEST(copy_bytes) {
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ _cleanup_close_ int infd = -EBADF;
+ int r, r2;
+ char buf[1024], buf2[1024];
+
+ infd = open("/usr/lib/os-release", O_RDONLY|O_CLOEXEC);
+ if (infd < 0)
+ infd = open("/etc/os-release", O_RDONLY|O_CLOEXEC);
+ assert_se(infd >= 0);
+
+ assert_se(pipe2(pipefd, O_CLOEXEC) == 0);
+
+ r = copy_bytes(infd, pipefd[1], UINT64_MAX, 0);
+ assert_se(r == 0);
+
+ r = read(pipefd[0], buf, sizeof(buf));
+ assert_se(r >= 0);
+
+ assert_se(lseek(infd, 0, SEEK_SET) == 0);
+ r2 = read(infd, buf2, sizeof(buf2));
+ assert_se(r == r2);
+
+ assert_se(strneq(buf, buf2, r));
+
+ /* test copy_bytes with invalid descriptors */
+ r = copy_bytes(pipefd[0], pipefd[0], 1, 0);
+ assert_se(r == -EBADF);
+
+ r = copy_bytes(pipefd[1], pipefd[1], 1, 0);
+ assert_se(r == -EBADF);
+
+ r = copy_bytes(pipefd[1], infd, 1, 0);
+ assert_se(r == -EBADF);
+}
+
+static void test_copy_bytes_regular_file_one(const char *src, bool try_reflink, uint64_t max_bytes) {
+ _cleanup_(unlink_tempfilep) char fn2[] = "/tmp/test-copy-file-XXXXXX";
+ _cleanup_(unlink_tempfilep) char fn3[] = "/tmp/test-copy-file-XXXXXX";
+ _cleanup_close_ int fd = -EBADF, fd2 = -EBADF, fd3 = -EBADF;
+ int r;
+ struct stat buf, buf2, buf3;
+
+ log_info("%s try_reflink=%s max_bytes=%" PRIu64, __func__, yes_no(try_reflink), max_bytes);
+
+ fd = open(src, O_CLOEXEC | O_PATH);
+ assert_se(fd >= 0);
+
+ fd2 = mkostemp_safe(fn2);
+ assert_se(fd2 >= 0);
+
+ fd3 = mkostemp_safe(fn3);
+ assert_se(fd3 >= 0);
+
+ r = copy_bytes(fd, fd2, max_bytes, try_reflink ? COPY_REFLINK : 0);
+ if (max_bytes == UINT64_MAX)
+ assert_se(r == 0);
+ else
+ assert_se(IN_SET(r, 0, 1));
+
+ assert_se(fstat(fd, &buf) == 0);
+ assert_se(fstat(fd2, &buf2) == 0);
+ assert_se((uint64_t) buf2.st_size == MIN((uint64_t) buf.st_size, max_bytes));
+
+ if (max_bytes < UINT64_MAX)
+ /* Make sure the file is now higher than max_bytes */
+ assert_se(ftruncate(fd2, max_bytes + 1) == 0);
+
+ assert_se(lseek(fd2, 0, SEEK_SET) == 0);
+
+ r = copy_bytes(fd2, fd3, max_bytes, try_reflink ? COPY_REFLINK : 0);
+ if (max_bytes == UINT64_MAX)
+ assert_se(r == 0);
+ else
+ /* We cannot distinguish between the input being exactly max_bytes
+ * or longer than max_bytes (without trying to read one more byte,
+ * or calling stat, or FION_READ, etc, and we don't want to do any
+ * of that). So we expect "truncation" since we know that file we
+ * are copying is exactly max_bytes bytes. */
+ assert_se(r == 1);
+
+ assert_se(fstat(fd3, &buf3) == 0);
+
+ if (max_bytes == UINT64_MAX)
+ assert_se(buf3.st_size == buf2.st_size);
+ else
+ assert_se((uint64_t) buf3.st_size == max_bytes);
+}
+
+TEST(copy_bytes_regular_file) {
+ test_copy_bytes_regular_file_one(saved_argv[0], false, UINT64_MAX);
+ test_copy_bytes_regular_file_one(saved_argv[0], true, UINT64_MAX);
+ test_copy_bytes_regular_file_one(saved_argv[0], false, 1000); /* smaller than copy buffer size */
+ test_copy_bytes_regular_file_one(saved_argv[0], true, 1000);
+ test_copy_bytes_regular_file_one(saved_argv[0], false, 32000); /* larger than copy buffer size */
+ test_copy_bytes_regular_file_one(saved_argv[0], true, 32000);
+}
+
+TEST(copy_atomic) {
+ _cleanup_(rm_rf_physical_and_freep) char *p = NULL;
+ const char *q;
+ int r;
+
+ assert_se(mkdtemp_malloc(NULL, &p) >= 0);
+
+ q = strjoina(p, "/fstab");
+
+ r = copy_file_atomic("/etc/fstab", q, 0644, COPY_REFLINK);
+ if (r == -ENOENT || ERRNO_IS_PRIVILEGE(r))
+ return;
+
+ assert_se(copy_file_atomic("/etc/fstab", q, 0644, COPY_REFLINK) == -EEXIST);
+
+ assert_se(copy_file_atomic("/etc/fstab", q, 0644, COPY_REPLACE) >= 0);
+}
+
+TEST(copy_proc) {
+ _cleanup_(rm_rf_physical_and_freep) char *p = NULL;
+ _cleanup_free_ char *f = NULL, *a = NULL, *b = NULL;
+
+ /* Check if copying data from /proc/ works correctly, i.e. let's see if https://lwn.net/Articles/846403/ is a problem for us */
+
+ assert_se(mkdtemp_malloc(NULL, &p) >= 0);
+ assert_se(f = path_join(p, "version"));
+ assert_se(copy_file("/proc/version", f, 0, MODE_INVALID, 0) >= 0);
+
+ assert_se(read_one_line_file("/proc/version", &a) >= 0);
+ assert_se(read_one_line_file(f, &b) >= 0);
+ assert_se(streq(a, b));
+ assert_se(!isempty(a));
+}
+
+TEST_RET(copy_holes) {
+ _cleanup_(unlink_tempfilep) char fn[] = "/var/tmp/test-copy-hole-fd-XXXXXX";
+ _cleanup_(unlink_tempfilep) char fn_copy[] = "/var/tmp/test-copy-hole-fd-XXXXXX";
+ struct stat stat;
+ off_t blksz;
+ int r, fd, fd_copy;
+ char *buf;
+
+ fd = mkostemp_safe(fn);
+ assert_se(fd >= 0);
+
+ fd_copy = mkostemp_safe(fn_copy);
+ assert_se(fd_copy >= 0);
+
+ r = RET_NERRNO(fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, 0, 1));
+ if (ERRNO_IS_NOT_SUPPORTED(r))
+ return log_tests_skipped("Filesystem doesn't support hole punching");
+ assert_se(r >= 0);
+
+ assert_se(fstat(fd, &stat) >= 0);
+ blksz = stat.st_blksize;
+ buf = alloca_safe(blksz);
+ memset(buf, 1, blksz);
+
+ /* We need to make sure to create hole in multiples of the block size, otherwise filesystems (btrfs)
+ * might silently truncate/extend the holes. */
+
+ assert_se(lseek(fd, blksz, SEEK_CUR) >= 0);
+ assert_se(write(fd, buf, blksz) >= 0);
+ assert_se(lseek(fd, 0, SEEK_END) == 2 * blksz);
+ /* Only ftruncate() can create holes at the end of a file. */
+ assert_se(ftruncate(fd, 3 * blksz) >= 0);
+ assert_se(lseek(fd, 0, SEEK_SET) >= 0);
+
+ assert_se(copy_bytes(fd, fd_copy, UINT64_MAX, COPY_HOLES) >= 0);
+
+ /* Test that the hole starts at the beginning of the file. */
+ assert_se(lseek(fd_copy, 0, SEEK_HOLE) == 0);
+ /* Test that the hole has the expected size. */
+ assert_se(lseek(fd_copy, 0, SEEK_DATA) == blksz);
+ assert_se(lseek(fd_copy, blksz, SEEK_HOLE) == 2 * blksz);
+ assert_se(lseek(fd_copy, 2 * blksz, SEEK_DATA) < 0 && errno == ENXIO);
+
+ /* Test that the copied file has the correct size. */
+ assert_se(fstat(fd_copy, &stat) >= 0);
+ assert_se(stat.st_size == 3 * blksz);
+
+ close(fd);
+ close(fd_copy);
+
+ return 0;
+}
+
+TEST_RET(copy_holes_with_gaps) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ _cleanup_close_ int tfd = -EBADF, fd = -EBADF, fd_copy = -EBADF;
+ struct stat st;
+ off_t blksz;
+ char *buf;
+ int r;
+
+ assert_se((tfd = mkdtemp_open(NULL, 0, &t)) >= 0);
+ assert_se((fd = openat(tfd, "src", O_CREAT | O_RDWR, 0600)) >= 0);
+ assert_se((fd_copy = openat(tfd, "dst", O_CREAT | O_WRONLY, 0600)) >= 0);
+
+ assert_se(fstat(fd, &st) >= 0);
+ blksz = st.st_blksize;
+ buf = alloca_safe(blksz);
+ memset(buf, 1, blksz);
+
+ /* Create a file with:
+ * - hole of 1 block
+ * - data of 2 block
+ * - hole of 2 blocks
+ * - data of 1 block
+ *
+ * Since sparse files are based on blocks and not bytes, we need to make
+ * sure that the holes are aligned to the block size.
+ */
+
+ r = RET_NERRNO(fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, 0, blksz));
+ if (ERRNO_IS_NOT_SUPPORTED(r))
+ return log_tests_skipped("Filesystem doesn't support hole punching");
+
+ assert_se(lseek(fd, blksz, SEEK_CUR) >= 0);
+ assert_se(loop_write(fd, buf, blksz) >= 0);
+ assert_se(loop_write(fd, buf, blksz) >= 0);
+ assert_se(lseek(fd, 2 * blksz, SEEK_CUR) >= 0);
+ assert_se(loop_write(fd, buf, blksz) >= 0);
+ assert_se(lseek(fd, 0, SEEK_SET) >= 0);
+ assert_se(fsync(fd) >= 0);
+
+ /* Copy to the start of the second hole */
+ assert_se(copy_bytes(fd, fd_copy, 3 * blksz, COPY_HOLES) >= 0);
+ assert_se(fstat(fd_copy, &st) >= 0);
+ assert_se(st.st_size == 3 * blksz);
+
+ /* Copy to the middle of the second hole */
+ assert_se(lseek(fd, 0, SEEK_SET) >= 0);
+ assert_se(lseek(fd_copy, 0, SEEK_SET) >= 0);
+ assert_se(ftruncate(fd_copy, 0) >= 0);
+ assert_se(copy_bytes(fd, fd_copy, 4 * blksz, COPY_HOLES) >= 0);
+ assert_se(fstat(fd_copy, &st) >= 0);
+ assert_se(st.st_size == 4 * blksz);
+
+ /* Copy to the end of the second hole */
+ assert_se(lseek(fd, 0, SEEK_SET) >= 0);
+ assert_se(lseek(fd_copy, 0, SEEK_SET) >= 0);
+ assert_se(ftruncate(fd_copy, 0) >= 0);
+ assert_se(copy_bytes(fd, fd_copy, 5 * blksz, COPY_HOLES) >= 0);
+ assert_se(fstat(fd_copy, &st) >= 0);
+ assert_se(st.st_size == 5 * blksz);
+
+ /* Copy everything */
+ assert_se(lseek(fd, 0, SEEK_SET) >= 0);
+ assert_se(lseek(fd_copy, 0, SEEK_SET) >= 0);
+ assert_se(ftruncate(fd_copy, 0) >= 0);
+ assert_se(copy_bytes(fd, fd_copy, UINT64_MAX, COPY_HOLES) >= 0);
+ assert_se(fstat(fd_copy, &st) >= 0);
+ assert_se(st.st_size == 6 * blksz);
+
+ return 0;
+}
+
+TEST(copy_lock) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ _cleanup_close_ int tfd = -EBADF, fd = -EBADF;
+
+ assert_se((tfd = mkdtemp_open(NULL, 0, &t)) >= 0);
+ assert_se(mkdirat(tfd, "abc", 0755) >= 0);
+ assert_se(write_string_file_at(tfd, "abc/def", "abc", WRITE_STRING_FILE_CREATE) >= 0);
+
+ assert_se((fd = copy_directory_at(tfd, "abc", tfd, "qed", COPY_LOCK_BSD)) >= 0);
+ assert_se(faccessat(tfd, "qed", F_OK, 0) >= 0);
+ assert_se(faccessat(tfd, "qed/def", F_OK, 0) >= 0);
+ assert_se(xopenat_lock(tfd, "qed", 0, 0, 0, LOCK_BSD, LOCK_EX|LOCK_NB) == -EAGAIN);
+ fd = safe_close(fd);
+
+ assert_se((fd = copy_file_at(tfd, "abc/def", tfd, "poi", 0, 0644, COPY_LOCK_BSD)));
+ assert_se(read_file_at_and_streq(tfd, "poi", "abc\n"));
+ assert_se(xopenat_lock(tfd, "poi", 0, 0, 0, LOCK_BSD, LOCK_EX|LOCK_NB) == -EAGAIN);
+ fd = safe_close(fd);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-core-unit.c b/src/test/test-core-unit.c
new file mode 100644
index 0000000..dc108cc
--- /dev/null
+++ b/src/test/test-core-unit.c
@@ -0,0 +1,120 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "escape.h"
+#include "tests.h"
+#include "unit.h"
+
+static void test_unit_escape_setting_one(
+ const char *s,
+ const char *expected_exec_env,
+ const char *expected_exec,
+ const char *expected_c) {
+
+ _cleanup_free_ char *a = NULL, *b = NULL, *c = NULL, *d = NULL,
+ *s_esc = NULL, *a_esc = NULL, *b_esc = NULL, *c_esc = NULL, *d_esc = NULL;
+ const char *t;
+
+ if (!expected_exec_env)
+ expected_exec_env = s;
+ if (!expected_exec)
+ expected_exec = expected_exec_env;
+ if (!expected_c)
+ expected_c = expected_exec;
+ assert_se(s_esc = cescape(s));
+
+ assert_se(t = unit_escape_setting(s, 0, &a));
+ assert_se(a_esc = cescape(t));
+ log_debug("%s: [%s] → [%s]", __func__, s_esc, a_esc);
+ assert_se(a == NULL);
+ assert_se(t == s);
+
+ assert_se(t = unit_escape_setting(s, UNIT_ESCAPE_EXEC_SYNTAX_ENV, &b));
+ assert_se(b_esc = cescape(t));
+ log_debug("%s: [%s] → [%s]", __func__, s_esc, b_esc);
+ assert_se(b == NULL || streq(b, t));
+ assert_se(streq(t, expected_exec_env));
+
+ assert_se(t = unit_escape_setting(s, UNIT_ESCAPE_EXEC_SYNTAX, &c));
+ assert_se(c_esc = cescape(t));
+ log_debug("%s: [%s] → [%s]", __func__, s_esc, c_esc);
+ assert_se(c == NULL || streq(c, t));
+ assert_se(streq(t, expected_exec));
+
+ assert_se(t = unit_escape_setting(s, UNIT_ESCAPE_C, &d));
+ assert_se(d_esc = cescape(t));
+ log_debug("%s: [%s] → [%s]", __func__, s_esc, d_esc);
+ assert_se(d == NULL || streq(d, t));
+ assert_se(streq(t, expected_c));
+}
+
+TEST(unit_escape_setting) {
+ test_unit_escape_setting_one("/sbin/sbash", NULL, NULL, NULL);
+ test_unit_escape_setting_one("$", "$$", "$", "$");
+ test_unit_escape_setting_one("$$", "$$$$", "$$", "$$");
+ test_unit_escape_setting_one("'", "'", NULL, "\\'");
+ test_unit_escape_setting_one("\"", "\\\"", NULL, NULL);
+ test_unit_escape_setting_one("\t", "\\t", NULL, NULL);
+ test_unit_escape_setting_one(" ", NULL, NULL, NULL);
+ test_unit_escape_setting_one("$;'\"\t\n", "$$;'\\\"\\t\\n", "$;'\\\"\\t\\n", "$;\\'\\\"\\t\\n");
+}
+
+static void test_unit_concat_strv_one(
+ char **s,
+ const char *expected_none,
+ const char *expected_exec_env,
+ const char *expected_exec,
+ const char *expected_c) {
+
+ _cleanup_free_ char *a = NULL, *b = NULL, *c = NULL, *d = NULL,
+ *s_ser = NULL, *s_esc = NULL, *a_esc = NULL, *b_esc = NULL, *c_esc = NULL, *d_esc = NULL;
+
+ assert_se(s_ser = strv_join(s, "_"));
+ assert_se(s_esc = cescape(s_ser));
+ if (!expected_exec_env)
+ expected_exec_env = expected_none;
+ if (!expected_exec)
+ expected_exec = expected_none;
+ if (!expected_c)
+ expected_c = expected_none;
+
+ assert_se(a = unit_concat_strv(s, 0));
+ assert_se(a_esc = cescape(a));
+ log_debug("%s: [%s] → [%s]", __func__, s_esc, a_esc);
+ assert_se(streq(a, expected_none));
+
+ assert_se(b = unit_concat_strv(s, UNIT_ESCAPE_EXEC_SYNTAX_ENV));
+ assert_se(b_esc = cescape(b));
+ log_debug("%s: [%s] → [%s]", __func__, s_esc, b_esc);
+ assert_se(streq(b, expected_exec_env));
+
+ assert_se(c = unit_concat_strv(s, UNIT_ESCAPE_EXEC_SYNTAX));
+ assert_se(c_esc = cescape(c));
+ log_debug("%s: [%s] → [%s]", __func__, s_esc, c_esc);
+ assert_se(streq(c, expected_exec));
+
+ assert_se(d = unit_concat_strv(s, UNIT_ESCAPE_C));
+ assert_se(d_esc = cescape(d));
+ log_debug("%s: [%s] → [%s]", __func__, s_esc, d_esc);
+ assert_se(streq(d, expected_c));
+}
+
+TEST(unit_concat_strv) {
+ test_unit_concat_strv_one(STRV_MAKE("a", "b", "c"),
+ "\"a\" \"b\" \"c\"",
+ NULL,
+ NULL,
+ NULL);
+ test_unit_concat_strv_one(STRV_MAKE("a", " ", "$", "$$", ""),
+ "\"a\" \" \" \"$\" \"$$\" \"\"",
+ "\"a\" \" \" \"$$\" \"$$$$\" \"\"",
+ NULL,
+ NULL);
+ test_unit_concat_strv_one(STRV_MAKE("\n", " ", "\t"),
+ "\"\n\" \" \" \"\t\"",
+ "\"\\n\" \" \" \"\\t\"",
+ "\"\\n\" \" \" \"\\t\"",
+ "\"\\n\" \" \" \"\\t\"");
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-coredump-util.c b/src/test/test-coredump-util.c
new file mode 100644
index 0000000..4e7f3b4
--- /dev/null
+++ b/src/test/test-coredump-util.c
@@ -0,0 +1,161 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <elf.h>
+
+#include "alloc-util.h"
+#include "coredump-util.h"
+#include "fileio.h"
+#include "fd-util.h"
+#include "format-util.h"
+#include "macro.h"
+#include "tests.h"
+
+TEST(coredump_filter_to_from_string) {
+ for (CoredumpFilter i = 0; i < _COREDUMP_FILTER_MAX; i++) {
+ const char *n;
+
+ assert_se(n = coredump_filter_to_string(i));
+ log_info("0x%x\t%s", 1u << i, n);
+ assert_se(coredump_filter_from_string(n) == i);
+
+ uint64_t f;
+ assert_se(coredump_filter_mask_from_string(n, &f) == 0);
+ assert_se(f == 1u << i);
+ }
+}
+
+TEST(coredump_filter_mask_from_string) {
+ uint64_t f;
+ assert_se(coredump_filter_mask_from_string("default", &f) == 0);
+ assert_se(f == COREDUMP_FILTER_MASK_DEFAULT);
+ assert_se(coredump_filter_mask_from_string("all", &f) == 0);
+ assert_se(f == COREDUMP_FILTER_MASK_ALL);
+
+ assert_se(coredump_filter_mask_from_string(" default\tdefault\tdefault ", &f) == 0);
+ assert_se(f == COREDUMP_FILTER_MASK_DEFAULT);
+
+ assert_se(coredump_filter_mask_from_string("defaulta", &f) < 0);
+ assert_se(coredump_filter_mask_from_string("default defaulta default", &f) < 0);
+ assert_se(coredump_filter_mask_from_string("default default defaulta", &f) < 0);
+
+ assert_se(coredump_filter_mask_from_string("private-anonymous default", &f) == 0);
+ assert_se(f == COREDUMP_FILTER_MASK_DEFAULT);
+
+ assert_se(coredump_filter_mask_from_string("shared-file-backed shared-dax", &f) == 0);
+ assert_se(f == (1 << COREDUMP_FILTER_SHARED_FILE_BACKED |
+ 1 << COREDUMP_FILTER_SHARED_DAX));
+
+ assert_se(coredump_filter_mask_from_string("private-file-backed private-dax 0xF", &f) == 0);
+ assert_se(f == (1 << COREDUMP_FILTER_PRIVATE_FILE_BACKED |
+ 1 << COREDUMP_FILTER_PRIVATE_DAX |
+ 0xF));
+
+ assert_se(coredump_filter_mask_from_string("11", &f) == 0);
+ assert_se(f == 0x11);
+
+ assert_se(coredump_filter_mask_from_string("0x1101", &f) == 0);
+ assert_se(f == 0x1101);
+
+ assert_se(coredump_filter_mask_from_string("0", &f) == 0);
+ assert_se(f == 0);
+
+ assert_se(coredump_filter_mask_from_string("all", &f) == 0);
+ assert_se(FLAGS_SET(f, (1 << COREDUMP_FILTER_PRIVATE_ANONYMOUS |
+ 1 << COREDUMP_FILTER_SHARED_ANONYMOUS |
+ 1 << COREDUMP_FILTER_PRIVATE_FILE_BACKED |
+ 1 << COREDUMP_FILTER_SHARED_FILE_BACKED |
+ 1 << COREDUMP_FILTER_ELF_HEADERS |
+ 1 << COREDUMP_FILTER_PRIVATE_HUGE |
+ 1 << COREDUMP_FILTER_SHARED_HUGE |
+ 1 << COREDUMP_FILTER_PRIVATE_DAX |
+ 1 << COREDUMP_FILTER_SHARED_DAX)));
+}
+
+static void test_parse_auxv_two(
+ uint8_t elf_class,
+ size_t offset,
+ const char *data,
+ size_t data_size,
+ int expect_at_secure,
+ uid_t expect_uid,
+ uid_t expect_euid,
+ gid_t expect_gid,
+ gid_t expect_egid) {
+
+ int at_secure;
+ uid_t uid, euid;
+ gid_t gid, egid;
+ assert_se(parse_auxv(LOG_ERR, elf_class, data, data_size,
+ &at_secure, &uid, &euid, &gid, &egid) == 0);
+
+ log_debug("[offset=%zu] at_secure=%d, uid="UID_FMT", euid="UID_FMT", gid="GID_FMT", egid="GID_FMT,
+ offset,
+ at_secure, uid, euid, gid, egid);
+
+ assert_se(uid == expect_uid);
+ assert_se(euid == expect_euid);
+ assert_se(gid == expect_gid);
+ assert_se(egid == expect_egid);
+}
+
+static void test_parse_auxv_one(
+ uint8_t elf_class,
+ int dir_fd,
+ const char *filename,
+ int expect_at_secure,
+ uid_t expect_uid,
+ uid_t expect_euid,
+ gid_t expect_gid,
+ gid_t expect_egid) {
+
+ _cleanup_free_ char *buf;
+ const char *data;
+ size_t data_size;
+ log_info("Parsing %s…", filename);
+ assert_se(read_full_file_at(dir_fd, filename, &buf, &data_size) >= 0);
+
+ for (size_t offset = 0; offset < 8; offset++) {
+ _cleanup_free_ char *buf2 = NULL;
+
+ if (offset == 0)
+ data = buf;
+ else {
+ assert_se(buf2 = malloc(offset + data_size));
+ memcpy(buf2 + offset, buf, data_size);
+ data = buf2 + offset;
+ }
+
+ test_parse_auxv_two(elf_class, offset, data, data_size,
+ expect_at_secure, expect_uid, expect_euid, expect_gid, expect_egid);
+ }
+}
+
+TEST(parse_auxv) {
+ _cleanup_free_ char *dir = NULL;
+ _cleanup_close_ int dir_fd = -EBADF;
+
+ assert_se(get_testdata_dir("auxv", &dir) >= 0);
+ dir_fd = open(dir, O_RDONLY | O_CLOEXEC | O_DIRECTORY | O_PATH);
+ assert_se(dir_fd >= 0);
+
+ if (__BYTE_ORDER == __LITTLE_ENDIAN) {
+ test_parse_auxv_one(ELFCLASS32, dir_fd, "resolved.arm32", 0, 193, 193, 193, 193);
+ test_parse_auxv_one(ELFCLASS64, dir_fd, "bash.riscv64", 0, 1001, 1001, 1001, 1001);
+ test_parse_auxv_one(ELFCLASS32, dir_fd, "sleep.i686", 0, 1000, 1000, 1000, 1000);
+ /* after chgrp and chmod g+s */
+ test_parse_auxv_one(ELFCLASS32, dir_fd, "sleep32.i686", 1, 1000, 1000, 1000, 10);
+ test_parse_auxv_one(ELFCLASS64, dir_fd, "sleep64.amd64", 1, 1000, 1000, 1000, 10);
+
+ test_parse_auxv_one(ELFCLASS64, dir_fd, "sudo.aarch64", 1, 1494200408, 0, 1494200408, 1494200408);
+ test_parse_auxv_one(ELFCLASS64, dir_fd, "sudo.amd64", 1, 1000, 0, 1000, 1000);
+
+ /* Those run unprivileged, but start as root. */
+ test_parse_auxv_one(ELFCLASS64, dir_fd, "dbus-broker-launch.amd64", 0, 0, 0, 0, 0);
+ test_parse_auxv_one(ELFCLASS64, dir_fd, "dbus-broker-launch.aarch64", 0, 0, 0, 0, 0);
+ test_parse_auxv_one(ELFCLASS64, dir_fd, "polkitd.aarch64", 0, 0, 0, 0, 0);
+ } else {
+ test_parse_auxv_one(ELFCLASS64, dir_fd, "cat.s390x", 0, 3481, 3481, 3481, 3481);
+ }
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-cpu-set-util.c b/src/test/test-cpu-set-util.c
new file mode 100644
index 0000000..a0660f5
--- /dev/null
+++ b/src/test/test-cpu-set-util.c
@@ -0,0 +1,280 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "cpu-set-util.h"
+#include "string-util.h"
+#include "tests.h"
+#include "macro.h"
+
+TEST(parse_cpu_set) {
+ CPUSet c = {};
+ _cleanup_free_ char *str = NULL;
+ int cpu;
+
+ /* Single value */
+ assert_se(parse_cpu_set_full("0", &c, true, NULL, "fake", 1, "CPUAffinity") >= 0);
+ assert_se(c.set);
+ assert_se(c.allocated >= DIV_ROUND_UP(sizeof(__cpu_mask), 8));
+ assert_se(CPU_ISSET_S(0, c.allocated, c.set));
+ assert_se(CPU_COUNT_S(c.allocated, c.set) == 1);
+
+ assert_se(str = cpu_set_to_string(&c));
+ log_info("cpu_set_to_string: %s", str);
+ str = mfree(str);
+ assert_se(str = cpu_set_to_range_string(&c));
+ log_info("cpu_set_to_range_string: %s", str);
+ assert_se(streq(str, "0"));
+ str = mfree(str);
+ cpu_set_reset(&c);
+
+ /* Simple range (from CPUAffinity example) */
+ assert_se(parse_cpu_set_full("1 2 4", &c, true, NULL, "fake", 1, "CPUAffinity") >= 0);
+ assert_se(c.set);
+ assert_se(c.allocated >= DIV_ROUND_UP(sizeof(__cpu_mask), 8));
+ assert_se(CPU_ISSET_S(1, c.allocated, c.set));
+ assert_se(CPU_ISSET_S(2, c.allocated, c.set));
+ assert_se(CPU_ISSET_S(4, c.allocated, c.set));
+ assert_se(CPU_COUNT_S(c.allocated, c.set) == 3);
+
+ assert_se(str = cpu_set_to_string(&c));
+ log_info("cpu_set_to_string: %s", str);
+ str = mfree(str);
+ assert_se(str = cpu_set_to_range_string(&c));
+ log_info("cpu_set_to_range_string: %s", str);
+ assert_se(streq(str, "1-2 4"));
+ str = mfree(str);
+ cpu_set_reset(&c);
+
+ /* A more interesting range */
+ assert_se(parse_cpu_set_full("0 1 2 3 8 9 10 11", &c, true, NULL, "fake", 1, "CPUAffinity") >= 0);
+ assert_se(c.allocated >= DIV_ROUND_UP(sizeof(__cpu_mask), 8));
+ assert_se(CPU_COUNT_S(c.allocated, c.set) == 8);
+ for (cpu = 0; cpu < 4; cpu++)
+ assert_se(CPU_ISSET_S(cpu, c.allocated, c.set));
+ for (cpu = 8; cpu < 12; cpu++)
+ assert_se(CPU_ISSET_S(cpu, c.allocated, c.set));
+
+ assert_se(str = cpu_set_to_string(&c));
+ log_info("cpu_set_to_string: %s", str);
+ str = mfree(str);
+ assert_se(str = cpu_set_to_range_string(&c));
+ log_info("cpu_set_to_range_string: %s", str);
+ assert_se(streq(str, "0-3 8-11"));
+ str = mfree(str);
+ cpu_set_reset(&c);
+
+ /* Quoted strings */
+ assert_se(parse_cpu_set_full("8 '9' 10 \"11\"", &c, true, NULL, "fake", 1, "CPUAffinity") >= 0);
+ assert_se(c.allocated >= DIV_ROUND_UP(sizeof(__cpu_mask), 8));
+ assert_se(CPU_COUNT_S(c.allocated, c.set) == 4);
+ for (cpu = 8; cpu < 12; cpu++)
+ assert_se(CPU_ISSET_S(cpu, c.allocated, c.set));
+ assert_se(str = cpu_set_to_string(&c));
+ log_info("cpu_set_to_string: %s", str);
+ str = mfree(str);
+ assert_se(str = cpu_set_to_range_string(&c));
+ log_info("cpu_set_to_range_string: %s", str);
+ assert_se(streq(str, "8-11"));
+ str = mfree(str);
+ cpu_set_reset(&c);
+
+ /* Use commas as separators */
+ assert_se(parse_cpu_set_full("0,1,2,3 8,9,10,11", &c, true, NULL, "fake", 1, "CPUAffinity") >= 0);
+ assert_se(c.allocated >= DIV_ROUND_UP(sizeof(__cpu_mask), 8));
+ assert_se(CPU_COUNT_S(c.allocated, c.set) == 8);
+ for (cpu = 0; cpu < 4; cpu++)
+ assert_se(CPU_ISSET_S(cpu, c.allocated, c.set));
+ for (cpu = 8; cpu < 12; cpu++)
+ assert_se(CPU_ISSET_S(cpu, c.allocated, c.set));
+ assert_se(str = cpu_set_to_string(&c));
+ log_info("cpu_set_to_string: %s", str);
+ str = mfree(str);
+ cpu_set_reset(&c);
+
+ /* Commas with spaces (and trailing comma, space) */
+ assert_se(parse_cpu_set_full("0, 1, 2, 3, 4, 5, 6, 7, 63, ", &c, true, NULL, "fake", 1, "CPUAffinity") >= 0);
+ assert_se(c.allocated >= DIV_ROUND_UP(sizeof(__cpu_mask), 8));
+ assert_se(CPU_COUNT_S(c.allocated, c.set) == 9);
+ for (cpu = 0; cpu < 8; cpu++)
+ assert_se(CPU_ISSET_S(cpu, c.allocated, c.set));
+
+ assert_se(CPU_ISSET_S(63, c.allocated, c.set));
+ assert_se(str = cpu_set_to_string(&c));
+ log_info("cpu_set_to_string: %s", str);
+ str = mfree(str);
+ assert_se(str = cpu_set_to_range_string(&c));
+ log_info("cpu_set_to_range_string: %s", str);
+ assert_se(streq(str, "0-7 63"));
+ str = mfree(str);
+ cpu_set_reset(&c);
+
+ /* Ranges */
+ assert_se(parse_cpu_set_full("0-3,8-11", &c, true, NULL, "fake", 1, "CPUAffinity") >= 0);
+ assert_se(c.allocated >= DIV_ROUND_UP(sizeof(__cpu_mask), 8));
+ assert_se(CPU_COUNT_S(c.allocated, c.set) == 8);
+ for (cpu = 0; cpu < 4; cpu++)
+ assert_se(CPU_ISSET_S(cpu, c.allocated, c.set));
+ for (cpu = 8; cpu < 12; cpu++)
+ assert_se(CPU_ISSET_S(cpu, c.allocated, c.set));
+ assert_se(str = cpu_set_to_string(&c));
+ log_info("cpu_set_to_string: %s", str);
+ str = mfree(str);
+ cpu_set_reset(&c);
+
+ /* Ranges with trailing comma, space */
+ assert_se(parse_cpu_set_full("0-3 8-11, ", &c, true, NULL, "fake", 1, "CPUAffinity") >= 0);
+ assert_se(c.allocated >= DIV_ROUND_UP(sizeof(__cpu_mask), 8));
+ assert_se(CPU_COUNT_S(c.allocated, c.set) == 8);
+ for (cpu = 0; cpu < 4; cpu++)
+ assert_se(CPU_ISSET_S(cpu, c.allocated, c.set));
+ for (cpu = 8; cpu < 12; cpu++)
+ assert_se(CPU_ISSET_S(cpu, c.allocated, c.set));
+ assert_se(str = cpu_set_to_string(&c));
+ log_info("cpu_set_to_string: %s", str);
+ str = mfree(str);
+ assert_se(str = cpu_set_to_range_string(&c));
+ log_info("cpu_set_to_range_string: %s", str);
+ assert_se(streq(str, "0-3 8-11"));
+ str = mfree(str);
+ cpu_set_reset(&c);
+
+ /* Negative range (returns empty cpu_set) */
+ assert_se(parse_cpu_set_full("3-0", &c, true, NULL, "fake", 1, "CPUAffinity") >= 0);
+ assert_se(c.allocated >= DIV_ROUND_UP(sizeof(__cpu_mask), 8));
+ assert_se(CPU_COUNT_S(c.allocated, c.set) == 0);
+ cpu_set_reset(&c);
+
+ /* Overlapping ranges */
+ assert_se(parse_cpu_set_full("0-7 4-11", &c, true, NULL, "fake", 1, "CPUAffinity") >= 0);
+ assert_se(c.allocated >= DIV_ROUND_UP(sizeof(__cpu_mask), 8));
+ assert_se(CPU_COUNT_S(c.allocated, c.set) == 12);
+ for (cpu = 0; cpu < 12; cpu++)
+ assert_se(CPU_ISSET_S(cpu, c.allocated, c.set));
+ assert_se(str = cpu_set_to_string(&c));
+ log_info("cpu_set_to_string: %s", str);
+ str = mfree(str);
+ assert_se(str = cpu_set_to_range_string(&c));
+ log_info("cpu_set_to_range_string: %s", str);
+ assert_se(streq(str, "0-11"));
+ str = mfree(str);
+ cpu_set_reset(&c);
+
+ /* Mix ranges and individual CPUs */
+ assert_se(parse_cpu_set_full("0,2 4-11", &c, true, NULL, "fake", 1, "CPUAffinity") >= 0);
+ assert_se(c.allocated >= DIV_ROUND_UP(sizeof(__cpu_mask), 8));
+ assert_se(CPU_COUNT_S(c.allocated, c.set) == 10);
+ assert_se(CPU_ISSET_S(0, c.allocated, c.set));
+ assert_se(CPU_ISSET_S(2, c.allocated, c.set));
+ for (cpu = 4; cpu < 12; cpu++)
+ assert_se(CPU_ISSET_S(cpu, c.allocated, c.set));
+ assert_se(str = cpu_set_to_string(&c));
+ log_info("cpu_set_to_string: %s", str);
+ str = mfree(str);
+ assert_se(str = cpu_set_to_range_string(&c));
+ log_info("cpu_set_to_range_string: %s", str);
+ assert_se(streq(str, "0 2 4-11"));
+ str = mfree(str);
+ cpu_set_reset(&c);
+
+ /* Garbage */
+ assert_se(parse_cpu_set_full("0 1 2 3 garbage", &c, true, NULL, "fake", 1, "CPUAffinity") == -EINVAL);
+ assert_se(!c.set);
+ assert_se(c.allocated == 0);
+
+ /* Range with garbage */
+ assert_se(parse_cpu_set_full("0-3 8-garbage", &c, true, NULL, "fake", 1, "CPUAffinity") == -EINVAL);
+ assert_se(!c.set);
+ assert_se(c.allocated == 0);
+
+ /* Empty string */
+ assert_se(parse_cpu_set_full("", &c, true, NULL, "fake", 1, "CPUAffinity") == 0);
+ assert_se(!c.set); /* empty string returns NULL */
+ assert_se(c.allocated == 0);
+
+ /* Runaway quoted string */
+ assert_se(parse_cpu_set_full("0 1 2 3 \"4 5 6 7 ", &c, true, NULL, "fake", 1, "CPUAffinity") == -EINVAL);
+ assert_se(!c.set);
+ assert_se(c.allocated == 0);
+
+ /* Maximum allocation */
+ assert_se(parse_cpu_set_full("8000-8191", &c, true, NULL, "fake", 1, "CPUAffinity") == 0);
+ assert_se(CPU_COUNT_S(c.allocated, c.set) == 192);
+ assert_se(str = cpu_set_to_string(&c));
+ log_info("cpu_set_to_string: %s", str);
+ str = mfree(str);
+ assert_se(str = cpu_set_to_range_string(&c));
+ log_info("cpu_set_to_range_string: %s", str);
+ assert_se(streq(str, "8000-8191"));
+ str = mfree(str);
+ cpu_set_reset(&c);
+}
+
+TEST(parse_cpu_set_extend) {
+ CPUSet c = {};
+ _cleanup_free_ char *s1 = NULL, *s2 = NULL;
+
+ assert_se(parse_cpu_set_extend("1 3", &c, true, NULL, "fake", 1, "CPUAffinity") == 1);
+ assert_se(CPU_COUNT_S(c.allocated, c.set) == 2);
+ assert_se(s1 = cpu_set_to_string(&c));
+ log_info("cpu_set_to_string: %s", s1);
+
+ assert_se(parse_cpu_set_extend("4", &c, true, NULL, "fake", 1, "CPUAffinity") == 1);
+ assert_se(CPU_COUNT_S(c.allocated, c.set) == 3);
+ assert_se(s2 = cpu_set_to_string(&c));
+ log_info("cpu_set_to_string: %s", s2);
+
+ assert_se(parse_cpu_set_extend("", &c, true, NULL, "fake", 1, "CPUAffinity") == 0);
+ assert_se(!c.set);
+ assert_se(c.allocated == 0);
+ log_info("cpu_set_to_string: (null)");
+}
+
+TEST(cpu_set_to_from_dbus) {
+ _cleanup_(cpu_set_reset) CPUSet c = {}, c2 = {};
+ _cleanup_free_ char *s = NULL;
+
+ assert_se(parse_cpu_set_extend("1 3 8 100-200", &c, true, NULL, "fake", 1, "CPUAffinity") == 1);
+ assert_se(s = cpu_set_to_string(&c));
+ log_info("cpu_set_to_string: %s", s);
+ assert_se(CPU_COUNT_S(c.allocated, c.set) == 104);
+
+ _cleanup_free_ uint8_t *array = NULL;
+ size_t allocated;
+ static const char expected[32] =
+ "\x0A\x01\x00\x00\x00\x00\x00\x00\x00\x00"
+ "\x00\x00\xF0\xFF\xFF\xFF\xFF\xFF\xFF\xFF"
+ "\xFF\xFF\xFF\xFF\xFF\x01";
+
+ assert_se(cpu_set_to_dbus(&c, &array, &allocated) == 0);
+ assert_se(array);
+ assert_se(allocated == c.allocated);
+
+ assert_se(allocated <= sizeof expected);
+ assert_se(allocated >= DIV_ROUND_UP(201u, 8u)); /* We need at least 201 bits for our mask */
+ assert_se(memcmp(array, expected, allocated) == 0);
+
+ assert_se(cpu_set_from_dbus(array, allocated, &c2) == 0);
+ assert_se(c2.set);
+ assert_se(c2.allocated == c.allocated);
+ assert_se(memcmp(c.set, c2.set, c.allocated) == 0);
+}
+
+TEST(cpus_in_affinity_mask) {
+ int r;
+
+ r = cpus_in_affinity_mask();
+ assert_se(r > 0);
+ log_info("cpus_in_affinity_mask: %d", r);
+}
+
+TEST(print_cpu_alloc_size) {
+ log_info("CPU_ALLOC_SIZE(1) = %zu", CPU_ALLOC_SIZE(1));
+ log_info("CPU_ALLOC_SIZE(9) = %zu", CPU_ALLOC_SIZE(9));
+ log_info("CPU_ALLOC_SIZE(64) = %zu", CPU_ALLOC_SIZE(64));
+ log_info("CPU_ALLOC_SIZE(65) = %zu", CPU_ALLOC_SIZE(65));
+ log_info("CPU_ALLOC_SIZE(1024) = %zu", CPU_ALLOC_SIZE(1024));
+ log_info("CPU_ALLOC_SIZE(1025) = %zu", CPU_ALLOC_SIZE(1025));
+ log_info("CPU_ALLOC_SIZE(8191) = %zu", CPU_ALLOC_SIZE(8191));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-creds.c b/src/test/test-creds.c
new file mode 100644
index 0000000..acb198c
--- /dev/null
+++ b/src/test/test-creds.c
@@ -0,0 +1,121 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "creds-util.h"
+#include "fileio.h"
+#include "path-util.h"
+#include "rm-rf.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+TEST(read_credential_strings) {
+ _cleanup_free_ char *x = NULL, *y = NULL, *saved = NULL, *p = NULL;
+ _cleanup_(rm_rf_physical_and_freep) char *tmp = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+
+ const char *e = getenv("CREDENTIALS_DIRECTORY");
+ if (e)
+ assert_se(saved = strdup(e));
+
+ assert_se(read_credential_strings_many("foo", &x, "bar", &y) == 0);
+ assert_se(x == NULL);
+ assert_se(y == NULL);
+
+ assert_se(mkdtemp_malloc(NULL, &tmp) >= 0);
+
+ assert_se(setenv("CREDENTIALS_DIRECTORY", tmp, /* override= */ true) >= 0);
+
+ assert_se(read_credential_strings_many("foo", &x, "bar", &y) == 0);
+ assert_se(x == NULL);
+ assert_se(y == NULL);
+
+ assert_se(p = path_join(tmp, "bar"));
+ assert_se(write_string_file(p, "piff", WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_AVOID_NEWLINE) >= 0);
+
+ assert_se(read_credential_strings_many("foo", &x, "bar", &y) == 0);
+ assert_se(x == NULL);
+ assert_se(streq(y, "piff"));
+
+ assert_se(write_string_file(p, "paff", WRITE_STRING_FILE_TRUNCATE|WRITE_STRING_FILE_AVOID_NEWLINE) >= 0);
+
+ assert_se(read_credential_strings_many("foo", &x, "bar", &y) == 0);
+ assert_se(x == NULL);
+ assert_se(streq(y, "piff"));
+
+ p = mfree(p);
+ assert_se(p = path_join(tmp, "foo"));
+ assert_se(write_string_file(p, "knurz", WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_AVOID_NEWLINE) >= 0);
+
+ assert_se(read_credential_strings_many("foo", &x, "bar", &y) >= 0);
+ assert_se(streq(x, "knurz"));
+ assert_se(streq(y, "piff"));
+
+ y = mfree(y);
+
+ assert_se(read_credential_strings_many("foo", &x, "bar", &y) >= 0);
+ assert_se(streq(x, "knurz"));
+ assert_se(streq(y, "paff"));
+
+ p = mfree(p);
+ assert_se(p = path_join(tmp, "bazz"));
+ assert_se(f = fopen(p, "w"));
+ assert_se(fwrite("x\0y", 1, 3, f) == 3); /* embedded NUL byte should result in EBADMSG when reading back with read_credential_strings_many() */
+ f = safe_fclose(f);
+
+ assert_se(read_credential_strings_many("bazz", &x, "foo", &y) == -EBADMSG);
+ assert_se(streq(x, "knurz"));
+ assert_se(streq(y, "paff"));
+
+ if (saved)
+ assert_se(setenv("CREDENTIALS_DIRECTORY", saved, /* override= */ 1) >= 0);
+ else
+ assert_se(unsetenv("CREDENTIALS_DIRECTORY") >= 0);
+}
+
+TEST(credential_name_valid) {
+ char buf[NAME_MAX+2];
+
+ assert_se(!credential_name_valid(NULL));
+ assert_se(!credential_name_valid(""));
+ assert_se(!credential_name_valid("."));
+ assert_se(!credential_name_valid(".."));
+ assert_se(!credential_name_valid("foo/bar"));
+ assert_se(credential_name_valid("foo"));
+
+ memset(buf, 'x', sizeof(buf)-1);
+ buf[sizeof(buf)-1] = 0;
+ assert_se(!credential_name_valid(buf));
+
+ buf[sizeof(buf)-2] = 0;
+ assert_se(credential_name_valid(buf));
+}
+
+TEST(credential_glob_valid) {
+ char buf[NAME_MAX+2];
+
+ assert_se(!credential_glob_valid(NULL));
+ assert_se(!credential_glob_valid(""));
+ assert_se(!credential_glob_valid("."));
+ assert_se(!credential_glob_valid(".."));
+ assert_se(!credential_glob_valid("foo/bar"));
+ assert_se(credential_glob_valid("foo"));
+ assert_se(credential_glob_valid("foo*"));
+ assert_se(credential_glob_valid("x*"));
+ assert_se(credential_glob_valid("*"));
+ assert_se(!credential_glob_valid("?"));
+ assert_se(!credential_glob_valid("*a"));
+ assert_se(!credential_glob_valid("a?"));
+ assert_se(!credential_glob_valid("a[abc]"));
+ assert_se(!credential_glob_valid("a[abc]"));
+
+ memset(buf, 'x', sizeof(buf)-1);
+ buf[sizeof(buf)-1] = 0;
+ assert_se(!credential_glob_valid(buf));
+
+ buf[sizeof(buf)-2] = 0;
+ assert_se(credential_glob_valid(buf));
+
+ buf[sizeof(buf)-2] = '*';
+ assert_se(credential_glob_valid(buf));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-cryptolib.c b/src/test/test-cryptolib.c
new file mode 100644
index 0000000..6202a5d
--- /dev/null
+++ b/src/test/test-cryptolib.c
@@ -0,0 +1,38 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "gcrypt-util.h"
+#include "macro.h"
+#include "openssl-util.h"
+#include "string-util.h"
+#include "tests.h"
+
+TEST(string_hashsum) {
+ _cleanup_free_ char *out1 = NULL, *out2 = NULL, *out3 = NULL, *out4 = NULL;
+
+ assert_se(string_hashsum("asdf", 4,
+ OPENSSL_OR_GCRYPT("SHA224", GCRY_MD_SHA224),
+ &out1) == 0);
+ /* echo -n 'asdf' | sha224sum - */
+ assert_se(streq(out1, "7872a74bcbf298a1e77d507cd95d4f8d96131cbbd4cdfc571e776c8a"));
+
+ assert_se(string_hashsum("asdf", 4,
+ OPENSSL_OR_GCRYPT("SHA256", GCRY_MD_SHA256),
+ &out2) == 0);
+ /* echo -n 'asdf' | sha256sum - */
+ assert_se(streq(out2, "f0e4c2f76c58916ec258f246851bea091d14d4247a2fc3e18694461b1816e13b"));
+
+ assert_se(string_hashsum("", 0,
+ OPENSSL_OR_GCRYPT("SHA224", GCRY_MD_SHA224),
+ &out3) == 0);
+ /* echo -n '' | sha224sum - */
+ assert_se(streq(out3, "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f"));
+
+ assert_se(string_hashsum("", 0,
+ OPENSSL_OR_GCRYPT("SHA256", GCRY_MD_SHA256),
+ &out4) == 0);
+ /* echo -n '' | sha256sum - */
+ assert_se(streq(out4, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-daemon.c b/src/test/test-daemon.c
new file mode 100644
index 0000000..b880521
--- /dev/null
+++ b/src/test/test-daemon.c
@@ -0,0 +1,60 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <unistd.h>
+
+#include "sd-daemon.h"
+
+#include "parse-util.h"
+#include "strv.h"
+#include "time-util.h"
+#include "tests.h"
+
+int main(int argc, char *argv[]) {
+ _cleanup_strv_free_ char **l = NULL;
+ int n, i;
+ usec_t duration = USEC_PER_SEC / 10;
+
+ test_setup_logging(LOG_DEBUG);
+
+ if (argc >= 2) {
+ unsigned x;
+
+ assert_se(safe_atou(argv[1], &x) >= 0);
+ duration = x * USEC_PER_SEC;
+ }
+
+ n = sd_listen_fds_with_names(false, &l);
+ if (n < 0) {
+ log_error_errno(n, "Failed to get listening fds: %m");
+ return EXIT_FAILURE;
+ }
+
+ for (i = 0; i < n; i++)
+ log_info("fd=%i name=%s", SD_LISTEN_FDS_START + i, l[i]);
+
+ sd_notify(0,
+ "STATUS=Starting up");
+ usleep_safe(duration);
+
+ sd_notify(0,
+ "STATUS=Running\n"
+ "READY=1");
+ usleep_safe(duration);
+
+ sd_notify(0,
+ "STATUS=Reloading\n"
+ "RELOADING=1");
+ usleep_safe(duration);
+
+ sd_notify(0,
+ "STATUS=Running\n"
+ "READY=1");
+ usleep_safe(duration);
+
+ sd_notify(0,
+ "STATUS=Quitting\n"
+ "STOPPING=1");
+ usleep_safe(duration);
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/test/test-data-fd-util.c b/src/test/test-data-fd-util.c
new file mode 100644
index 0000000..aa68132
--- /dev/null
+++ b/src/test/test-data-fd-util.c
@@ -0,0 +1,148 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "data-fd-util.h"
+#include "fd-util.h"
+#include "memory-util.h"
+#include "process-util.h"
+#include "tests.h"
+#include "random-util.h"
+
+static void test_acquire_data_fd_one(unsigned flags) {
+ char wbuffer[196*1024 - 7];
+ char rbuffer[sizeof(wbuffer)];
+ int fd;
+
+ fd = acquire_data_fd("foo", 3, flags);
+ assert_se(fd >= 0);
+
+ zero(rbuffer);
+ assert_se(read(fd, rbuffer, sizeof(rbuffer)) == 3);
+ assert_se(streq(rbuffer, "foo"));
+
+ fd = safe_close(fd);
+
+ fd = acquire_data_fd("", 0, flags);
+ assert_se(fd >= 0);
+
+ zero(rbuffer);
+ assert_se(read(fd, rbuffer, sizeof(rbuffer)) == 0);
+ assert_se(streq(rbuffer, ""));
+
+ fd = safe_close(fd);
+
+ random_bytes(wbuffer, sizeof(wbuffer));
+
+ fd = acquire_data_fd(wbuffer, sizeof(wbuffer), flags);
+ assert_se(fd >= 0);
+
+ zero(rbuffer);
+ assert_se(read(fd, rbuffer, sizeof(rbuffer)) == sizeof(rbuffer));
+ assert_se(memcmp(rbuffer, wbuffer, sizeof(rbuffer)) == 0);
+
+ fd = safe_close(fd);
+}
+
+TEST(acquire_data_fd) {
+ test_acquire_data_fd_one(0);
+ test_acquire_data_fd_one(ACQUIRE_NO_DEV_NULL);
+ test_acquire_data_fd_one(ACQUIRE_NO_MEMFD);
+ test_acquire_data_fd_one(ACQUIRE_NO_DEV_NULL|ACQUIRE_NO_MEMFD);
+ test_acquire_data_fd_one(ACQUIRE_NO_PIPE);
+ test_acquire_data_fd_one(ACQUIRE_NO_DEV_NULL|ACQUIRE_NO_PIPE);
+ test_acquire_data_fd_one(ACQUIRE_NO_MEMFD|ACQUIRE_NO_PIPE);
+ test_acquire_data_fd_one(ACQUIRE_NO_DEV_NULL|ACQUIRE_NO_MEMFD|ACQUIRE_NO_PIPE);
+ test_acquire_data_fd_one(ACQUIRE_NO_DEV_NULL|ACQUIRE_NO_MEMFD|ACQUIRE_NO_PIPE|ACQUIRE_NO_TMPFILE);
+}
+
+static void assert_equal_fd(int fd1, int fd2) {
+ for (;;) {
+ uint8_t a[4096], b[4096];
+ ssize_t x, y;
+
+ x = read(fd1, a, sizeof(a));
+ assert_se(x >= 0);
+
+ y = read(fd2, b, sizeof(b));
+ assert_se(y >= 0);
+
+ assert_se(x == y);
+
+ if (x == 0)
+ break;
+
+ assert_se(memcmp(a, b, x) == 0);
+ }
+}
+
+TEST(copy_data_fd) {
+ _cleanup_close_ int fd1 = -EBADF, fd2 = -EBADF;
+ _cleanup_close_pair_ int sfd[2] = EBADF_PAIR;
+ _cleanup_(sigkill_waitp) pid_t pid = -1;
+ int r;
+
+ fd1 = open("/etc/fstab", O_RDONLY|O_CLOEXEC);
+ if (fd1 >= 0) {
+
+ fd2 = copy_data_fd(fd1);
+ assert_se(fd2 >= 0);
+
+ assert_se(lseek(fd1, 0, SEEK_SET) == 0);
+ assert_equal_fd(fd1, fd2);
+ }
+
+ fd1 = safe_close(fd1);
+ fd2 = safe_close(fd2);
+
+ fd1 = acquire_data_fd("hallo", 6, 0);
+ assert_se(fd1 >= 0);
+
+ fd2 = copy_data_fd(fd1);
+ assert_se(fd2 >= 0);
+
+ safe_close(fd1);
+ fd1 = acquire_data_fd("hallo", 6, 0);
+ assert_se(fd1 >= 0);
+
+ assert_equal_fd(fd1, fd2);
+
+ fd1 = safe_close(fd1);
+ fd2 = safe_close(fd2);
+
+ assert_se(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, sfd) >= 0);
+
+ r = safe_fork("(sd-pipe)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_LOG, &pid);
+ assert_se(r >= 0);
+
+ if (r == 0) {
+ /* child */
+
+ sfd[0] = safe_close(sfd[0]);
+
+ for (uint64_t i = 0; i < 1536*1024 / sizeof(uint64_t); i++)
+ assert_se(write(sfd[1], &i, sizeof(i)) == sizeof(i));
+
+ sfd[1] = safe_close(sfd[1]);
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ sfd[1] = safe_close(sfd[1]);
+
+ fd2 = copy_data_fd(sfd[0]);
+ assert_se(fd2 >= 0);
+
+ uint64_t j;
+ for (uint64_t i = 0; i < 1536*1024 / sizeof(uint64_t); i++) {
+ assert_se(read(fd2, &j, sizeof(j)) == sizeof(j));
+ assert_se(i == j);
+ }
+
+ assert_se(read(fd2, &j, sizeof(j)) == 0);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-date.c b/src/test/test-date.c
new file mode 100644
index 0000000..162ac34
--- /dev/null
+++ b/src/test/test-date.c
@@ -0,0 +1,112 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "string-util.h"
+#include "tests.h"
+#include "time-util.h"
+
+static void test_should_pass(const char *p) {
+ usec_t t, q;
+ char buf[FORMAT_TIMESTAMP_MAX], buf_relative[FORMAT_TIMESTAMP_RELATIVE_MAX];
+
+ log_info("Test: %s", p);
+ assert_se(parse_timestamp(p, &t) >= 0);
+ assert_se(format_timestamp_style(buf, sizeof(buf), t, TIMESTAMP_US));
+ log_info("\"%s\" → \"%s\"", p, buf);
+
+ assert_se(parse_timestamp(buf, &q) >= 0);
+ if (q != t)
+ log_error("round-trip failed: \"%s\" → \"%s\"",
+ buf, FORMAT_TIMESTAMP_STYLE(q, TIMESTAMP_US));
+ assert_se(q == t);
+
+ assert_se(format_timestamp_relative(buf_relative, sizeof(buf_relative), t));
+ log_info("%s", strna(buf_relative));
+}
+
+static void test_should_parse(const char *p) {
+ usec_t t;
+
+ log_info("Test: %s", p);
+ assert_se(parse_timestamp(p, &t) >= 0);
+ log_info("\"%s\" → \"@%" PRI_USEC "\"", p, t);
+}
+
+static void test_should_fail(const char *p) {
+ usec_t t;
+ int r;
+
+ log_info("Test: %s", p);
+ r = parse_timestamp(p, &t);
+ if (r >= 0)
+ log_info("\"%s\" → \"@%" PRI_USEC "\" (unexpected)", p, t);
+ else
+ log_info("parse_timestamp() returns %d (expected)", r);
+ assert_se(r < 0);
+}
+
+static void test_one(const char *p) {
+ _cleanup_free_ char *with_utc = NULL;
+
+ with_utc = strjoin(p, " UTC");
+ test_should_pass(p);
+ test_should_pass(with_utc);
+}
+
+static void test_one_noutc(const char *p) {
+ _cleanup_free_ char *with_utc = NULL;
+
+ with_utc = strjoin(p, " UTC");
+ test_should_pass(p);
+ test_should_fail(with_utc);
+}
+
+int main(int argc, char *argv[]) {
+ /* Tests have hard-coded results that do not expect a specific timezone to be set by the caller */
+ assert_se(unsetenv("TZ") >= 0);
+
+ test_setup_logging(LOG_DEBUG);
+
+ test_one("17:41");
+ test_one("18:42:44");
+ test_one("18:42:44.0");
+ test_one("18:42:44.999999999999");
+ test_one("12-10-02 12:13:14");
+ test_one("12-10-2 12:13:14");
+ test_one("12-10-03 12:13");
+ test_one("2012-12-30 18:42");
+ test_one("2012-10-02");
+ test_one("Mar 12 12:01:01");
+ test_one("Mar 12 12:01:01.687197");
+ test_one("Tue 2012-10-02");
+ test_one("yesterday");
+ test_one("today");
+ test_one("tomorrow");
+ test_one_noutc("16:20 UTC");
+ test_one_noutc("16:20 Asia/Seoul");
+ test_one_noutc("tomorrow Asia/Seoul");
+ test_one_noutc("2012-12-30 18:42 Asia/Seoul");
+ test_one_noutc("now");
+ test_one_noutc("+2d");
+ test_one_noutc("+2y 4d");
+ test_one_noutc("5months ago");
+ test_one_noutc("@1395716396");
+ test_should_parse("1970-1-1 UTC");
+ test_should_pass("1970-1-1 00:00:01 UTC");
+ test_should_fail("1969-12-31 UTC");
+ test_should_fail("-1000y");
+ test_should_fail("today UTC UTC");
+ test_should_fail("now Asia/Seoul");
+ test_should_fail("+2d Asia/Seoul");
+ test_should_fail("@1395716396 Asia/Seoul");
+#if SIZEOF_TIME_T == 8
+ test_should_pass("9999-12-30 23:59:59 UTC");
+ test_should_fail("9999-12-31 00:00:00 UTC");
+ test_should_fail("10000-01-01 00:00:00 UTC");
+#elif SIZEOF_TIME_T == 4
+ test_should_pass("2038-01-18 03:14:07 UTC");
+ test_should_fail("2038-01-18 03:14:08 UTC");
+#endif
+
+ return 0;
+}
diff --git a/src/test/test-dev-setup.c b/src/test/test-dev-setup.c
new file mode 100644
index 0000000..b75576a
--- /dev/null
+++ b/src/test/test-dev-setup.c
@@ -0,0 +1,66 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "capability-util.h"
+#include "dev-setup.h"
+#include "fs-util.h"
+#include "mkdir.h"
+#include "path-util.h"
+#include "rm-rf.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+int main(int argc, char *argv[]) {
+ _cleanup_(rm_rf_physical_and_freep) char *p = NULL;
+ const char *f;
+ struct stat st;
+
+ test_setup_logging(LOG_DEBUG);
+
+ if (have_effective_cap(CAP_DAC_OVERRIDE) <= 0)
+ return log_tests_skipped("missing capability (CAP_DAC_OVERRIDE)");
+
+ assert_se(mkdtemp_malloc("/tmp/test-dev-setupXXXXXX", &p) >= 0);
+
+ f = prefix_roota(p, "/run/systemd");
+ assert_se(mkdir_p(f, 0755) >= 0);
+
+ assert_se(make_inaccessible_nodes(f, 1, 1) >= 0);
+
+ f = prefix_roota(p, "/run/systemd/inaccessible/reg");
+ assert_se(stat(f, &st) >= 0);
+ assert_se(S_ISREG(st.st_mode));
+ assert_se((st.st_mode & 07777) == 0000);
+
+ f = prefix_roota(p, "/run/systemd/inaccessible/dir");
+ assert_se(stat(f, &st) >= 0);
+ assert_se(S_ISDIR(st.st_mode));
+ assert_se((st.st_mode & 07777) == 0000);
+
+ f = prefix_roota(p, "/run/systemd/inaccessible/fifo");
+ assert_se(stat(f, &st) >= 0);
+ assert_se(S_ISFIFO(st.st_mode));
+ assert_se((st.st_mode & 07777) == 0000);
+
+ f = prefix_roota(p, "/run/systemd/inaccessible/sock");
+ assert_se(stat(f, &st) >= 0);
+ assert_se(S_ISSOCK(st.st_mode));
+ assert_se((st.st_mode & 07777) == 0000);
+
+ f = prefix_roota(p, "/run/systemd/inaccessible/chr");
+ if (stat(f, &st) < 0)
+ assert_se(errno == ENOENT);
+ else {
+ assert_se(S_ISCHR(st.st_mode));
+ assert_se((st.st_mode & 07777) == 0000);
+ }
+
+ f = prefix_roota(p, "/run/systemd/inaccessible/blk");
+ if (stat(f, &st) < 0)
+ assert_se(errno == ENOENT);
+ else {
+ assert_se(S_ISBLK(st.st_mode));
+ assert_se((st.st_mode & 07777) == 0000);
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/test/test-device-nodes.c b/src/test/test-device-nodes.c
new file mode 100644
index 0000000..36fa2ce
--- /dev/null
+++ b/src/test/test-device-nodes.c
@@ -0,0 +1,38 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+#include <sys/types.h>
+
+#include "alloc-util.h"
+#include "device-nodes.h"
+#include "string-util.h"
+#include "tests.h"
+
+/* helpers for test_encode_devnode_name */
+static char *do_encode_string(const char *in) {
+ size_t out_len = strlen(in) * 4 + 1;
+ char *out = malloc(out_len);
+
+ assert_se(out);
+ assert_se(encode_devnode_name(in, out, out_len) >= 0);
+ puts(out);
+
+ return out;
+}
+
+static bool expect_encoded_as(const char *in, const char *expected) {
+ _cleanup_free_ char *encoded = do_encode_string(in);
+ return streq(encoded, expected);
+}
+
+TEST(encode_devnode_name) {
+ assert_se(expect_encoded_as("systemd sucks", "systemd\\x20sucks"));
+ assert_se(expect_encoded_as("pinkiepie", "pinkiepie"));
+ assert_se(expect_encoded_as("valíd\\ųtf8", "valíd\\x5cųtf8"));
+ assert_se(expect_encoded_as("s/ash/ng", "s\\x2fash\\x2fng"));
+ assert_se(expect_encoded_as("/", "\\x2f"));
+ assert_se(expect_encoded_as("!", "\\x21"));
+ assert_se(expect_encoded_as("QEMU ", "QEMU\\x20\\x20\\x20\\x20"));
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-devnum-util.c b/src/test/test-devnum-util.c
new file mode 100644
index 0000000..2068e35
--- /dev/null
+++ b/src/test/test-devnum-util.c
@@ -0,0 +1,124 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/stat.h>
+
+#include "devnum-util.h"
+#include "path-util.h"
+#include "stat-util.h"
+#include "tests.h"
+
+TEST(parse_devnum) {
+ dev_t dev;
+
+ assert_se(parse_devnum("", &dev) == -EINVAL);
+ assert_se(parse_devnum("junk", &dev) == -EINVAL);
+ assert_se(parse_devnum("0", &dev) == -EINVAL);
+ assert_se(parse_devnum("5", &dev) == -EINVAL);
+ assert_se(parse_devnum("5:", &dev) == -EINVAL);
+ assert_se(parse_devnum(":5", &dev) == -EINVAL);
+ assert_se(parse_devnum("-1:-1", &dev) == -EINVAL);
+#if SIZEOF_DEV_T < 8
+ assert_se(parse_devnum("4294967295:4294967295", &dev) == -EINVAL);
+#endif
+ assert_se(parse_devnum("8:11", &dev) >= 0 && major(dev) == 8 && minor(dev) == 11);
+ assert_se(parse_devnum("0:0", &dev) >= 0 && major(dev) == 0 && minor(dev) == 0);
+}
+
+TEST(device_major_minor_valid) {
+ /* on glibc dev_t is 64-bit, even though in the kernel it is only 32-bit */
+ assert_cc(sizeof(dev_t) == sizeof(uint64_t));
+
+ assert_se(DEVICE_MAJOR_VALID(0U));
+ assert_se(DEVICE_MINOR_VALID(0U));
+
+ assert_se(DEVICE_MAJOR_VALID(1U));
+ assert_se(DEVICE_MINOR_VALID(1U));
+
+ assert_se(!DEVICE_MAJOR_VALID(-1U));
+ assert_se(!DEVICE_MINOR_VALID(-1U));
+
+ assert_se(DEVICE_MAJOR_VALID(1U << 10));
+ assert_se(DEVICE_MINOR_VALID(1U << 10));
+
+ assert_se(DEVICE_MAJOR_VALID((1U << 12) - 1));
+ assert_se(DEVICE_MINOR_VALID((1U << 20) - 1));
+
+ assert_se(!DEVICE_MAJOR_VALID((1U << 12)));
+ assert_se(!DEVICE_MINOR_VALID((1U << 20)));
+
+ assert_se(!DEVICE_MAJOR_VALID(1U << 25));
+ assert_se(!DEVICE_MINOR_VALID(1U << 25));
+
+ assert_se(!DEVICE_MAJOR_VALID(UINT32_MAX));
+ assert_se(!DEVICE_MINOR_VALID(UINT32_MAX));
+
+ assert_se(!DEVICE_MAJOR_VALID(UINT64_MAX));
+ assert_se(!DEVICE_MINOR_VALID(UINT64_MAX));
+
+ assert_se(DEVICE_MAJOR_VALID(major(0)));
+ assert_se(DEVICE_MINOR_VALID(minor(0)));
+}
+
+static void test_device_path_make_canonical_one(const char *path) {
+ _cleanup_free_ char *resolved = NULL, *raw = NULL;
+ struct stat st;
+ dev_t devno;
+ mode_t mode;
+ int r;
+
+ log_debug("> %s", path);
+
+ if (stat(path, &st) < 0) {
+ assert_se(errno == ENOENT);
+ log_notice("Path %s not found, skipping test", path);
+ return;
+ }
+
+ r = device_path_make_canonical(st.st_mode, st.st_rdev, &resolved);
+ if (r == -ENOENT) {
+ /* maybe /dev/char/x:y and /dev/block/x:y are missing in this test environment, because we
+ * run in a container or so? */
+ log_notice("Device %s cannot be resolved, skipping test", path);
+ return;
+ }
+
+ assert_se(r >= 0);
+ assert_se(path_equal(path, resolved));
+
+ assert_se(device_path_make_major_minor(st.st_mode, st.st_rdev, &raw) >= 0);
+ assert_se(device_path_parse_major_minor(raw, &mode, &devno) >= 0);
+
+ assert_se(st.st_rdev == devno);
+ assert_se((st.st_mode & S_IFMT) == (mode & S_IFMT));
+}
+
+TEST(device_path_make_canonical) {
+ test_device_path_make_canonical_one("/dev/null");
+ test_device_path_make_canonical_one("/dev/zero");
+ test_device_path_make_canonical_one("/dev/full");
+ test_device_path_make_canonical_one("/dev/random");
+ test_device_path_make_canonical_one("/dev/urandom");
+ test_device_path_make_canonical_one("/dev/tty");
+
+ if (is_device_node("/run/systemd/inaccessible/blk") > 0) {
+ test_device_path_make_canonical_one("/run/systemd/inaccessible/chr");
+ test_device_path_make_canonical_one("/run/systemd/inaccessible/blk");
+ }
+}
+
+static void test_devnum_format_str_one(dev_t devnum, const char *s) {
+ dev_t x;
+
+ assert_se(streq(FORMAT_DEVNUM(devnum), s));
+ assert_se(parse_devnum(s, &x) >= 0);
+ assert_se(x == devnum);
+}
+
+TEST(devnum_format_str) {
+ test_devnum_format_str_one(makedev(0, 0), "0:0");
+ test_devnum_format_str_one(makedev(1, 2), "1:2");
+ test_devnum_format_str_one(makedev(99, 100), "99:100");
+ test_devnum_format_str_one(makedev(4095, 1048575), "4095:1048575");
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-dlopen-so.c b/src/test/test-dlopen-so.c
new file mode 100644
index 0000000..e98b8da
--- /dev/null
+++ b/src/test/test-dlopen-so.c
@@ -0,0 +1,76 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <dlfcn.h>
+#include <stdlib.h>
+
+#include "bpf-dlopen.h"
+#include "cryptsetup-util.h"
+#include "elf-util.h"
+#include "idn-util.h"
+#include "libfido2-util.h"
+#include "macro.h"
+#include "main-func.h"
+#include "password-quality-util-passwdqc.h"
+#include "password-quality-util-pwquality.h"
+#include "pcre2-util.h"
+#include "pkcs11-util.h"
+#include "qrcode-util.h"
+#include "tests.h"
+#include "tpm2-util.h"
+
+static int run(int argc, char **argv) {
+ test_setup_logging(LOG_DEBUG);
+
+ /* Try to load each of our weak library dependencies once. This is supposed to help finding cases
+ * where .so versions change and distributions update, but systemd doesn't have the new so names
+ * around yet. */
+
+#if HAVE_LIBIDN2 || HAVE_LIBIDN
+ assert_se(dlopen_idn() >= 0);
+#endif
+
+#if HAVE_LIBCRYPTSETUP
+ assert_se(dlopen_cryptsetup() >= 0);
+#endif
+
+#if HAVE_PASSWDQC
+ assert_se(dlopen_passwdqc() >= 0);
+#endif
+
+#if HAVE_PWQUALITY
+ assert_se(dlopen_pwquality() >= 0);
+#endif
+
+#if HAVE_QRENCODE
+ assert_se(dlopen_qrencode() >= 0);
+#endif
+
+#if HAVE_TPM2
+ assert_se(dlopen_tpm2() >= 0);
+#endif
+
+#if HAVE_LIBFIDO2
+ assert_se(dlopen_libfido2() >= 0);
+#endif
+
+#if HAVE_LIBBPF
+ assert_se(dlopen_bpf() >= 0);
+#endif
+
+#if HAVE_ELFUTILS
+ assert_se(dlopen_dw() >= 0);
+ assert_se(dlopen_elf() >= 0);
+#endif
+
+#if HAVE_PCRE2
+ assert_se(dlopen_pcre2() >= 0);
+#endif
+
+#if HAVE_P11KIT
+ assert_se(dlopen_p11kit() >= 0);
+#endif
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/test/test-dlopen.c b/src/test/test-dlopen.c
new file mode 100644
index 0000000..9c31537
--- /dev/null
+++ b/src/test/test-dlopen.c
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <dlfcn.h>
+#include <stdlib.h>
+
+#include "macro.h"
+
+int main(int argc, char **argv) {
+ void *handles[argc - 1];
+ int i;
+
+ for (i = 0; i < argc - 1; i++)
+ assert_se(handles[i] = dlopen(argv[i + 1], RTLD_NOW));
+
+ for (i--; i >= 0; i--)
+ assert_se(dlclose(handles[i]) == 0);
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/test/test-dns-domain.c b/src/test/test-dns-domain.c
new file mode 100644
index 0000000..6c107e2
--- /dev/null
+++ b/src/test/test-dns-domain.c
@@ -0,0 +1,753 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "dns-domain.h"
+#include "macro.h"
+#include "string-util.h"
+#include "tests.h"
+
+static void test_dns_label_unescape_one(const char *what, const char *expect, size_t buffer_sz, int ret, int ret_ldh) {
+ char buffer[buffer_sz];
+ int r;
+ const char *w = what;
+
+ log_info("%s, %s, %zu, →%d/%d", what, expect, buffer_sz, ret, ret_ldh);
+
+ r = dns_label_unescape(&w, buffer, buffer_sz, 0);
+ assert_se(r == ret);
+ if (r >= 0)
+ assert_se(streq(buffer, expect));
+
+ w = what;
+ r = dns_label_unescape(&w, buffer, buffer_sz, DNS_LABEL_LDH);
+ assert_se(r == ret_ldh);
+ if (r >= 0)
+ assert_se(streq(buffer, expect));
+
+ w = what;
+ r = dns_label_unescape(&w, buffer, buffer_sz, DNS_LABEL_NO_ESCAPES);
+ const int ret_noe = strchr(what, '\\') ? -EINVAL : ret;
+ assert_se(r == ret_noe);
+ if (r >= 0)
+ assert_se(streq(buffer, expect));
+}
+
+TEST(dns_label_unescape) {
+ test_dns_label_unescape_one("hallo", "hallo", 6, 5, 5);
+ test_dns_label_unescape_one("hallo", "hallo", 4, -ENOBUFS, -ENOBUFS);
+ test_dns_label_unescape_one("", "", 10, 0, 0);
+ test_dns_label_unescape_one("hallo\\.foobar", "hallo.foobar", 20, 12, -EINVAL);
+ test_dns_label_unescape_one("hallo.foobar", "hallo", 10, 5, 5);
+ test_dns_label_unescape_one("hallo\n.foobar", "hallo", 20, -EINVAL, -EINVAL);
+ test_dns_label_unescape_one("hallo\\", "hallo", 20, -EINVAL, -EINVAL);
+ test_dns_label_unescape_one("hallo\\032 ", "hallo ", 20, 7, -EINVAL);
+ test_dns_label_unescape_one(".", "", 20, 0, 0);
+ test_dns_label_unescape_one("..", "", 20, -EINVAL, -EINVAL);
+ test_dns_label_unescape_one(".foobar", "", 20, -EINVAL, -EINVAL);
+ test_dns_label_unescape_one("foobar.", "foobar", 20, 6, 6);
+ test_dns_label_unescape_one("foobar..", "foobar", 20, -EINVAL, -EINVAL);
+ test_dns_label_unescape_one("foo-bar", "foo-bar", 20, 7, 7);
+ test_dns_label_unescape_one("foo-", "foo-", 20, 4, -EINVAL);
+ test_dns_label_unescape_one("-foo", "-foo", 20, 4, -EINVAL);
+ test_dns_label_unescape_one("-foo-", "-foo-", 20, 5, -EINVAL);
+ test_dns_label_unescape_one("foo-.", "foo-", 20, 4, -EINVAL);
+ test_dns_label_unescape_one("foo.-", "foo", 20, 3, 3);
+ test_dns_label_unescape_one("foo\\032", "foo ", 20, 4, -EINVAL);
+ test_dns_label_unescape_one("foo\\045", "foo-", 20, 4, -EINVAL);
+ test_dns_label_unescape_one("głąb", "głąb", 20, 6, -EINVAL);
+}
+
+static void test_dns_name_to_wire_format_one(const char *what, const char *expect, size_t buffer_sz, int ret) {
+ uint8_t buffer[buffer_sz];
+ int r;
+
+ log_info("%s, %s, %zu, →%d", what, strnull(expect), buffer_sz, ret);
+
+ r = dns_name_to_wire_format(what, buffer, buffer_sz, false);
+ assert_se(r == ret);
+
+ if (r >= 0) {
+ assert(expect); /* for gcc */
+ assert_se(memcmp(buffer, expect, r) == 0);
+ }
+}
+
+TEST(dns_name_to_wire_format) {
+ static const char out0[] = { 0 };
+ static const char out1[] = { 3, 'f', 'o', 'o', 0 };
+ static const char out2[] = { 5, 'h', 'a', 'l', 'l', 'o', 3, 'f', 'o', 'o', 3, 'b', 'a', 'r', 0 };
+ static const char out3[] = { 4, ' ', 'f', 'o', 'o', 3, 'b', 'a', 'r', 0 };
+ static const char out4[] = { 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 9, 'a', '1', '2', '3', '4', '5', '6', '7', '8',
+ 3, 'a', '1', '2', 0 };
+
+ test_dns_name_to_wire_format_one("", out0, sizeof(out0), sizeof(out0));
+
+ test_dns_name_to_wire_format_one("foo", out1, sizeof(out1), sizeof(out1));
+ test_dns_name_to_wire_format_one("foo", out1, sizeof(out1) + 1, sizeof(out1));
+ test_dns_name_to_wire_format_one("foo", out1, sizeof(out1) - 1, -ENOBUFS);
+
+ test_dns_name_to_wire_format_one("hallo.foo.bar", out2, sizeof(out2), sizeof(out2));
+ test_dns_name_to_wire_format_one("hallo.foo..bar", NULL, 32, -EINVAL);
+
+ test_dns_name_to_wire_format_one("\\032foo.bar", out3, sizeof(out3), sizeof(out3));
+
+ test_dns_name_to_wire_format_one("a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a123", NULL, 500, -EINVAL);
+ test_dns_name_to_wire_format_one("a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12", out4, sizeof(out4), sizeof(out4));
+}
+
+static void test_dns_label_unescape_suffix_one(const char *what, const char *expect1, const char *expect2, size_t buffer_sz, int ret1, int ret2) {
+ char buffer[buffer_sz];
+ const char *label;
+ int r;
+
+ log_info("%s, %s, %s, %zu, %d, %d", what, expect1, expect2, buffer_sz, ret1, ret2);
+
+ label = what + strlen(what);
+
+ r = dns_label_unescape_suffix(what, &label, buffer, buffer_sz);
+ assert_se(r == ret1);
+ if (r >= 0)
+ assert_se(streq(buffer, expect1));
+
+ r = dns_label_unescape_suffix(what, &label, buffer, buffer_sz);
+ assert_se(r == ret2);
+ if (r >= 0)
+ assert_se(streq(buffer, expect2));
+}
+
+TEST(dns_label_unescape_suffix) {
+ test_dns_label_unescape_suffix_one("hallo", "hallo", "", 6, 5, 0);
+ test_dns_label_unescape_suffix_one("hallo", "hallo", "", 4, -ENOBUFS, -ENOBUFS);
+ test_dns_label_unescape_suffix_one("", "", "", 10, 0, 0);
+ test_dns_label_unescape_suffix_one("hallo\\.foobar", "hallo.foobar", "", 20, 12, 0);
+ test_dns_label_unescape_suffix_one("hallo.foobar", "foobar", "hallo", 10, 6, 5);
+ test_dns_label_unescape_suffix_one("hallo.foobar\n", "foobar", "foobar", 20, -EINVAL, -EINVAL);
+ test_dns_label_unescape_suffix_one("hallo\\", "hallo", "hallo", 20, -EINVAL, -EINVAL);
+ test_dns_label_unescape_suffix_one("hallo\\032 ", "hallo ", "", 20, 7, 0);
+ test_dns_label_unescape_suffix_one(".", "", "", 20, 0, 0);
+ test_dns_label_unescape_suffix_one("..", "", "", 20, 0, -EINVAL);
+ test_dns_label_unescape_suffix_one(".foobar", "foobar", "", 20, 6, -EINVAL);
+ test_dns_label_unescape_suffix_one("foobar.", "foobar", "", 20, 6, 0);
+ test_dns_label_unescape_suffix_one("foo\\\\bar", "foo\\bar", "", 20, 7, 0);
+ test_dns_label_unescape_suffix_one("foo.bar", "bar", "foo", 20, 3, 3);
+ test_dns_label_unescape_suffix_one("foo..bar", "bar", "", 20, 3, -EINVAL);
+ test_dns_label_unescape_suffix_one("foo...bar", "bar", "", 20, 3, -EINVAL);
+ test_dns_label_unescape_suffix_one("foo\\.bar", "foo.bar", "", 20, 7, 0);
+ test_dns_label_unescape_suffix_one("foo\\\\.bar", "bar", "foo\\", 20, 3, 4);
+ test_dns_label_unescape_suffix_one("foo\\\\\\.bar", "foo\\.bar", "", 20, 8, 0);
+}
+
+static void test_dns_label_escape_one(const char *what, size_t l, const char *expect, int ret) {
+ _cleanup_free_ char *t = NULL;
+ int r;
+
+ log_info("%s, %zu, %s, →%d", what, l, strnull(expect), ret);
+
+ r = dns_label_escape_new(what, l, &t);
+ assert_se(r == ret);
+
+ if (r < 0)
+ return;
+
+ assert_se(streq_ptr(expect, t));
+}
+
+TEST(dns_label_escape) {
+ test_dns_label_escape_one("", 0, NULL, -EINVAL);
+ test_dns_label_escape_one("hallo", 5, "hallo", 5);
+ test_dns_label_escape_one("hallo", 6, "hallo\\000", 9);
+ test_dns_label_escape_one("hallo hallo.foobar,waldi", 24, "hallo\\032hallo\\.foobar\\044waldi", 31);
+}
+
+static void test_dns_name_normalize_one(const char *what, const char *expect, int ret) {
+ _cleanup_free_ char *t = NULL;
+ int r;
+
+ r = dns_name_normalize(what, 0, &t);
+ assert_se(r == ret);
+
+ if (r < 0)
+ return;
+
+ assert_se(streq_ptr(expect, t));
+}
+
+TEST(dns_name_normalize) {
+ test_dns_name_normalize_one("", ".", 0);
+ test_dns_name_normalize_one("f", "f", 0);
+ test_dns_name_normalize_one("f.waldi", "f.waldi", 0);
+ test_dns_name_normalize_one("f \\032.waldi", "f\\032\\032.waldi", 0);
+ test_dns_name_normalize_one("\\000", "\\000", 0);
+ test_dns_name_normalize_one("..", NULL, -EINVAL);
+ test_dns_name_normalize_one(".foobar", NULL, -EINVAL);
+ test_dns_name_normalize_one("foobar.", "foobar", 0);
+ test_dns_name_normalize_one(".", ".", 0);
+}
+
+static void test_dns_name_equal_one(const char *a, const char *b, int ret) {
+ int r;
+
+ r = dns_name_equal(a, b);
+ assert_se(r == ret);
+
+ r = dns_name_equal(b, a);
+ assert_se(r == ret);
+}
+
+TEST(dns_name_equal) {
+ test_dns_name_equal_one("", "", true);
+ test_dns_name_equal_one("x", "x", true);
+ test_dns_name_equal_one("x", "x.", true);
+ test_dns_name_equal_one("abc.def", "abc.def", true);
+ test_dns_name_equal_one("abc.def", "ABC.def", true);
+ test_dns_name_equal_one("abc.def", "CBA.def", false);
+ test_dns_name_equal_one("", "xxx", false);
+ test_dns_name_equal_one("ab", "a", false);
+ test_dns_name_equal_one("\\000", "\\000", true);
+ test_dns_name_equal_one(".", "", true);
+ test_dns_name_equal_one(".", ".", true);
+ test_dns_name_equal_one("..", "..", -EINVAL);
+}
+
+static void test_dns_name_between_one(const char *a, const char *b, const char *c, int ret) {
+ int r;
+
+ r = dns_name_between(a, b, c);
+ assert_se(r == ret);
+
+ r = dns_name_between(c, b, a);
+ if (ret >= 0)
+ assert_se(r == 0 || dns_name_equal(a, c) > 0);
+ else
+ assert_se(r == ret);
+}
+
+TEST(dns_name_between) {
+ /* see https://tools.ietf.org/html/rfc4034#section-6.1
+ Note that we use "\033.z.example" in stead of "\001.z.example" as we
+ consider the latter invalid */
+ test_dns_name_between_one("example", "a.example", "yljkjljk.a.example", true);
+ test_dns_name_between_one("a.example", "yljkjljk.a.example", "Z.a.example", true);
+ test_dns_name_between_one("yljkjljk.a.example", "Z.a.example", "zABC.a.EXAMPLE", true);
+ test_dns_name_between_one("Z.a.example", "zABC.a.EXAMPLE", "z.example", true);
+ test_dns_name_between_one("zABC.a.EXAMPLE", "z.example", "\\033.z.example", true);
+ test_dns_name_between_one("z.example", "\\033.z.example", "*.z.example", true);
+ test_dns_name_between_one("\\033.z.example", "*.z.example", "\\200.z.example", true);
+ test_dns_name_between_one("*.z.example", "\\200.z.example", "example", true);
+ test_dns_name_between_one("\\200.z.example", "example", "a.example", true);
+
+ test_dns_name_between_one("example", "a.example", "example", true);
+ test_dns_name_between_one("example", "example", "example", false);
+ test_dns_name_between_one("example", "example", "yljkjljk.a.example", false);
+ test_dns_name_between_one("example", "yljkjljk.a.example", "yljkjljk.a.example", false);
+ test_dns_name_between_one("hkps.pool.sks-keyservers.net", "_pgpkey-https._tcp.hkps.pool.sks-keyservers.net", "ipv4.pool.sks-keyservers.net", true);
+}
+
+static void test_dns_name_endswith_one(const char *a, const char *b, int ret) {
+ assert_se(dns_name_endswith(a, b) == ret);
+}
+
+TEST(dns_name_endswith) {
+ test_dns_name_endswith_one("", "", true);
+ test_dns_name_endswith_one("", "xxx", false);
+ test_dns_name_endswith_one("xxx", "", true);
+ test_dns_name_endswith_one("x", "x", true);
+ test_dns_name_endswith_one("x", "y", false);
+ test_dns_name_endswith_one("x.y", "y", true);
+ test_dns_name_endswith_one("x.y", "Y", true);
+ test_dns_name_endswith_one("x.y", "x", false);
+ test_dns_name_endswith_one("x.y.z", "Z", true);
+ test_dns_name_endswith_one("x.y.z", "y.Z", true);
+ test_dns_name_endswith_one("x.y.z", "x.y.Z", true);
+ test_dns_name_endswith_one("x.y.z", "waldo", false);
+ test_dns_name_endswith_one("x.y.z.u.v.w", "y.z", false);
+ test_dns_name_endswith_one("x.y.z.u.v.w", "u.v.w", true);
+ test_dns_name_endswith_one("x.y\001.z", "waldo", -EINVAL);
+}
+
+static void test_dns_name_startswith_one(const char *a, const char *b, int ret) {
+ assert_se(dns_name_startswith(a, b) == ret);
+}
+
+TEST(dns_name_startswith) {
+ test_dns_name_startswith_one("", "", true);
+ test_dns_name_startswith_one("", "xxx", false);
+ test_dns_name_startswith_one("xxx", "", true);
+ test_dns_name_startswith_one("x", "x", true);
+ test_dns_name_startswith_one("x", "y", false);
+ test_dns_name_startswith_one("x.y", "x.y", true);
+ test_dns_name_startswith_one("x.y", "y.x", false);
+ test_dns_name_startswith_one("x.y", "x", true);
+ test_dns_name_startswith_one("x.y", "X", true);
+ test_dns_name_startswith_one("x.y", "y", false);
+ test_dns_name_startswith_one("x.y", "", true);
+ test_dns_name_startswith_one("x.y", "X", true);
+}
+
+TEST(dns_name_is_root) {
+ assert_se(dns_name_is_root(""));
+ assert_se(dns_name_is_root("."));
+ assert_se(!dns_name_is_root("xxx"));
+ assert_se(!dns_name_is_root("xxx."));
+ assert_se(!dns_name_is_root(".."));
+}
+
+TEST(dns_name_is_single_label) {
+ assert_se(!dns_name_is_single_label(""));
+ assert_se(!dns_name_is_single_label("."));
+ assert_se(!dns_name_is_single_label(".."));
+ assert_se(dns_name_is_single_label("x"));
+ assert_se(dns_name_is_single_label("x."));
+ assert_se(!dns_name_is_single_label("xx.yy"));
+}
+
+static void test_dns_name_reverse_one(const char *address, const char *name) {
+ _cleanup_free_ char *p = NULL;
+ union in_addr_union a, b = {};
+ int familya, familyb;
+
+ assert_se(in_addr_from_string_auto(address, &familya, &a) >= 0);
+ assert_se(dns_name_reverse(familya, &a, &p) >= 0);
+ assert_se(streq(p, name));
+ assert_se(dns_name_address(p, &familyb, &b) > 0);
+ assert_se(familya == familyb);
+ assert_se(in_addr_equal(familya, &a, &b));
+}
+
+TEST(dns_name_reverse) {
+ test_dns_name_reverse_one("47.11.8.15", "15.8.11.47.in-addr.arpa");
+ test_dns_name_reverse_one("fe80::47", "7.4.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.e.f.ip6.arpa");
+ test_dns_name_reverse_one("127.0.0.1", "1.0.0.127.in-addr.arpa");
+ test_dns_name_reverse_one("::1", "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa");
+}
+
+static void test_dns_name_concat_one(const char *a, const char *b, int r, const char *result) {
+ _cleanup_free_ char *p = NULL;
+
+ assert_se(dns_name_concat(a, b, 0, &p) == r);
+ assert_se(streq_ptr(p, result));
+}
+
+TEST(dns_name_concat) {
+ test_dns_name_concat_one("", "", 0, ".");
+ test_dns_name_concat_one(".", "", 0, ".");
+ test_dns_name_concat_one("", ".", 0, ".");
+ test_dns_name_concat_one(".", ".", 0, ".");
+ test_dns_name_concat_one("foo", "bar", 0, "foo.bar");
+ test_dns_name_concat_one("foo.foo", "bar.bar", 0, "foo.foo.bar.bar");
+ test_dns_name_concat_one("foo", NULL, 0, "foo");
+ test_dns_name_concat_one("foo", ".", 0, "foo");
+ test_dns_name_concat_one("foo.", "bar.", 0, "foo.bar");
+ test_dns_name_concat_one(NULL, NULL, 0, ".");
+ test_dns_name_concat_one(NULL, ".", 0, ".");
+ test_dns_name_concat_one(NULL, "foo", 0, "foo");
+}
+
+static void test_dns_name_is_valid_one(const char *s, int ret, int ret_ldh) {
+ log_info("%s, →%d", s, ret);
+
+ assert_se(dns_name_is_valid(s) == ret);
+ assert_se(dns_name_is_valid_ldh(s) == ret_ldh);
+}
+
+TEST(dns_name_is_valid) {
+ test_dns_name_is_valid_one("foo", 1, 1);
+ test_dns_name_is_valid_one("foo.", 1, 1);
+ test_dns_name_is_valid_one("foo..", 0, 0);
+ test_dns_name_is_valid_one("Foo", 1, 1);
+ test_dns_name_is_valid_one("foo.bar", 1, 1);
+ test_dns_name_is_valid_one("foo.bar.baz", 1, 1);
+ test_dns_name_is_valid_one("", 1, 1);
+ test_dns_name_is_valid_one("foo..bar", 0, 0);
+ test_dns_name_is_valid_one(".foo.bar", 0, 0);
+ test_dns_name_is_valid_one("foo.bar.", 1, 1);
+ test_dns_name_is_valid_one("foo.bar..", 0, 0);
+ test_dns_name_is_valid_one("\\zbar", 0, 0);
+ test_dns_name_is_valid_one("ä", 1, 0);
+ test_dns_name_is_valid_one("\n", 0, 0);
+
+ test_dns_name_is_valid_one("dash-", 1, 0);
+ test_dns_name_is_valid_one("-dash", 1, 0);
+ test_dns_name_is_valid_one("dash-dash", 1, 1);
+ test_dns_name_is_valid_one("foo.dash-", 1, 0);
+ test_dns_name_is_valid_one("foo.-dash", 1, 0);
+ test_dns_name_is_valid_one("foo.dash-dash", 1, 1);
+ test_dns_name_is_valid_one("foo.dash-.bar", 1, 0);
+ test_dns_name_is_valid_one("foo.-dash.bar", 1, 0);
+ test_dns_name_is_valid_one("foo.dash-dash.bar", 1, 1);
+ test_dns_name_is_valid_one("dash-.bar", 1, 0);
+ test_dns_name_is_valid_one("-dash.bar", 1, 0);
+ test_dns_name_is_valid_one("dash-dash.bar", 1, 1);
+ test_dns_name_is_valid_one("-.bar", 1, 0);
+ test_dns_name_is_valid_one("foo.-", 1, 0);
+
+ /* 256 characters */
+ test_dns_name_is_valid_one("a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345", 0, 0);
+
+ /* 255 characters */
+ test_dns_name_is_valid_one("a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a1234", 0, 0);
+
+ /* 254 characters */
+ test_dns_name_is_valid_one("a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a123", 0, 0);
+
+ /* 253 characters */
+ test_dns_name_is_valid_one("a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12345678.a12", 1, 1);
+
+ /* label of 64 chars length */
+ test_dns_name_is_valid_one("a123456789a123456789a123456789a123456789a123456789a123456789a123", 0, 0);
+
+ /* label of 63 chars length */
+ test_dns_name_is_valid_one("a123456789a123456789a123456789a123456789a123456789a123456789a12", 1, 1);
+}
+
+TEST(dns_service_name_is_valid) {
+ assert_se(dns_service_name_is_valid("Lennart's Compüter"));
+ assert_se(dns_service_name_is_valid("piff.paff"));
+
+ assert_se(!dns_service_name_is_valid(NULL));
+ assert_se(!dns_service_name_is_valid(""));
+ assert_se(!dns_service_name_is_valid("foo\nbar"));
+ assert_se(!dns_service_name_is_valid("foo\201bar"));
+ assert_se(!dns_service_name_is_valid("this is an overly long string that is certainly longer than 63 characters"));
+}
+
+TEST(dns_srv_type_is_valid) {
+ assert_se(dns_srv_type_is_valid("_http._tcp"));
+ assert_se(dns_srv_type_is_valid("_foo-bar._tcp"));
+ assert_se(dns_srv_type_is_valid("_w._udp"));
+ assert_se(dns_srv_type_is_valid("_a800._tcp"));
+ assert_se(dns_srv_type_is_valid("_a-800._tcp"));
+
+ assert_se(!dns_srv_type_is_valid(NULL));
+ assert_se(!dns_srv_type_is_valid(""));
+ assert_se(!dns_srv_type_is_valid("x"));
+ assert_se(!dns_srv_type_is_valid("_foo"));
+ assert_se(!dns_srv_type_is_valid("_tcp"));
+ assert_se(!dns_srv_type_is_valid("_"));
+ assert_se(!dns_srv_type_is_valid("_foo."));
+ assert_se(!dns_srv_type_is_valid("_föo._tcp"));
+ assert_se(!dns_srv_type_is_valid("_f\no._tcp"));
+ assert_se(!dns_srv_type_is_valid("_800._tcp"));
+ assert_se(!dns_srv_type_is_valid("_-800._tcp"));
+ assert_se(!dns_srv_type_is_valid("_-foo._tcp"));
+ assert_se(!dns_srv_type_is_valid("_piep._foo._udp"));
+}
+
+TEST(dnssd_srv_type_is_valid) {
+ assert_se(dnssd_srv_type_is_valid("_http._tcp"));
+ assert_se(dnssd_srv_type_is_valid("_foo-bar._tcp"));
+ assert_se(dnssd_srv_type_is_valid("_w._udp"));
+ assert_se(dnssd_srv_type_is_valid("_a800._tcp"));
+ assert_se(dnssd_srv_type_is_valid("_a-800._tcp"));
+
+ assert_se(!dnssd_srv_type_is_valid(NULL));
+ assert_se(!dnssd_srv_type_is_valid(""));
+ assert_se(!dnssd_srv_type_is_valid("x"));
+ assert_se(!dnssd_srv_type_is_valid("_foo"));
+ assert_se(!dnssd_srv_type_is_valid("_tcp"));
+ assert_se(!dnssd_srv_type_is_valid("_"));
+ assert_se(!dnssd_srv_type_is_valid("_foo."));
+ assert_se(!dnssd_srv_type_is_valid("_föo._tcp"));
+ assert_se(!dnssd_srv_type_is_valid("_f\no._tcp"));
+ assert_se(!dnssd_srv_type_is_valid("_800._tcp"));
+ assert_se(!dnssd_srv_type_is_valid("_-800._tcp"));
+ assert_se(!dnssd_srv_type_is_valid("_-foo._tcp"));
+ assert_se(!dnssd_srv_type_is_valid("_piep._foo._udp"));
+ assert_se(!dnssd_srv_type_is_valid("_foo._unknown"));
+}
+
+static void test_dns_service_join_one(const char *a, const char *b, const char *c, int r, const char *d) {
+ _cleanup_free_ char *x = NULL, *y = NULL, *z = NULL, *t = NULL;
+
+ log_info("%s, %s, %s, →%d, %s", strnull(a), strnull(b), strnull(c), r, strnull(d));
+
+ assert_se(dns_service_join(a, b, c, &t) == r);
+ assert_se(streq_ptr(t, d));
+
+ if (r < 0)
+ return;
+
+ assert_se(dns_service_split(t, &x, &y, &z) >= 0);
+ assert_se(streq_ptr(a, x));
+ assert_se(streq_ptr(b, y));
+ assert_se(dns_name_equal(c, z) > 0);
+}
+
+TEST(dns_service_join) {
+ test_dns_service_join_one("", "", "", -EINVAL, NULL);
+ test_dns_service_join_one("", "_http._tcp", "", -EINVAL, NULL);
+ test_dns_service_join_one("", "_http._tcp", "foo", -EINVAL, NULL);
+ test_dns_service_join_one("foo", "", "foo", -EINVAL, NULL);
+ test_dns_service_join_one("foo", "foo", "foo", -EINVAL, NULL);
+
+ test_dns_service_join_one("foo", "_http._tcp", "", 0, "foo._http._tcp");
+ test_dns_service_join_one(NULL, "_http._tcp", "", 0, "_http._tcp");
+ test_dns_service_join_one("foo", "_http._tcp", "foo", 0, "foo._http._tcp.foo");
+ test_dns_service_join_one(NULL, "_http._tcp", "foo", 0, "_http._tcp.foo");
+ test_dns_service_join_one("Lennart's PC", "_pc._tcp", "foo.bar.com", 0, "Lennart\\039s\\032PC._pc._tcp.foo.bar.com");
+ test_dns_service_join_one(NULL, "_pc._tcp", "foo.bar.com", 0, "_pc._tcp.foo.bar.com");
+}
+
+static void test_dns_service_split_one(const char *joined, const char *a, const char *b, const char *c, int r) {
+ _cleanup_free_ char *x = NULL, *y = NULL, *z = NULL, *t = NULL;
+
+ log_info("%s, %s, %s, %s, →%d", joined, strnull(a), strnull(b), strnull(c), r);
+
+ assert_se(dns_service_split(joined, &x, &y, &z) == r);
+ assert_se(streq_ptr(x, a));
+ assert_se(streq_ptr(y, b));
+ assert_se(streq_ptr(z, c));
+
+ if (r < 0)
+ return;
+
+ if (y) {
+ assert_se(dns_service_join(x, y, z, &t) == 0);
+ assert_se(dns_name_equal(joined, t) > 0);
+ } else
+ assert_se(!x && dns_name_equal(z, joined) > 0);
+}
+
+TEST(dns_service_split) {
+ test_dns_service_split_one("", NULL, NULL, ".", 0);
+ test_dns_service_split_one("foo", NULL, NULL, "foo", 0);
+ test_dns_service_split_one("foo.bar", NULL, NULL, "foo.bar", 0);
+ test_dns_service_split_one("_foo.bar", NULL, NULL, "_foo.bar", 0);
+ test_dns_service_split_one("_foo._bar", NULL, "_foo._bar", ".", 0);
+ test_dns_service_split_one("_meh._foo._bar", "_meh", "_foo._bar", ".", 0);
+ test_dns_service_split_one("Wuff\\032Wuff._foo._bar.waldo.com", "Wuff Wuff", "_foo._bar", "waldo.com", 0);
+ test_dns_service_split_one("_Q._Q-------------------------------------------------------------", NULL, "_Q._Q-------------------------------------------------------------", ".", 0);
+}
+
+static void test_dns_name_change_suffix_one(const char *name, const char *old_suffix, const char *new_suffix, int r, const char *result) {
+ _cleanup_free_ char *s = NULL;
+
+ log_info("%s, %s, %s, →%s", name, old_suffix, new_suffix, strnull(result));
+
+ assert_se(dns_name_change_suffix(name, old_suffix, new_suffix, &s) == r);
+ assert_se(streq_ptr(s, result));
+}
+
+TEST(dns_name_change_suffix) {
+ test_dns_name_change_suffix_one("foo.bar", "bar", "waldo", 1, "foo.waldo");
+ test_dns_name_change_suffix_one("foo.bar.waldi.quux", "foo.bar.waldi.quux", "piff.paff", 1, "piff.paff");
+ test_dns_name_change_suffix_one("foo.bar.waldi.quux", "bar.waldi.quux", "piff.paff", 1, "foo.piff.paff");
+ test_dns_name_change_suffix_one("foo.bar.waldi.quux", "waldi.quux", "piff.paff", 1, "foo.bar.piff.paff");
+ test_dns_name_change_suffix_one("foo.bar.waldi.quux", "quux", "piff.paff", 1, "foo.bar.waldi.piff.paff");
+ test_dns_name_change_suffix_one("foo.bar.waldi.quux", "", "piff.paff", 1, "foo.bar.waldi.quux.piff.paff");
+ test_dns_name_change_suffix_one("", "", "piff.paff", 1, "piff.paff");
+ test_dns_name_change_suffix_one("", "", "", 1, ".");
+ test_dns_name_change_suffix_one("a", "b", "c", 0, NULL);
+}
+
+static void test_dns_name_suffix_one(const char *name, unsigned n_labels, const char *result, int ret) {
+ const char *p = NULL;
+
+ log_info("%s, %u, → %s, %d", name, n_labels, strnull(result), ret);
+
+ assert_se(ret == dns_name_suffix(name, n_labels, &p));
+ assert_se(streq_ptr(p, result));
+}
+
+TEST(dns_name_suffix) {
+ test_dns_name_suffix_one("foo.bar", 2, "foo.bar", 0);
+ test_dns_name_suffix_one("foo.bar", 1, "bar", 1);
+ test_dns_name_suffix_one("foo.bar", 0, "", 2);
+ test_dns_name_suffix_one("foo.bar", 3, NULL, -EINVAL);
+ test_dns_name_suffix_one("foo.bar", 4, NULL, -EINVAL);
+
+ test_dns_name_suffix_one("bar", 1, "bar", 0);
+ test_dns_name_suffix_one("bar", 0, "", 1);
+ test_dns_name_suffix_one("bar", 2, NULL, -EINVAL);
+ test_dns_name_suffix_one("bar", 3, NULL, -EINVAL);
+
+ test_dns_name_suffix_one("", 0, "", 0);
+ test_dns_name_suffix_one("", 1, NULL, -EINVAL);
+ test_dns_name_suffix_one("", 2, NULL, -EINVAL);
+}
+
+static void test_dns_name_count_labels_one(const char *name, int n) {
+ log_info("%s, →%d", name, n);
+
+ assert_se(dns_name_count_labels(name) == n);
+}
+
+TEST(dns_name_count_labels) {
+ test_dns_name_count_labels_one("foo.bar.quux.", 3);
+ test_dns_name_count_labels_one("foo.bar.quux", 3);
+ test_dns_name_count_labels_one("foo.bar.", 2);
+ test_dns_name_count_labels_one("foo.bar", 2);
+ test_dns_name_count_labels_one("foo.", 1);
+ test_dns_name_count_labels_one("foo", 1);
+ test_dns_name_count_labels_one("", 0);
+ test_dns_name_count_labels_one(".", 0);
+ test_dns_name_count_labels_one("..", -EINVAL);
+}
+
+static void test_dns_name_equal_skip_one(const char *a, unsigned n_labels, const char *b, int ret) {
+ log_info("%s, %u, %s, →%d", a, n_labels, b, ret);
+
+ assert_se(dns_name_equal_skip(a, n_labels, b) == ret);
+}
+
+TEST(dns_name_equal_skip) {
+ test_dns_name_equal_skip_one("foo", 0, "bar", 0);
+ test_dns_name_equal_skip_one("foo", 0, "foo", 1);
+ test_dns_name_equal_skip_one("foo", 1, "foo", 0);
+ test_dns_name_equal_skip_one("foo", 2, "foo", 0);
+
+ test_dns_name_equal_skip_one("foo.bar", 0, "foo.bar", 1);
+ test_dns_name_equal_skip_one("foo.bar", 1, "foo.bar", 0);
+ test_dns_name_equal_skip_one("foo.bar", 2, "foo.bar", 0);
+ test_dns_name_equal_skip_one("foo.bar", 3, "foo.bar", 0);
+
+ test_dns_name_equal_skip_one("foo.bar", 0, "bar", 0);
+ test_dns_name_equal_skip_one("foo.bar", 1, "bar", 1);
+ test_dns_name_equal_skip_one("foo.bar", 2, "bar", 0);
+ test_dns_name_equal_skip_one("foo.bar", 3, "bar", 0);
+
+ test_dns_name_equal_skip_one("foo.bar", 0, "", 0);
+ test_dns_name_equal_skip_one("foo.bar", 1, "", 0);
+ test_dns_name_equal_skip_one("foo.bar", 2, "", 1);
+ test_dns_name_equal_skip_one("foo.bar", 3, "", 0);
+
+ test_dns_name_equal_skip_one("", 0, "", 1);
+ test_dns_name_equal_skip_one("", 1, "", 0);
+ test_dns_name_equal_skip_one("", 1, "foo", 0);
+ test_dns_name_equal_skip_one("", 2, "foo", 0);
+}
+
+TEST(dns_name_compare_func) {
+ assert_se(dns_name_compare_func("", "") == 0);
+ assert_se(dns_name_compare_func("", ".") == 0);
+ assert_se(dns_name_compare_func(".", "") == 0);
+ assert_se(dns_name_compare_func("foo", "foo.") == 0);
+ assert_se(dns_name_compare_func("foo.", "foo") == 0);
+ assert_se(dns_name_compare_func("foo", "foo") == 0);
+ assert_se(dns_name_compare_func("foo.", "foo.") == 0);
+ assert_se(dns_name_compare_func("heise.de", "HEISE.DE.") == 0);
+
+ assert_se(dns_name_compare_func("de.", "heise.de") != 0);
+}
+
+static void test_dns_name_common_suffix_one(const char *a, const char *b, const char *result) {
+ const char *c;
+
+ log_info("%s, %s, →%s", a, b, result);
+
+ assert_se(dns_name_common_suffix(a, b, &c) >= 0);
+ assert_se(streq(c, result));
+}
+
+TEST(dns_name_common_suffix) {
+ test_dns_name_common_suffix_one("", "", "");
+ test_dns_name_common_suffix_one("foo", "", "");
+ test_dns_name_common_suffix_one("", "foo", "");
+ test_dns_name_common_suffix_one("foo", "bar", "");
+ test_dns_name_common_suffix_one("bar", "foo", "");
+ test_dns_name_common_suffix_one("foo", "foo", "foo");
+ test_dns_name_common_suffix_one("quux.foo", "foo", "foo");
+ test_dns_name_common_suffix_one("foo", "quux.foo", "foo");
+ test_dns_name_common_suffix_one("this.is.a.short.sentence", "this.is.another.short.sentence", "short.sentence");
+ test_dns_name_common_suffix_one("FOO.BAR", "tEST.bAR", "BAR");
+}
+
+static void test_dns_name_apply_idna_one(const char *s, int expected, const char *result) {
+ _cleanup_free_ char *buf = NULL;
+ int r;
+
+ r = dns_name_apply_idna(s, &buf);
+ log_debug("dns_name_apply_idna: \"%s\" → %d/\"%s\" (expected %d/\"%s\")",
+ s, r, strnull(buf), expected, strnull(result));
+
+ /* Different libidn2 versions are more and less accepting
+ * of underscore-prefixed names. So let's list the lowest
+ * expected return value. */
+ assert_se(r >= expected);
+ if (expected == 1)
+ assert_se(dns_name_equal(buf, result) == 1);
+}
+
+TEST(dns_name_apply_idna) {
+ const int ret = HAVE_LIBIDN2 | HAVE_LIBIDN;
+
+ /* IDNA2008 forbids names with hyphens in third and fourth positions
+ * (https://tools.ietf.org/html/rfc5891#section-4.2.3.1).
+ * IDNA2003 does not have this restriction
+ * (https://tools.ietf.org/html/rfc3490#section-5).
+ * This means that when using libidn we will transform and test more
+ * labels. If registrars follow IDNA2008 we'll just be performing a
+ * useless lookup.
+ */
+ const int ret2 = HAVE_LIBIDN;
+
+ test_dns_name_apply_idna_one("", ret, "");
+ test_dns_name_apply_idna_one("foo", ret, "foo");
+ test_dns_name_apply_idna_one("foo.", ret, "foo");
+ test_dns_name_apply_idna_one("foo.bar", ret, "foo.bar");
+ test_dns_name_apply_idna_one("foo.bar.", ret, "foo.bar");
+ test_dns_name_apply_idna_one("föö", ret, "xn--f-1gaa");
+ test_dns_name_apply_idna_one("föö.", ret, "xn--f-1gaa");
+ test_dns_name_apply_idna_one("föö.bär", ret, "xn--f-1gaa.xn--br-via");
+ test_dns_name_apply_idna_one("föö.bär.", ret, "xn--f-1gaa.xn--br-via");
+ test_dns_name_apply_idna_one("xn--f-1gaa.xn--br-via", ret, "xn--f-1gaa.xn--br-via");
+
+ test_dns_name_apply_idna_one("_443._tcp.fedoraproject.org", ret2,
+ "_443._tcp.fedoraproject.org");
+ test_dns_name_apply_idna_one("_443", ret2, "_443");
+ test_dns_name_apply_idna_one("gateway", ret, "gateway");
+ test_dns_name_apply_idna_one("_gateway", ret2, "_gateway");
+
+ test_dns_name_apply_idna_one("r3---sn-ab5l6ne7.googlevideo.com", ret2,
+ ret2 ? "r3---sn-ab5l6ne7.googlevideo.com" : "");
+}
+
+TEST(dns_name_is_valid_or_address) {
+ assert_se(dns_name_is_valid_or_address(NULL) == 0);
+ assert_se(dns_name_is_valid_or_address("") == 0);
+ assert_se(dns_name_is_valid_or_address("foobar") > 0);
+ assert_se(dns_name_is_valid_or_address("foobar.com") > 0);
+ assert_se(dns_name_is_valid_or_address("foobar..com") == 0);
+ assert_se(dns_name_is_valid_or_address("foobar.com.") > 0);
+ assert_se(dns_name_is_valid_or_address("127.0.0.1") > 0);
+ assert_se(dns_name_is_valid_or_address("::") > 0);
+ assert_se(dns_name_is_valid_or_address("::1") > 0);
+}
+
+TEST(dns_name_dot_suffixed) {
+ assert_se(dns_name_dot_suffixed("") == 0);
+ assert_se(dns_name_dot_suffixed(".") > 0);
+ assert_se(dns_name_dot_suffixed("foo") == 0);
+ assert_se(dns_name_dot_suffixed("foo.") > 0);
+ assert_se(dns_name_dot_suffixed("foo\\..") > 0);
+ assert_se(dns_name_dot_suffixed("foo\\.") == 0);
+ assert_se(dns_name_dot_suffixed("foo.bar.") > 0);
+ assert_se(dns_name_dot_suffixed("foo.bar\\.\\.\\..") > 0);
+ assert_se(dns_name_dot_suffixed("foo.bar\\.\\.\\.\\.") == 0);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-ellipsize.c b/src/test/test-ellipsize.c
new file mode 100644
index 0000000..c272c56
--- /dev/null
+++ b/src/test/test-ellipsize.c
@@ -0,0 +1,159 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+
+#include "alloc-util.h"
+#include "constants.h"
+#include "escape.h"
+#include "string-util.h"
+#include "strv.h"
+#include "terminal-util.h"
+#include "tests.h"
+#include "utf8.h"
+
+static void test_ellipsize_mem_one(const char *s, size_t old_length, size_t new_length) {
+ _cleanup_free_ char *n = NULL;
+ _cleanup_free_ char *t1 = NULL, *t2 = NULL, *t3 = NULL;
+ char buf[LINE_MAX];
+ bool has_wide_chars;
+ size_t max_width;
+
+ n = memdup_suffix0(s, old_length);
+
+ if (!utf8_is_valid(n))
+ /* We don't support invalid sequences… */
+ return;
+
+ /* Report out inputs. We duplicate the data so that cellescape
+ * can properly report truncated multibyte sequences. */
+ log_info("%s \"%s\" old_length=%zu/%zu new_length=%zu", __func__,
+ cellescape(buf, sizeof buf, n),
+ old_length, utf8_console_width(n),
+ new_length);
+
+ /* To keep this test simple, any case with wide chars starts with this glyph */
+ has_wide_chars = startswith(s, "你");
+ max_width = MIN(utf8_console_width(n), new_length);
+
+ t1 = ellipsize_mem(n, old_length, new_length, 30);
+ log_info("30%% → %s utf8_console_width=%zu", t1, utf8_console_width(t1));
+ if (!has_wide_chars)
+ assert_se(utf8_console_width(t1) == max_width);
+ else
+ assert_se(utf8_console_width(t1) <= max_width);
+
+ t2 = ellipsize_mem(n, old_length, new_length, 90);
+ log_info("90%% → %s utf8_console_width=%zu", t2, utf8_console_width(t2));
+ if (!has_wide_chars)
+ assert_se(utf8_console_width(t2) == max_width);
+ else
+ assert_se(utf8_console_width(t2) <= max_width);
+
+ t3 = ellipsize_mem(n, old_length, new_length, 100);
+ log_info("100%% → %s utf8_console_width=%zu", t3, utf8_console_width(t3));
+ if (!has_wide_chars)
+ assert_se(utf8_console_width(t3) == max_width);
+ else
+ assert_se(utf8_console_width(t3) <= max_width);
+
+ if (new_length >= old_length) {
+ assert_se(streq(t1, n));
+ assert_se(streq(t2, n));
+ assert_se(streq(t3, n));
+ }
+}
+
+TEST(ellipsize_mem) {
+ FOREACH_STRING(s,
+ "_XXXXXXXXXXX_", /* ASCII */
+ "_aąęółśćńżźć_", /* two-byte utf-8 */
+ "გამარჯობა", /* multi-byte utf-8 */
+ "你好世界", /* wide characters */
+ "你გą世óoó界") /* a mix */
+ for (ssize_t l = strlen(s); l >= 0; l--)
+ for (ssize_t k = strlen(s) + 1; k >= 0; k--)
+ test_ellipsize_mem_one(s, l, k);
+}
+
+static void test_ellipsize_one(const char *p) {
+ _cleanup_free_ char *t = NULL;
+ t = ellipsize(p, columns(), 70);
+ puts(t);
+ free(t);
+ t = ellipsize(p, columns(), 0);
+ puts(t);
+ free(t);
+ t = ellipsize(p, columns(), 100);
+ puts(t);
+ free(t);
+ t = ellipsize(p, 0, 50);
+ puts(t);
+ free(t);
+ t = ellipsize(p, 1, 50);
+ puts(t);
+ free(t);
+ t = ellipsize(p, 2, 50);
+ puts(t);
+ free(t);
+ t = ellipsize(p, 3, 50);
+ puts(t);
+ free(t);
+ t = ellipsize(p, 4, 50);
+ puts(t);
+ free(t);
+ t = ellipsize(p, 5, 50);
+ puts(t);
+}
+
+TEST(ellipsize) {
+ test_ellipsize_one(DIGITS LETTERS DIGITS LETTERS);
+ test_ellipsize_one("한국어한국어한국어한국어한국어한국어한국어한국어한국어한국어한국어한국어한국어한국어한국어한국어한국어한국어");
+ test_ellipsize_one("-日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国日本国");
+ test_ellipsize_one("中国中国中国中国中国中国中国中国中国中国中国中国中国中国中国中国中国中国中国中国中国中国-中国中国中国中国中国中国中国中国中国中国中国中国中国");
+ test_ellipsize_one("sÿstëmd sÿstëmd sÿstëmd sÿstëmd sÿstëmd sÿstëmd sÿstëmd sÿstëmd sÿstëmd sÿstëmd sÿstëmd sÿstëmd sÿstëmd");
+ test_ellipsize_one("🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮🐮");
+ test_ellipsize_one("Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.");
+ test_ellipsize_one("shórt");
+}
+
+TEST(ellipsize_ansi) {
+ const char *s = ANSI_HIGHLIGHT_YELLOW_UNDERLINE "yęllow"
+ ANSI_HIGHLIGHT_GREY_UNDERLINE "grěy"
+ ANSI_HIGHLIGHT_BLUE_UNDERLINE "blue"
+ ANSI_NORMAL "nórmął";
+ size_t len = strlen(s);
+
+ for (unsigned percent = 0; percent <= 100; percent += 15)
+ for (ssize_t x = 21; x >= 0; x--) {
+ _cleanup_free_ char *t = ellipsize_mem(s, len, x, percent);
+ printf("%02zd: \"%s\"\n", x, t);
+ assert_se(utf8_is_valid(t));
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *e = cescape(t);
+ printf(" : \"%s\"\n", e);
+ }
+ }
+}
+
+TEST(ellipsize_ansi_cats) {
+ _cleanup_free_ char *e = NULL, *f = NULL, *g = NULL, *h = NULL;
+
+ /* Make sure we don't cut off in the middle of an ANSI escape sequence. */
+
+ e = ellipsize("01" ANSI_NORMAL "23", 4, 0);
+ puts(e);
+ assert_se(streq(e, "01" ANSI_NORMAL "23"));
+ f = ellipsize("ab" ANSI_NORMAL "cd", 4, 90);
+ puts(f);
+ assert_se(streq(f, "ab" ANSI_NORMAL "cd"));
+
+ g = ellipsize("🐱🐱" ANSI_NORMAL "🐱🐱" ANSI_NORMAL, 5, 0);
+ puts(g);
+ assert_se(streq(g, "…" ANSI_NORMAL "🐱🐱" ANSI_NORMAL));
+ h = ellipsize("🐱🐱" ANSI_NORMAL "🐱🐱" ANSI_NORMAL, 5, 90);
+ puts(h);
+ assert_se(streq(h, "🐱…" ANSI_NORMAL "🐱" ANSI_NORMAL));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-emergency-action.c b/src/test/test-emergency-action.c
new file mode 100644
index 0000000..5c0ce7f
--- /dev/null
+++ b/src/test/test-emergency-action.c
@@ -0,0 +1,51 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "emergency-action.h"
+#include "tests.h"
+
+TEST(parse_emergency_action) {
+ EmergencyAction x;
+
+ assert_se(parse_emergency_action("none", RUNTIME_SCOPE_USER, &x) == 0);
+ assert_se(x == EMERGENCY_ACTION_NONE);
+ assert_se(parse_emergency_action("reboot", RUNTIME_SCOPE_USER, &x) == -EOPNOTSUPP);
+ assert_se(parse_emergency_action("reboot-force", RUNTIME_SCOPE_USER, &x) == -EOPNOTSUPP);
+ assert_se(parse_emergency_action("reboot-immediate", RUNTIME_SCOPE_USER, &x) == -EOPNOTSUPP);
+ assert_se(parse_emergency_action("poweroff", RUNTIME_SCOPE_USER, &x) == -EOPNOTSUPP);
+ assert_se(parse_emergency_action("poweroff-force", RUNTIME_SCOPE_USER, &x) == -EOPNOTSUPP);
+ assert_se(parse_emergency_action("poweroff-immediate", RUNTIME_SCOPE_USER, &x) == -EOPNOTSUPP);
+ assert_se(x == EMERGENCY_ACTION_NONE);
+ assert_se(parse_emergency_action("exit", RUNTIME_SCOPE_USER, &x) == 0);
+ assert_se(x == EMERGENCY_ACTION_EXIT);
+ assert_se(parse_emergency_action("exit-force", RUNTIME_SCOPE_USER, &x) == 0);
+ assert_se(x == EMERGENCY_ACTION_EXIT_FORCE);
+ assert_se(parse_emergency_action("exit-forcee", RUNTIME_SCOPE_USER, &x) == -EINVAL);
+
+ assert_se(parse_emergency_action("none", RUNTIME_SCOPE_SYSTEM, &x) == 0);
+ assert_se(x == EMERGENCY_ACTION_NONE);
+ assert_se(parse_emergency_action("reboot", RUNTIME_SCOPE_SYSTEM, &x) == 0);
+ assert_se(x == EMERGENCY_ACTION_REBOOT);
+ assert_se(parse_emergency_action("reboot-force", RUNTIME_SCOPE_SYSTEM, &x) == 0);
+ assert_se(x == EMERGENCY_ACTION_REBOOT_FORCE);
+ assert_se(parse_emergency_action("reboot-immediate", RUNTIME_SCOPE_SYSTEM, &x) == 0);
+ assert_se(x == EMERGENCY_ACTION_REBOOT_IMMEDIATE);
+ assert_se(parse_emergency_action("poweroff", RUNTIME_SCOPE_SYSTEM, &x) == 0);
+ assert_se(x == EMERGENCY_ACTION_POWEROFF);
+ assert_se(parse_emergency_action("poweroff-force", RUNTIME_SCOPE_SYSTEM, &x) == 0);
+ assert_se(x == EMERGENCY_ACTION_POWEROFF_FORCE);
+ assert_se(parse_emergency_action("poweroff-immediate", RUNTIME_SCOPE_SYSTEM, &x) == 0);
+ assert_se(parse_emergency_action("exit", RUNTIME_SCOPE_SYSTEM, &x) == 0);
+ assert_se(parse_emergency_action("exit-force", RUNTIME_SCOPE_SYSTEM, &x) == 0);
+ assert_se(parse_emergency_action("exit-forcee", RUNTIME_SCOPE_SYSTEM, &x) == -EINVAL);
+ assert_se(x == EMERGENCY_ACTION_EXIT_FORCE);
+ assert_se(parse_emergency_action("kexec", RUNTIME_SCOPE_SYSTEM, &x) == 0);
+ assert_se(parse_emergency_action("kexec-force", RUNTIME_SCOPE_SYSTEM, &x) == 0);
+ assert_se(parse_emergency_action("kexec-forcee", RUNTIME_SCOPE_SYSTEM, &x) == -EINVAL);
+ assert_se(x == EMERGENCY_ACTION_KEXEC_FORCE);
+ assert_se(parse_emergency_action("halt", RUNTIME_SCOPE_SYSTEM, &x) == 0);
+ assert_se(parse_emergency_action("halt-force", RUNTIME_SCOPE_SYSTEM, &x) == 0);
+ assert_se(parse_emergency_action("halt-forcee", RUNTIME_SCOPE_SYSTEM, &x) == -EINVAL);
+ assert_se(x == EMERGENCY_ACTION_HALT_FORCE);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-engine.c b/src/test/test-engine.c
new file mode 100644
index 0000000..cf77e7c
--- /dev/null
+++ b/src/test/test-engine.c
@@ -0,0 +1,300 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <stdio.h>
+
+#include "bus-util.h"
+#include "manager.h"
+#include "manager-dump.h"
+#include "rm-rf.h"
+#include "service.h"
+#include "slice.h"
+#include "special.h"
+#include "strv.h"
+#include "tests.h"
+#include "unit-serialize.h"
+
+static void verify_dependency_atoms(void) {
+ UnitDependencyAtom combined = 0, multi_use_atoms = 0;
+
+ /* Let's guarantee that our dependency type/atom translation tables are fully correct */
+
+ for (UnitDependency d = 0; d < _UNIT_DEPENDENCY_MAX; d++) {
+ UnitDependencyAtom a;
+ UnitDependency reverse;
+ bool has_superset = false;
+
+ assert_se((a = unit_dependency_to_atom(d)) >= 0);
+
+ for (UnitDependency t = 0; t < _UNIT_DEPENDENCY_MAX; t++) {
+ UnitDependencyAtom b;
+
+ if (t == d)
+ continue;
+
+ assert_se((b = unit_dependency_to_atom(t)) >= 0);
+
+ if ((a & b) == a) {
+ has_superset = true;
+ break;
+ }
+ }
+
+ reverse = unit_dependency_from_unique_atom(a);
+ assert_se(reverse == _UNIT_DEPENDENCY_INVALID || reverse >= 0);
+
+ assert_se((reverse < 0) == has_superset); /* If one dependency type is a superset of another,
+ * then the reverse mapping is not unique, verify
+ * that. */
+
+ log_info("Verified dependency type: %s", unit_dependency_to_string(d));
+
+ multi_use_atoms |= combined & a;
+ combined |= a;
+ }
+
+ /* Make sure all atoms are used, i.e. there's at least one dependency type that references it. */
+ assert_se(combined == _UNIT_DEPENDENCY_ATOM_MAX);
+
+ for (UnitDependencyAtom a = 1; a <= _UNIT_DEPENDENCY_ATOM_MAX; a <<= 1) {
+
+ if (multi_use_atoms & a) {
+ /* If an atom is used by multiple dep types, then mapping the atom to a dependency is
+ * not unique and *must* fail */
+ assert_se(unit_dependency_from_unique_atom(a) == _UNIT_DEPENDENCY_INVALID);
+ continue;
+ }
+
+ /* If only a single dep type uses specific atom, let's guarantee our mapping table is
+ complete, and thus the atom can be mapped to the single dep type that is used. */
+ assert_se(unit_dependency_from_unique_atom(a) >= 0);
+ }
+}
+
+int main(int argc, char *argv[]) {
+ _cleanup_(rm_rf_physical_and_freep) char *runtime_dir = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error err = SD_BUS_ERROR_NULL;
+ _cleanup_(manager_freep) Manager *m = NULL;
+ Unit *a = NULL, *b = NULL, *c = NULL, *d = NULL, *e = NULL, *g = NULL,
+ *h = NULL, *i = NULL, *a_conj = NULL, *unit_with_multiple_dashes = NULL, *stub = NULL,
+ *tomato = NULL, *sauce = NULL, *fruit = NULL, *zupa = NULL;
+ Job *j;
+ int r;
+
+ test_setup_logging(LOG_DEBUG);
+
+ r = enter_cgroup_subroot(NULL);
+ if (r == -ENOMEDIUM)
+ return log_tests_skipped("cgroupfs not available");
+
+ /* prepare the test */
+ _cleanup_free_ char *unit_dir = NULL;
+ assert_se(get_testdata_dir("units", &unit_dir) >= 0);
+ assert_se(set_unit_path(unit_dir) >= 0);
+ assert_se(runtime_dir = setup_fake_runtime_dir());
+
+ r = manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_BASIC, &m);
+ if (manager_errno_skip_test(r))
+ return log_tests_skipped_errno(r, "manager_new");
+ assert_se(r >= 0);
+ assert_se(manager_startup(m, NULL, NULL, NULL) >= 0);
+
+ printf("Load1:\n");
+ assert_se(manager_load_startable_unit_or_warn(m, "a.service", NULL, &a) >= 0);
+ assert_se(manager_load_startable_unit_or_warn(m, "b.service", NULL, &b) >= 0);
+ assert_se(manager_load_startable_unit_or_warn(m, "c.service", NULL, &c) >= 0);
+ manager_dump_units(m, stdout, /* patterns= */ NULL, "\t");
+
+ printf("Test1: (Trivial)\n");
+ r = manager_add_job(m, JOB_START, c, JOB_REPLACE, NULL, &err, &j);
+ if (sd_bus_error_is_set(&err))
+ log_error("error: %s: %s", err.name, err.message);
+ assert_se(r == 0);
+ manager_dump_jobs(m, stdout, /* patterns= */ NULL, "\t");
+
+ printf("Load2:\n");
+ manager_clear_jobs(m);
+ assert_se(manager_load_startable_unit_or_warn(m, "d.service", NULL, &d) >= 0);
+ assert_se(manager_load_startable_unit_or_warn(m, "e.service", NULL, &e) >= 0);
+ manager_dump_units(m, stdout, /* patterns= */ NULL, "\t");
+
+ printf("Test2: (Cyclic Order, Unfixable)\n");
+ assert_se(manager_add_job(m, JOB_START, d, JOB_REPLACE, NULL, NULL, &j) == -EDEADLK);
+ manager_dump_jobs(m, stdout, /* patterns= */ NULL, "\t");
+
+ printf("Test3: (Cyclic Order, Fixable, Garbage Collector)\n");
+ assert_se(manager_add_job(m, JOB_START, e, JOB_REPLACE, NULL, NULL, &j) == 0);
+ manager_dump_jobs(m, stdout, /* patterns= */ NULL, "\t");
+
+ printf("Test4: (Identical transaction)\n");
+ assert_se(manager_add_job(m, JOB_START, e, JOB_FAIL, NULL, NULL, &j) == 0);
+ manager_dump_jobs(m, stdout, /* patterns= */ NULL, "\t");
+
+ printf("Load3:\n");
+ assert_se(manager_load_startable_unit_or_warn(m, "g.service", NULL, &g) >= 0);
+ manager_dump_units(m, stdout, /* patterns= */ NULL, "\t");
+
+ printf("Test5: (Colliding transaction, fail)\n");
+ assert_se(manager_add_job(m, JOB_START, g, JOB_FAIL, NULL, NULL, &j) == -EDEADLK);
+
+ printf("Test6: (Colliding transaction, replace)\n");
+ assert_se(manager_add_job(m, JOB_START, g, JOB_REPLACE, NULL, NULL, &j) == 0);
+ manager_dump_jobs(m, stdout, /* patterns= */ NULL, "\t");
+
+ printf("Test7: (Unmergeable job type, fail)\n");
+ assert_se(manager_add_job(m, JOB_STOP, g, JOB_FAIL, NULL, NULL, &j) == -EDEADLK);
+
+ printf("Test8: (Mergeable job type, fail)\n");
+ assert_se(manager_add_job(m, JOB_RESTART, g, JOB_FAIL, NULL, NULL, &j) == 0);
+ manager_dump_jobs(m, stdout, /* patterns= */ NULL, "\t");
+
+ printf("Test9: (Unmergeable job type, replace)\n");
+ assert_se(manager_add_job(m, JOB_STOP, g, JOB_REPLACE, NULL, NULL, &j) == 0);
+ manager_dump_jobs(m, stdout, /* patterns= */ NULL, "\t");
+
+ printf("Load4:\n");
+ assert_se(manager_load_startable_unit_or_warn(m, "h.service", NULL, &h) >= 0);
+ manager_dump_units(m, stdout, /* patterns= */ NULL, "\t");
+
+ printf("Test10: (Unmergeable job type of auxiliary job, fail)\n");
+ assert_se(manager_add_job(m, JOB_START, h, JOB_FAIL, NULL, NULL, &j) == 0);
+ manager_dump_jobs(m, stdout, /* patterns= */ NULL, "\t");
+
+ printf("Load5:\n");
+ manager_clear_jobs(m);
+ assert_se(manager_load_startable_unit_or_warn(m, "i.service", NULL, &i) >= 0);
+ SERVICE(a)->state = SERVICE_RUNNING;
+ SERVICE(d)->state = SERVICE_RUNNING;
+ manager_dump_units(m, stdout, /* patterns= */ NULL, "\t");
+
+ printf("Test11: (Start/stop job ordering, execution cycle)\n");
+ assert_se(manager_add_job(m, JOB_START, i, JOB_FAIL, NULL, NULL, &j) == 0);
+ assert_se(unit_has_job_type(a, JOB_STOP));
+ assert_se(unit_has_job_type(d, JOB_STOP));
+ assert_se(unit_has_job_type(b, JOB_START));
+ manager_dump_jobs(m, stdout, /* patterns= */ NULL, "\t");
+
+ printf("Load6:\n");
+ manager_clear_jobs(m);
+ assert_se(manager_load_startable_unit_or_warn(m, "a-conj.service", NULL, &a_conj) >= 0);
+ SERVICE(a)->state = SERVICE_DEAD;
+ manager_dump_units(m, stdout, /* patterns= */ NULL, "\t");
+
+ printf("Test12: (Trivial cycle, Unfixable)\n");
+ assert_se(manager_add_job(m, JOB_START, a_conj, JOB_REPLACE, NULL, NULL, &j) == -EDEADLK);
+ manager_dump_jobs(m, stdout, /* patterns= */ NULL, "\t");
+
+ assert_se(!hashmap_get(unit_get_dependencies(a, UNIT_PROPAGATES_RELOAD_TO), b));
+ assert_se(!hashmap_get(unit_get_dependencies(b, UNIT_RELOAD_PROPAGATED_FROM), a));
+ assert_se(!hashmap_get(unit_get_dependencies(a, UNIT_PROPAGATES_RELOAD_TO), c));
+ assert_se(!hashmap_get(unit_get_dependencies(c, UNIT_RELOAD_PROPAGATED_FROM), a));
+
+ assert_se(unit_add_dependency(a, UNIT_PROPAGATES_RELOAD_TO, b, true, UNIT_DEPENDENCY_UDEV) >= 0);
+ assert_se(unit_add_dependency(a, UNIT_PROPAGATES_RELOAD_TO, c, true, UNIT_DEPENDENCY_PROC_SWAP) >= 0);
+
+ assert_se( hashmap_get(unit_get_dependencies(a, UNIT_PROPAGATES_RELOAD_TO), b));
+ assert_se( hashmap_get(unit_get_dependencies(b, UNIT_RELOAD_PROPAGATED_FROM), a));
+ assert_se( hashmap_get(unit_get_dependencies(a, UNIT_PROPAGATES_RELOAD_TO), c));
+ assert_se( hashmap_get(unit_get_dependencies(c, UNIT_RELOAD_PROPAGATED_FROM), a));
+
+ unit_remove_dependencies(a, UNIT_DEPENDENCY_UDEV);
+
+ assert_se(!hashmap_get(unit_get_dependencies(a, UNIT_PROPAGATES_RELOAD_TO), b));
+ assert_se(!hashmap_get(unit_get_dependencies(b, UNIT_RELOAD_PROPAGATED_FROM), a));
+ assert_se( hashmap_get(unit_get_dependencies(a, UNIT_PROPAGATES_RELOAD_TO), c));
+ assert_se( hashmap_get(unit_get_dependencies(c, UNIT_RELOAD_PROPAGATED_FROM), a));
+
+ unit_remove_dependencies(a, UNIT_DEPENDENCY_PROC_SWAP);
+
+ assert_se(!hashmap_get(unit_get_dependencies(a, UNIT_PROPAGATES_RELOAD_TO), b));
+ assert_se(!hashmap_get(unit_get_dependencies(b, UNIT_RELOAD_PROPAGATED_FROM), a));
+ assert_se(!hashmap_get(unit_get_dependencies(a, UNIT_PROPAGATES_RELOAD_TO), c));
+ assert_se(!hashmap_get(unit_get_dependencies(c, UNIT_RELOAD_PROPAGATED_FROM), a));
+
+ assert_se(manager_load_unit(m, "unit-with-multiple-dashes.service", NULL, NULL, &unit_with_multiple_dashes) >= 0);
+
+ assert_se(strv_equal(unit_with_multiple_dashes->documentation, STRV_MAKE("man:test", "man:override2", "man:override3")));
+ assert_se(streq_ptr(unit_with_multiple_dashes->description, "override4"));
+
+ /* Now merge a synthetic unit into the existing one */
+ assert_se(unit_new_for_name(m, sizeof(Service), "merged.service", &stub) >= 0);
+ assert_se(unit_add_dependency_by_name(stub, UNIT_AFTER, SPECIAL_BASIC_TARGET, true, UNIT_DEPENDENCY_FILE) >= 0);
+ assert_se(unit_add_dependency_by_name(stub, UNIT_AFTER, "quux.target", true, UNIT_DEPENDENCY_FILE) >= 0);
+ assert_se(unit_add_dependency_by_name(stub, UNIT_AFTER, SPECIAL_ROOT_SLICE, true, UNIT_DEPENDENCY_FILE) >= 0);
+ assert_se(unit_add_dependency_by_name(stub, UNIT_REQUIRES, "non-existing.mount", true, UNIT_DEPENDENCY_FILE) >= 0);
+ assert_se(unit_add_dependency_by_name(stub, UNIT_ON_FAILURE, "non-existing-on-failure.target", true, UNIT_DEPENDENCY_FILE) >= 0);
+ assert_se(unit_add_dependency_by_name(stub, UNIT_ON_SUCCESS, "non-existing-on-success.target", true, UNIT_DEPENDENCY_FILE) >= 0);
+
+ log_info("/* Merging a+stub, dumps before */");
+ unit_dump(a, stderr, NULL);
+ unit_dump(stub, stderr, NULL);
+ assert_se(unit_merge(a, stub) >= 0);
+ log_info("/* Dump of merged a+stub */");
+ unit_dump(a, stderr, NULL);
+
+ assert_se( unit_has_dependency(a, UNIT_ATOM_AFTER, manager_get_unit(m, SPECIAL_BASIC_TARGET)));
+ assert_se( unit_has_dependency(a, UNIT_ATOM_AFTER, manager_get_unit(m, "quux.target")));
+ assert_se( unit_has_dependency(a, UNIT_ATOM_AFTER, manager_get_unit(m, SPECIAL_ROOT_SLICE)));
+ assert_se( unit_has_dependency(a, UNIT_ATOM_PULL_IN_START, manager_get_unit(m, "non-existing.mount")));
+ assert_se( unit_has_dependency(a, UNIT_ATOM_RETROACTIVE_START_REPLACE, manager_get_unit(m, "non-existing.mount")));
+ assert_se( unit_has_dependency(a, UNIT_ATOM_ON_FAILURE, manager_get_unit(m, "non-existing-on-failure.target")));
+ assert_se( unit_has_dependency(manager_get_unit(m, "non-existing-on-failure.target"), UNIT_ATOM_ON_FAILURE_OF, a));
+ assert_se( unit_has_dependency(a, UNIT_ATOM_ON_SUCCESS, manager_get_unit(m, "non-existing-on-success.target")));
+ assert_se( unit_has_dependency(manager_get_unit(m, "non-existing-on-success.target"), UNIT_ATOM_ON_SUCCESS_OF, a));
+ assert_se(!unit_has_dependency(a, UNIT_ATOM_ON_FAILURE, manager_get_unit(m, "basic.target")));
+ assert_se(!unit_has_dependency(a, UNIT_ATOM_ON_SUCCESS, manager_get_unit(m, "basic.target")));
+ assert_se(!unit_has_dependency(a, UNIT_ATOM_ON_FAILURE_OF, manager_get_unit(m, "basic.target")));
+ assert_se(!unit_has_dependency(a, UNIT_ATOM_ON_SUCCESS_OF, manager_get_unit(m, "basic.target")));
+ assert_se(!unit_has_dependency(a, UNIT_ATOM_PROPAGATES_RELOAD_TO, manager_get_unit(m, "non-existing-on-failure.target")));
+
+ assert_se(unit_has_name(a, "a.service"));
+ assert_se(unit_has_name(a, "merged.service"));
+
+ unsigned mm = 1;
+ Unit *other;
+
+ UNIT_FOREACH_DEPENDENCY(other, a, UNIT_ATOM_AFTER) {
+ mm *= unit_has_name(other, SPECIAL_BASIC_TARGET) ? 3 : 1;
+ mm *= unit_has_name(other, "quux.target") ? 5 : 1;
+ mm *= unit_has_name(other, SPECIAL_ROOT_SLICE) ? 7 : 1;
+ }
+
+ UNIT_FOREACH_DEPENDENCY(other, a, UNIT_ATOM_ON_FAILURE)
+ mm *= unit_has_name(other, "non-existing-on-failure.target") ? 11 : 1;
+
+ UNIT_FOREACH_DEPENDENCY(other, a, UNIT_ATOM_PULL_IN_START)
+ mm *= unit_has_name(other, "non-existing.mount") ? 13 : 1;
+
+ assert_se(mm == 3U*5U*7U*11U*13U);
+
+ verify_dependency_atoms();
+
+ /* Test adding multiple Slice= dependencies; only the last should remain */
+ assert_se(unit_new_for_name(m, sizeof(Service), "tomato.service", &tomato) >= 0);
+ assert_se(unit_new_for_name(m, sizeof(Slice), "sauce.slice", &sauce) >= 0);
+ assert_se(unit_new_for_name(m, sizeof(Slice), "fruit.slice", &fruit) >= 0);
+ assert_se(unit_new_for_name(m, sizeof(Slice), "zupa.slice", &zupa) >= 0);
+
+ unit_set_slice(tomato, sauce);
+ unit_set_slice(tomato, fruit);
+ unit_set_slice(tomato, zupa);
+
+ assert_se(UNIT_GET_SLICE(tomato) == zupa);
+ assert_se(!unit_has_dependency(tomato, UNIT_ATOM_IN_SLICE, sauce));
+ assert_se(!unit_has_dependency(tomato, UNIT_ATOM_IN_SLICE, fruit));
+ assert_se( unit_has_dependency(tomato, UNIT_ATOM_IN_SLICE, zupa));
+
+ assert_se(!unit_has_dependency(tomato, UNIT_ATOM_REFERENCES, sauce));
+ assert_se(!unit_has_dependency(tomato, UNIT_ATOM_REFERENCES, fruit));
+ assert_se( unit_has_dependency(tomato, UNIT_ATOM_REFERENCES, zupa));
+
+ assert_se(!unit_has_dependency(sauce, UNIT_ATOM_SLICE_OF, tomato));
+ assert_se(!unit_has_dependency(fruit, UNIT_ATOM_SLICE_OF, tomato));
+ assert_se( unit_has_dependency(zupa, UNIT_ATOM_SLICE_OF, tomato));
+
+ assert_se(!unit_has_dependency(sauce, UNIT_ATOM_REFERENCED_BY, tomato));
+ assert_se(!unit_has_dependency(fruit, UNIT_ATOM_REFERENCED_BY, tomato));
+ assert_se( unit_has_dependency(zupa, UNIT_ATOM_REFERENCED_BY, tomato));
+
+ return 0;
+}
diff --git a/src/test/test-env-file.c b/src/test/test-env-file.c
new file mode 100644
index 0000000..3fc6d62
--- /dev/null
+++ b/src/test/test-env-file.c
@@ -0,0 +1,191 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "env-file.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "macro.h"
+#include "strv.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+/* In case of repeating keys, later entries win. */
+
+#define env_file_1 \
+ "a=a\n" \
+ "a=b\n" \
+ "a=b\n" \
+ "a=a\n" \
+ "b=b\\\n" \
+ "c\n" \
+ "d= d\\\n" \
+ "e \\\n" \
+ "f \n" \
+ "g=g\\ \n" \
+ "h= ąęół\\ śćńźżμ \n" \
+ "i=i\\"
+
+#define env_file_2 \
+ "a=a\\\n"
+
+#define env_file_3 \
+ "#SPAMD_ARGS=\"-d --socketpath=/var/lib/bulwark/spamd \\\n" \
+ "#--nouser-config \\\n" \
+ "normal1=line\\\n" \
+ "111\n" \
+ ";normal=ignored \\\n" \
+ "normal2=line222\n" \
+ "normal ignored \\\n"
+
+#define env_file_4 \
+ "# Generated\n" \
+ "\n" \
+ "HWMON_MODULES=\"coretemp f71882fg\"\n" \
+ "\n" \
+ "# For compatibility reasons\n" \
+ "\n" \
+ "MODULE_0=coretemp\n" \
+ "MODULE_1=f71882fg"
+
+#define env_file_5 \
+ "a=\n" \
+ "b="
+
+#define env_file_6 \
+ "a=\\ \\n \\t \\x \\y \\' \n" \
+ "b= \\$' \n" \
+ "c= ' \\n\\t\\$\\`\\\\\n" \
+ "' \n" \
+ "d= \" \\n\\t\\$\\`\\\\\n" \
+ "\" \n"
+
+TEST(load_env_file_1) {
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-load-env-file.XXXXXX";
+ assert_se(write_tmpfile(name, env_file_1) == 0);
+
+ _cleanup_strv_free_ char **data = NULL;
+ assert_se(load_env_file(NULL, name, &data) == 0);
+ assert_se(streq(data[0], "a=a"));
+ assert_se(streq(data[1], "b=bc"));
+ assert_se(streq(data[2], "d=de f"));
+ assert_se(streq(data[3], "g=g "));
+ assert_se(streq(data[4], "h=ąęół śćńźżμ"));
+ assert_se(streq(data[5], "i=i"));
+ assert_se(data[6] == NULL);
+}
+
+TEST(load_env_file_2) {
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-load-env-file.XXXXXX";
+ assert_se(write_tmpfile(name, env_file_2) == 0);
+
+ _cleanup_strv_free_ char **data = NULL;
+ assert_se(load_env_file(NULL, name, &data) == 0);
+ assert_se(streq(data[0], "a=a"));
+ assert_se(data[1] == NULL);
+}
+
+TEST(load_env_file_3) {
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-load-env-file.XXXXXX";
+ assert_se(write_tmpfile(name, env_file_3) == 0);
+
+ _cleanup_strv_free_ char **data = NULL;
+ assert_se(load_env_file(NULL, name, &data) == 0);
+ assert_se(streq(data[0], "normal1=line111"));
+ assert_se(streq(data[1], "normal2=line222"));
+ assert_se(data[2] == NULL);
+}
+
+TEST(load_env_file_4) {
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-load-env-file.XXXXXX";
+ assert_se(write_tmpfile(name, env_file_4) == 0);
+
+ _cleanup_strv_free_ char **data = NULL;
+ assert_se(load_env_file(NULL, name, &data) == 0);
+ assert_se(streq(data[0], "HWMON_MODULES=coretemp f71882fg"));
+ assert_se(streq(data[1], "MODULE_0=coretemp"));
+ assert_se(streq(data[2], "MODULE_1=f71882fg"));
+ assert_se(data[3] == NULL);
+}
+
+TEST(load_env_file_5) {
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-load-env-file.XXXXXX";
+ assert_se(write_tmpfile(name, env_file_5) == 0);
+
+ _cleanup_strv_free_ char **data = NULL;
+ assert_se(load_env_file(NULL, name, &data) == 0);
+ assert_se(streq(data[0], "a="));
+ assert_se(streq(data[1], "b="));
+ assert_se(data[2] == NULL);
+}
+
+TEST(load_env_file_6) {
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-load-env-file.XXXXXX";
+ assert_se(write_tmpfile(name, env_file_6) == 0);
+
+ _cleanup_strv_free_ char **data = NULL;
+ assert_se(load_env_file(NULL, name, &data) == 0);
+ assert_se(streq(data[0], "a= n t x y '"));
+ assert_se(streq(data[1], "b=$'"));
+ assert_se(streq(data[2], "c= \\n\\t\\$\\`\\\\\n"));
+ assert_se(streq(data[3], "d= \\n\\t$`\\\n"));
+ assert_se(data[4] == NULL);
+}
+
+TEST(load_env_file_invalid_utf8) {
+ /* Test out a couple of assignments where the key/value has an invalid
+ * UTF-8 character ("noncharacter")
+ *
+ * See: https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Non-characters
+ */
+ FOREACH_STRING(s,
+ "fo\ufffeo=bar",
+ "foo=b\uffffar",
+ "baz=hello world\ufffe") {
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-load-env-file.XXXXXX";
+ assert_se(write_tmpfile(name, s) == 0);
+
+ _cleanup_strv_free_ char **data = NULL;
+ assert_se(load_env_file(NULL, name, &data) == -EINVAL);
+ assert_se(!data);
+ }
+}
+
+TEST(write_and_load_env_file) {
+ /* Make sure that our writer, parser and the shell agree on what our env var files mean */
+
+ FOREACH_STRING(v,
+ "obbardc-laptop",
+ "obbardc\\-laptop",
+ "obbardc-lap\\top",
+ "obbardc-lap\\top",
+ "obbardc-lap\\\\top",
+ "double\"quote",
+ "single\'quote",
+ "dollar$dollar",
+ "newline\nnewline") {
+ _cleanup_(unlink_and_freep) char *p = NULL;
+ _cleanup_strv_free_ char **l = NULL;
+ _cleanup_free_ char *j = NULL, *w = NULL, *cmd = NULL, *from_shell = NULL;
+ _cleanup_pclose_ FILE *f = NULL;
+ size_t sz;
+
+ assert_se(tempfn_random_child(NULL, NULL, &p) >= 0);
+
+ assert_se(j = strjoin("TEST=", v));
+ assert_se(write_env_file(AT_FDCWD, p, STRV_MAKE("# header 1", "", "# header 2"), STRV_MAKE(j)) >= 0);
+
+ assert_se(cmd = strjoin(". ", p, " && /bin/echo -n \"$TEST\""));
+ assert_se(f = popen(cmd, "re"));
+ assert_se(read_full_stream(f, &from_shell, &sz) >= 0);
+ assert_se(sz == strlen(v));
+ assert_se(streq(from_shell, v));
+
+ assert_se(load_env_file(NULL, p, &l) >= 0);
+ assert_se(strv_equal(l, STRV_MAKE(j)));
+
+ assert_se(parse_env_file(NULL, p, "TEST", &w) >= 0);
+ assert_se(streq_ptr(w, v));
+ }
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-env-util.c b/src/test/test-env-util.c
new file mode 100644
index 0000000..dffbad6
--- /dev/null
+++ b/src/test/test-env-util.c
@@ -0,0 +1,563 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "env-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "parse-util.h"
+#include "process-util.h"
+#include "serialize.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+
+TEST(strv_env_delete) {
+ _cleanup_strv_free_ char **a = NULL, **b = NULL, **c = NULL, **d = NULL;
+
+ a = strv_new("FOO=BAR", "WALDO=WALDO", "WALDO=", "PIEP", "SCHLUMPF=SMURF");
+ assert_se(a);
+
+ b = strv_new("PIEP", "FOO");
+ assert_se(b);
+
+ c = strv_new("SCHLUMPF");
+ assert_se(c);
+
+ d = strv_env_delete(a, 2, b, c);
+ assert_se(d);
+
+ assert_se(streq(d[0], "WALDO=WALDO"));
+ assert_se(streq(d[1], "WALDO="));
+ assert_se(strv_length(d) == 2);
+}
+
+TEST(strv_env_get) {
+ char **l = STRV_MAKE("ONE_OR_TWO=1", "THREE=3", "ONE_OR_TWO=2", "FOUR=4");
+
+ assert_se(streq(strv_env_get(l, "ONE_OR_TWO"), "2"));
+ assert_se(streq(strv_env_get(l, "THREE"), "3"));
+ assert_se(streq(strv_env_get(l, "FOUR"), "4"));
+}
+
+TEST(strv_env_pairs_get) {
+ char **l = STRV_MAKE("ONE_OR_TWO", "1", "THREE", "3", "ONE_OR_TWO", "2", "FOUR", "4", "FIVE", "5", "SIX", "FIVE", "SEVEN", "7");
+
+ assert_se(streq(strv_env_pairs_get(l, "ONE_OR_TWO"), "2"));
+ assert_se(streq(strv_env_pairs_get(l, "THREE"), "3"));
+ assert_se(streq(strv_env_pairs_get(l, "FOUR"), "4"));
+ assert_se(streq(strv_env_pairs_get(l, "FIVE"), "5"));
+}
+
+TEST(strv_env_unset) {
+ _cleanup_strv_free_ char **l = NULL;
+
+ l = strv_new("PIEP", "SCHLUMPF=SMURFF", "NANANANA=YES");
+ assert_se(l);
+
+ assert_se(strv_env_unset(l, "SCHLUMPF") == l);
+
+ assert_se(streq(l[0], "PIEP"));
+ assert_se(streq(l[1], "NANANANA=YES"));
+ assert_se(strv_length(l) == 2);
+}
+
+TEST(strv_env_merge) {
+ char **a = STRV_MAKE("FOO=BAR", "WALDO=WALDO", "WALDO=", "PIEP", "SCHLUMPF=SMURF", "EQ===");
+ char **b = STRV_MAKE("FOO=KKK", "FOO=", "PIEP=", "SCHLUMPF=SMURFF", "NANANANA=YES");
+
+ _cleanup_strv_free_ char **r = strv_env_merge(NULL, a, NULL, b, NULL, a, b, b, NULL);
+ assert_se(r);
+ assert_se(streq(r[0], "FOO="));
+ assert_se(streq(r[1], "WALDO="));
+ assert_se(streq(r[2], "PIEP"));
+ assert_se(streq(r[3], "SCHLUMPF=SMURFF"));
+ assert_se(streq(r[4], "EQ==="));
+ assert_se(streq(r[5], "PIEP="));
+ assert_se(streq(r[6], "NANANANA=YES"));
+ assert_se(strv_length(r) == 7);
+
+ assert_se(strv_env_clean(r) == r);
+ assert_se(streq(r[0], "FOO="));
+ assert_se(streq(r[1], "WALDO="));
+ assert_se(streq(r[2], "SCHLUMPF=SMURFF"));
+ assert_se(streq(r[3], "EQ==="));
+ assert_se(streq(r[4], "PIEP="));
+ assert_se(streq(r[5], "NANANANA=YES"));
+ assert_se(strv_length(r) == 6);
+}
+
+TEST(strv_env_replace_strdup) {
+ _cleanup_strv_free_ char **a = NULL;
+
+ assert_se(strv_env_replace_strdup(&a, "a=a") == 1);
+ assert_se(strv_env_replace_strdup(&a, "b=b") == 1);
+ assert_se(strv_env_replace_strdup(&a, "a=A") == 0);
+ assert_se(strv_env_replace_strdup(&a, "c") == -EINVAL);
+
+ assert_se(strv_length(a) == 2);
+ strv_sort(a);
+ assert_se(streq(a[0], "a=A"));
+ assert_se(streq(a[1], "b=b"));
+}
+
+TEST(strv_env_replace_strdup_passthrough) {
+ _cleanup_strv_free_ char **a = NULL;
+
+ assert_se(putenv((char*) "a=a") == 0);
+ assert_se(putenv((char*) "b=") == 0);
+ assert_se(unsetenv("c") == 0);
+
+ assert_se(strv_env_replace_strdup_passthrough(&a, "a") == 1);
+ assert_se(strv_env_replace_strdup_passthrough(&a, "b") == 1);
+ assert_se(strv_env_replace_strdup_passthrough(&a, "c") == 1);
+ assert_se(strv_env_replace_strdup_passthrough(&a, "a") == 0);
+ assert_se(strv_env_replace_strdup_passthrough(&a, "$a") == -EINVAL);
+
+ assert_se(strv_length(a) == 3);
+ assert_se(streq(a[0], "a=a"));
+ assert_se(streq(a[1], "b="));
+ assert_se(streq(a[2], "c="));
+}
+
+TEST(strv_env_assign) {
+ _cleanup_strv_free_ char **a = NULL;
+
+ assert_se(strv_env_assign(&a, "a", "a") == 1);
+ assert_se(strv_env_assign(&a, "b", "b") == 1);
+ assert_se(strv_env_assign(&a, "a", "A") == 0);
+ assert_se(strv_env_assign(&a, "b", NULL) == 0);
+
+ assert_se(strv_env_assign(&a, "a=", "B") == -EINVAL);
+
+ assert_se(strv_length(a) == 1);
+ assert_se(streq(a[0], "a=A"));
+}
+
+TEST(strv_env_assign_many) {
+ _cleanup_strv_free_ char **a = NULL;
+
+ assert_se(strv_env_assign_many(&a, "a", "a", "b", "b") >= 0);
+
+ assert_se(strv_length(a) == 2);
+ assert_se(strv_contains(a, "a=a"));
+ assert_se(strv_contains(a, "b=b"));
+
+ assert_se(strv_env_assign_many(&a, "a", "A", "b", "b", "c", "c") >= 0);
+ assert_se(strv_length(a) == 3);
+ assert_se(strv_contains(a, "a=A"));
+ assert_se(strv_contains(a, "b=b"));
+ assert_se(strv_contains(a, "c=c"));
+
+ assert_se(strv_env_assign_many(&a, "b", NULL, "c", "C") >= 0);
+ assert_se(strv_length(a) == 2);
+ assert_se(strv_contains(a, "a=A"));
+ assert_se(strv_contains(a, "c=C"));
+
+ assert_se(strv_env_assign_many(&a, "a=", "B") == -EINVAL);
+ assert_se(strv_length(a) == 2);
+ assert_se(strv_contains(a, "a=A"));
+ assert_se(strv_contains(a, "c=C"));
+}
+
+TEST(env_strv_get_n) {
+ const char *_env[] = {
+ "FOO=NO NO NO",
+ "FOO=BAR BAR",
+ "BAR=waldo",
+ "PATH=unset",
+ NULL
+ };
+ char **env = (char**) _env;
+
+ assert_se(streq(strv_env_get_n(env, "FOO__", 3, 0), "BAR BAR"));
+ assert_se(streq(strv_env_get_n(env, "FOO__", 3, REPLACE_ENV_USE_ENVIRONMENT), "BAR BAR"));
+ assert_se(streq(strv_env_get_n(env, "FOO", 3, 0), "BAR BAR"));
+ assert_se(streq(strv_env_get_n(env, "FOO", 3, REPLACE_ENV_USE_ENVIRONMENT), "BAR BAR"));
+
+ assert_se(streq(strv_env_get_n(env, "PATH__", 4, 0), "unset"));
+ assert_se(streq(strv_env_get_n(env, "PATH", 4, 0), "unset"));
+ assert_se(streq(strv_env_get_n(env, "PATH__", 4, REPLACE_ENV_USE_ENVIRONMENT), "unset"));
+ assert_se(streq(strv_env_get_n(env, "PATH", 4, REPLACE_ENV_USE_ENVIRONMENT), "unset"));
+
+ env[3] = NULL; /* kill our $PATH */
+
+ assert_se(!strv_env_get_n(env, "PATH__", 4, 0));
+ assert_se(!strv_env_get_n(env, "PATH", 4, 0));
+ assert_se(streq_ptr(strv_env_get_n(env, "PATH__", 4, REPLACE_ENV_USE_ENVIRONMENT),
+ getenv("PATH")));
+ assert_se(streq_ptr(strv_env_get_n(env, "PATH", 4, REPLACE_ENV_USE_ENVIRONMENT),
+ getenv("PATH")));
+}
+
+static void test_replace_env1(bool braceless) {
+ log_info("/* %s(braceless=%s) */", __func__, yes_no(braceless));
+
+ const char *env[] = {
+ "FOO=BAR BAR",
+ "BAR=waldo",
+ NULL
+ };
+ _cleanup_free_ char *t = NULL, *s = NULL, *q = NULL, *r = NULL, *p = NULL;
+ unsigned flags = REPLACE_ENV_ALLOW_BRACELESS*braceless;
+
+ assert_se(replace_env("FOO=$FOO=${FOO}", (char**) env, flags, &t) >= 0);
+ assert_se(streq(t, braceless ? "FOO=BAR BAR=BAR BAR" : "FOO=$FOO=BAR BAR"));
+
+ assert_se(replace_env("BAR=$BAR=${BAR}", (char**) env, flags, &s) >= 0);
+ assert_se(streq(s, braceless ? "BAR=waldo=waldo" : "BAR=$BAR=waldo"));
+
+ assert_se(replace_env("BARBAR=$BARBAR=${BARBAR}", (char**) env, flags, &q) >= 0);
+ assert_se(streq(q, braceless ? "BARBAR==" : "BARBAR=$BARBAR="));
+
+ assert_se(replace_env("BAR=$BAR$BAR${BAR}${BAR}", (char**) env, flags, &r) >= 0);
+ assert_se(streq(r, braceless ? "BAR=waldowaldowaldowaldo" : "BAR=$BAR$BARwaldowaldo"));
+
+ assert_se(replace_env("${BAR}$BAR$BAR", (char**) env, flags, &p) >= 0);
+ assert_se(streq(p, braceless ? "waldowaldowaldo" : "waldo$BAR$BAR"));
+}
+
+static void test_replace_env2(bool extended) {
+ log_info("/* %s(extended=%s) */", __func__, yes_no(extended));
+
+ const char *env[] = {
+ "FOO=foo",
+ "BAR=bar",
+ NULL
+ };
+ _cleanup_free_ char *t = NULL, *s = NULL, *q = NULL, *r = NULL, *p = NULL, *x = NULL, *y = NULL;
+ unsigned flags = REPLACE_ENV_ALLOW_EXTENDED*extended;
+
+ assert_se(replace_env("FOO=${FOO:-${BAR}}", (char**) env, flags, &t) >= 0);
+ assert_se(streq(t, extended ? "FOO=foo" : "FOO=${FOO:-bar}"));
+
+ assert_se(replace_env("BAR=${XXX:-${BAR}}", (char**) env, flags, &s) >= 0);
+ assert_se(streq(s, extended ? "BAR=bar" : "BAR=${XXX:-bar}"));
+
+ assert_se(replace_env("XXX=${XXX:+${BAR}}", (char**) env, flags, &q) >= 0);
+ assert_se(streq(q, extended ? "XXX=" : "XXX=${XXX:+bar}"));
+
+ assert_se(replace_env("FOO=${FOO:+${BAR}}", (char**) env, flags, &r) >= 0);
+ assert_se(streq(r, extended ? "FOO=bar" : "FOO=${FOO:+bar}"));
+
+ assert_se(replace_env("FOO=${FOO:-${BAR}post}", (char**) env, flags, &p) >= 0);
+ assert_se(streq(p, extended ? "FOO=foo" : "FOO=${FOO:-barpost}"));
+
+ assert_se(replace_env("XXX=${XXX:+${BAR}post}", (char**) env, flags, &x) >= 0);
+ assert_se(streq(x, extended ? "XXX=" : "XXX=${XXX:+barpost}"));
+
+ assert_se(replace_env("FOO=${FOO}between${BAR:-baz}", (char**) env, flags, &y) >= 0);
+ assert_se(streq(y, extended ? "FOO=foobetweenbar" : "FOO=foobetween${BAR:-baz}"));
+}
+
+TEST(replace_env) {
+ test_replace_env1(false);
+ test_replace_env1(true);
+ test_replace_env2(false);
+ test_replace_env2(true);
+}
+
+TEST(replace_env_argv) {
+ const char *env[] = {
+ "FOO=BAR BAR",
+ "BAR=waldo",
+ NULL
+ };
+ const char *line[] = {
+ "FOO$FOO",
+ "FOO$FOOFOO",
+ "FOO${FOO}$FOO",
+ "FOO${FOO}",
+ "${FOO}",
+ "$FOO",
+ "$FOO$FOO",
+ "${FOO}${BAR}",
+ "${FOO",
+ "FOO$$${FOO}",
+ "$$FOO${FOO}",
+ "${FOO:-${BAR}}",
+ "${QUUX:-${FOO}}",
+ "${FOO:+${BAR}}",
+ "${QUUX:+${BAR}}",
+ "${FOO:+|${BAR}|}}",
+ "${FOO:+|${BAR}{|}",
+ NULL
+ };
+ _cleanup_strv_free_ char **r = NULL;
+
+ assert_se(replace_env_argv((char**) line, (char**) env, &r, NULL, NULL) >= 0);
+ assert_se(r);
+ assert_se(streq(r[0], "FOO$FOO"));
+ assert_se(streq(r[1], "FOO$FOOFOO"));
+ assert_se(streq(r[2], "FOOBAR BAR$FOO"));
+ assert_se(streq(r[3], "FOOBAR BAR"));
+ assert_se(streq(r[4], "BAR BAR"));
+ assert_se(streq(r[5], "BAR"));
+ assert_se(streq(r[6], "BAR"));
+ assert_se(streq(r[7], "BAR BARwaldo"));
+ assert_se(streq(r[8], "${FOO"));
+ assert_se(streq(r[9], "FOO$BAR BAR"));
+ assert_se(streq(r[10], "$FOOBAR BAR"));
+ assert_se(streq(r[11], "${FOO:-waldo}"));
+ assert_se(streq(r[12], "${QUUX:-BAR BAR}"));
+ assert_se(streq(r[13], "${FOO:+waldo}"));
+ assert_se(streq(r[14], "${QUUX:+waldo}"));
+ assert_se(streq(r[15], "${FOO:+|waldo|}}"));
+ assert_se(streq(r[16], "${FOO:+|waldo{|}"));
+ assert_se(strv_length(r) == 17);
+}
+
+TEST(replace_env_argv_bad) {
+
+ const char *env[] = {
+ "FOO=BAR BAR",
+ "BAR=waldo",
+ NULL
+ };
+
+ const char *line[] = {
+ "$FOO",
+ "A${FOO}B",
+ "a${~}${%}b",
+ "x${}y",
+ "$UNSET2",
+ "z${UNSET3}z${UNSET1}z",
+ "piff${UNSET2}piff",
+ NULL
+ };
+
+ _cleanup_strv_free_ char **bad = NULL, **unset = NULL, **replaced = NULL;
+
+ assert_se(replace_env_argv((char**) line, (char**) env, &replaced, &unset, &bad) >= 0);
+
+ assert_se(strv_equal(replaced, STRV_MAKE(
+ "BAR",
+ "BAR",
+ "ABAR BARB",
+ "ab",
+ "xy",
+ "zzz",
+ "piffpiff")));
+
+ assert_se(strv_equal(unset, STRV_MAKE(
+ "UNSET1",
+ "UNSET2",
+ "UNSET3")));
+ assert_se(strv_equal(bad, STRV_MAKE("",
+ "%",
+ "~")));
+}
+
+TEST(env_clean) {
+ _cleanup_strv_free_ char **e = strv_new("FOOBAR=WALDO",
+ "FOOBAR=WALDO",
+ "FOOBAR",
+ "F",
+ "X=",
+ "F=F",
+ "=",
+ "=F",
+ "",
+ "0000=000",
+ "äöüß=abcd",
+ "abcd=äöüß",
+ "xyz\n=xyz",
+ "xyz=xyz\n",
+ "another=one",
+ "another=final one",
+ "CRLF=\r\n",
+ "LESS_TERMCAP_mb=\x1b[01;31m",
+ "BASH_FUNC_foo%%=() { echo foo\n}");
+ assert_se(e);
+ assert_se(!strv_env_is_valid(e));
+ assert_se(strv_env_clean(e) == e);
+ assert_se(strv_env_is_valid(e));
+
+ assert_se(streq(e[0], "FOOBAR=WALDO"));
+ assert_se(streq(e[1], "X="));
+ assert_se(streq(e[2], "F=F"));
+ assert_se(streq(e[3], "abcd=äöüß"));
+ assert_se(streq(e[4], "xyz=xyz\n"));
+ assert_se(streq(e[5], "another=final one"));
+ assert_se(streq(e[6], "CRLF=\r\n"));
+ assert_se(streq(e[7], "LESS_TERMCAP_mb=\x1b[01;31m"));
+ assert_se(e[8] == NULL);
+}
+
+TEST(env_name_is_valid) {
+ assert_se(env_name_is_valid("test"));
+
+ assert_se(!env_name_is_valid(NULL));
+ assert_se(!env_name_is_valid(""));
+ assert_se(!env_name_is_valid("xxx\a"));
+ assert_se(!env_name_is_valid("xxx\007b"));
+ assert_se(!env_name_is_valid("\007\009"));
+ assert_se(!env_name_is_valid("5_starting_with_a_number_is_wrong"));
+ assert_se(!env_name_is_valid("#¤%&?_only_numbers_letters_and_underscore_allowed"));
+}
+
+TEST(env_value_is_valid) {
+ assert_se(env_value_is_valid(""));
+ assert_se(env_value_is_valid("głąb kapuściany"));
+ assert_se(env_value_is_valid("printf \"\\x1b]0;<mock-chroot>\\x07<mock-chroot>\""));
+ assert_se(env_value_is_valid("tab\tcharacter"));
+ assert_se(env_value_is_valid("new\nline"));
+ assert_se(env_value_is_valid("Show this?\rNope. Show that!"));
+ assert_se(env_value_is_valid("new DOS\r\nline"));
+
+ assert_se(!env_value_is_valid("\xc5")); /* A truncated utf-8-encoded "ł".
+ * We currently disallow that. */
+}
+
+TEST(env_assignment_is_valid) {
+ assert_se(env_assignment_is_valid("a="));
+ assert_se(env_assignment_is_valid("b=głąb kapuściany"));
+ assert_se(env_assignment_is_valid("c=\\007\\009\\011"));
+ assert_se(env_assignment_is_valid("e=printf \"\\x1b]0;<mock-chroot>\\x07<mock-chroot>\""));
+ assert_se(env_assignment_is_valid("f=tab\tcharacter"));
+ assert_se(env_assignment_is_valid("g=new\nline"));
+
+ assert_se(!env_assignment_is_valid("="));
+ assert_se(!env_assignment_is_valid("a b="));
+ assert_se(!env_assignment_is_valid("a ="));
+ assert_se(!env_assignment_is_valid(" b="));
+ /* no dots or dashes: http://tldp.org/LDP/abs/html/gotchas.html */
+ assert_se(!env_assignment_is_valid("a.b="));
+ assert_se(!env_assignment_is_valid("a-b="));
+ assert_se(!env_assignment_is_valid("\007=głąb kapuściany"));
+ assert_se(!env_assignment_is_valid("c\009=\007\009\011"));
+ assert_se(!env_assignment_is_valid("głąb=printf \"\x1b]0;<mock-chroot>\x07<mock-chroot>\""));
+}
+
+TEST(putenv_dup) {
+ assert_se(putenv_dup("A=a1", true) == 0);
+ assert_se(streq_ptr(getenv("A"), "a1"));
+ assert_se(putenv_dup("A=a1", true) == 0);
+ assert_se(streq_ptr(getenv("A"), "a1"));
+ assert_se(putenv_dup("A=a2", false) == 0);
+ assert_se(streq_ptr(getenv("A"), "a1"));
+ assert_se(putenv_dup("A=a2", true) == 0);
+ assert_se(streq_ptr(getenv("A"), "a2"));
+}
+
+TEST(setenv_systemd_exec_pid) {
+ _cleanup_free_ char *saved = NULL;
+ const char *e;
+ pid_t p;
+
+ e = getenv("SYSTEMD_EXEC_PID");
+ if (e)
+ assert_se(saved = strdup(e));
+
+ assert_se(unsetenv("SYSTEMD_EXEC_PID") >= 0);
+ assert_se(setenv_systemd_exec_pid(true) == 0);
+ assert_se(!getenv("SYSTEMD_EXEC_PID"));
+
+ assert_se(setenv("SYSTEMD_EXEC_PID", "*", 1) >= 0);
+ assert_se(setenv_systemd_exec_pid(true) == 0);
+ assert_se(e = getenv("SYSTEMD_EXEC_PID"));
+ assert_se(streq(e, "*"));
+
+ assert_se(setenv("SYSTEMD_EXEC_PID", "123abc", 1) >= 0);
+ assert_se(setenv_systemd_exec_pid(true) == 1);
+ assert_se(e = getenv("SYSTEMD_EXEC_PID"));
+ assert_se(parse_pid(e, &p) >= 0);
+ assert_se(p == getpid_cached());
+
+ assert_se(unsetenv("SYSTEMD_EXEC_PID") >= 0);
+ assert_se(setenv_systemd_exec_pid(false) == 1);
+ assert_se(e = getenv("SYSTEMD_EXEC_PID"));
+ assert_se(parse_pid(e, &p) >= 0);
+ assert_se(p == getpid_cached());
+
+ assert_se(set_unset_env("SYSTEMD_EXEC_PID", saved, 1) >= 0);
+}
+
+TEST(getenv_steal_erase) {
+ int r;
+
+ r = safe_fork("(sd-getenvstealerase)", FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT, NULL);
+ if (r == 0) {
+ _cleanup_strv_free_ char **l = NULL;
+
+ /* child */
+
+ assert_se(getenv_steal_erase("thisenvvardefinitelywontexist", NULL) == 0);
+
+ l = strv_new("FOO=BAR", "QUUX=PIFF", "ONE=TWO", "A=B");
+ assert_se(strv_length(l) == 4);
+
+ environ = l;
+
+ STRV_FOREACH(e, environ) {
+ _cleanup_free_ char *n = NULL, *copy1 = NULL, *copy2 = NULL;
+ char *eq;
+
+ eq = strchr(*e, '=');
+ if (!eq)
+ continue;
+
+ n = strndup(*e, eq - *e);
+ assert_se(n);
+
+ copy1 = strdup(eq + 1);
+ assert_se(copy1);
+
+ assert_se(streq_ptr(getenv(n), copy1));
+ assert_se(getenv(n) == eq + 1);
+ assert_se(getenv_steal_erase(n, &copy2) > 0);
+ assert_se(streq_ptr(copy1, copy2));
+ assert_se(isempty(eq + 1));
+ assert_se(!getenv(n));
+ }
+
+ environ = NULL;
+ l = strv_free(l);
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(r > 0);
+}
+
+TEST(strv_env_name_is_valid) {
+ assert_se(strv_env_name_is_valid(STRV_MAKE("HOME", "USER", "SHELL", "PATH")));
+ assert_se(!strv_env_name_is_valid(STRV_MAKE("", "PATH", "home", "user", "SHELL")));
+ assert_se(!strv_env_name_is_valid(STRV_MAKE("HOME", "USER", "SHELL", "USER")));
+}
+
+TEST(getenv_path_list) {
+ _cleanup_strv_free_ char **path_list = NULL;
+
+ /* Empty paths */
+ FOREACH_STRING(s, "", ":", ":::::", " : ::: :: :") {
+ assert_se(setenv("TEST_GETENV_PATH_LIST", s, 1) >= 0);
+ assert_se(getenv_path_list("TEST_GETENV_PATH_LIST", &path_list) == -EINVAL);
+ assert_se(!path_list);
+ }
+
+ /* Invalid paths */
+ FOREACH_STRING(s, ".", "..", "/../", "/", "/foo/bar/baz/../foo", "foo/bar/baz") {
+ assert_se(setenv("TEST_GETENV_PATH_LIST", s, 1) >= 0);
+ assert_se(getenv_path_list("TEST_GETENV_PATH_LIST", &path_list) == -EINVAL);
+ assert_se(!path_list);
+ }
+
+ /* Valid paths mixed with invalid ones */
+ assert_se(setenv("TEST_GETENV_PATH_LIST", "/foo:/bar/baz:/../:/hello", 1) >= 0);
+ assert_se(getenv_path_list("TEST_GETENV_PATH_LIST", &path_list) == -EINVAL);
+ assert_se(!path_list);
+
+ /* Finally some valid paths */
+ assert_se(setenv("TEST_GETENV_PATH_LIST", "/foo:/bar/baz:/hello/world:/path with spaces:/final", 1) >= 0);
+ assert_se(getenv_path_list("TEST_GETENV_PATH_LIST", &path_list) >= 0);
+ assert_se(streq(path_list[0], "/foo"));
+ assert_se(streq(path_list[1], "/bar/baz"));
+ assert_se(streq(path_list[2], "/hello/world"));
+ assert_se(streq(path_list[3], "/path with spaces"));
+ assert_se(streq(path_list[4], "/final"));
+ assert_se(path_list[5] == NULL);
+
+ assert_se(unsetenv("TEST_GETENV_PATH_LIST") >= 0);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-errno-list.c b/src/test/test-errno-list.c
new file mode 100644
index 0000000..f91a1f7
--- /dev/null
+++ b/src/test/test-errno-list.c
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+
+#include "errno-list.h"
+#include "errno-to-name.h"
+#include "macro.h"
+#include "string-util.h"
+#include "tests.h"
+
+TEST(errno_list) {
+ for (size_t i = 0; i < ELEMENTSOF(errno_names); i++) {
+ if (errno_names[i]) {
+ assert_se(streq(errno_to_name(i), errno_names[i]));
+ assert_se(errno_from_name(errno_names[i]) == (int) i);
+ }
+ }
+
+#ifdef ECANCELLED
+ /* ECANCELLED is an alias of ECANCELED. */
+ assert_se(streq(errno_to_name(ECANCELLED), "ECANCELED"));
+#endif
+ assert_se(streq(errno_to_name(ECANCELED), "ECANCELED"));
+
+#ifdef EREFUSED
+ /* EREFUSED is an alias of ECONNREFUSED. */
+ assert_se(streq(errno_to_name(EREFUSED), "ECONNREFUSED"));
+#endif
+ assert_se(streq(errno_to_name(ECONNREFUSED), "ECONNREFUSED"));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-errno-util.c b/src/test/test-errno-util.c
new file mode 100644
index 0000000..376d532
--- /dev/null
+++ b/src/test/test-errno-util.c
@@ -0,0 +1,112 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "errno-util.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "tests.h"
+
+TEST(strerror_not_threadsafe) {
+ /* Just check that strerror really is not thread-safe. */
+ log_info("strerror(%d) → %s", 200, strerror(200));
+ log_info("strerror(%d) → %s", 201, strerror(201));
+ log_info("strerror(%d) → %s", INT_MAX, strerror(INT_MAX));
+
+ log_info("strerror(%d), strerror(%d) → %p, %p", 200, 201, strerror(200), strerror(201));
+
+ /* This call is not allowed, because the first returned string becomes invalid when
+ * we call strerror the second time:
+ *
+ * log_info("strerror(%d), strerror(%d) → %s, %s", 200, 201, strerror(200), strerror(201));
+ */
+}
+
+TEST(STRERROR) {
+ /* Just check that STRERROR really is thread-safe. */
+ log_info("STRERROR(%d) → %s", 200, STRERROR(200));
+ log_info("STRERROR(%d) → %s", 201, STRERROR(201));
+ log_info("STRERROR(%d), STRERROR(%d) → %s, %s", 200, 201, STRERROR(200), STRERROR(201));
+
+ const char *a = STRERROR(200), *b = STRERROR(201);
+ assert_se(strstr(a, "200"));
+ assert_se(strstr(b, "201"));
+
+ /* Check with negative values */
+ assert_se(streq(a, STRERROR(-200)));
+ assert_se(streq(b, STRERROR(-201)));
+
+ const char *c = STRERROR(INT_MAX);
+ char buf[DECIMAL_STR_MAX(int)];
+ xsprintf(buf, "%d", INT_MAX); /* INT_MAX is hexadecimal, use printf to convert to decimal */
+ log_info("STRERROR(%d) → %s", INT_MAX, c);
+ assert_se(strstr(c, buf));
+}
+
+TEST(STRERROR_OR_ELSE) {
+ log_info("STRERROR_OR_ELSE(0, \"EOF\") → %s", STRERROR_OR_EOF(0));
+ log_info("STRERROR_OR_ELSE(EPERM, \"EOF\") → %s", STRERROR_OR_EOF(EPERM));
+ log_info("STRERROR_OR_ELSE(-EPERM, \"EOF\") → %s", STRERROR_OR_EOF(-EPERM));
+}
+
+TEST(PROTECT_ERRNO) {
+ errno = 12;
+ {
+ PROTECT_ERRNO;
+ errno = 11;
+ }
+ assert_se(errno == 12);
+}
+
+static void test_unprotect_errno_inner_function(void) {
+ PROTECT_ERRNO;
+
+ errno = 2222;
+}
+
+TEST(UNPROTECT_ERRNO) {
+ errno = 4711;
+
+ PROTECT_ERRNO;
+
+ errno = 815;
+
+ UNPROTECT_ERRNO;
+
+ assert_se(errno == 4711);
+
+ test_unprotect_errno_inner_function();
+
+ assert_se(errno == 4711);
+}
+
+TEST(RET_GATHER) {
+ int x = 0, y = 2;
+
+ assert_se(RET_GATHER(x, 5) == 0);
+ assert_se(RET_GATHER(x, -5) == -5);
+ assert_se(RET_GATHER(x, -1) == -5);
+
+ assert_se(RET_GATHER(x, y++) == -5);
+ assert_se(y == 3);
+}
+
+TEST(ERRNO_IS_TRANSIENT) {
+ assert_se( ERRNO_IS_NEG_TRANSIENT(-EINTR));
+ assert_se(!ERRNO_IS_NEG_TRANSIENT(EINTR));
+ assert_se( ERRNO_IS_TRANSIENT(-EINTR));
+ assert_se( ERRNO_IS_TRANSIENT(EINTR));
+
+ /* Test with type wider than int */
+ ssize_t r = -EAGAIN;
+ assert_se( ERRNO_IS_NEG_TRANSIENT(r));
+
+ /* On 64-bit arches, now (int) r == EAGAIN */
+ r = SSIZE_MAX - EAGAIN + 1;
+ assert_se(!ERRNO_IS_NEG_TRANSIENT(r));
+
+ assert_se(!ERRNO_IS_NEG_TRANSIENT(INT_MAX));
+ assert_se(!ERRNO_IS_NEG_TRANSIENT(INT_MIN));
+ assert_se(!ERRNO_IS_NEG_TRANSIENT(INTMAX_MAX));
+ assert_se(!ERRNO_IS_NEG_TRANSIENT(INTMAX_MIN));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-escape.c b/src/test/test-escape.c
new file mode 100644
index 0000000..21786ae
--- /dev/null
+++ b/src/test/test-escape.c
@@ -0,0 +1,242 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "escape.h"
+#include "macro.h"
+#include "tests.h"
+
+TEST(cescape) {
+ _cleanup_free_ char *t = NULL;
+
+ assert_se(t = cescape("abc\\\"\b\f\n\r\t\v\a\003\177\234\313"));
+ assert_se(streq(t, "abc\\\\\\\"\\b\\f\\n\\r\\t\\v\\a\\003\\177\\234\\313"));
+}
+
+TEST(xescape) {
+ _cleanup_free_ char *t = NULL;
+
+ assert_se(t = xescape("abc\\\"\b\f\n\r\t\v\a\003\177\234\313", ""));
+ assert_se(streq(t, "abc\\x5c\"\\x08\\x0c\\x0a\\x0d\\x09\\x0b\\x07\\x03\\x7f\\x9c\\xcb"));
+}
+
+static void test_xescape_full_one(bool eight_bits) {
+ const char* escaped = !eight_bits ?
+ "a\\x62c\\x5c\"\\x08\\x0c\\x0a\\x0d\\x09\\x0b\\x07\\x03\\x7f\\x9c\\xcb" :
+ "a\\x62c\\x5c\"\\x08\\x0c\\x0a\\x0d\\x09\\x0b\\x07\\x03\177\234\313";
+ const unsigned full_fit = !eight_bits ? 55 : 46;
+ XEscapeFlags flags = eight_bits * XESCAPE_8_BIT;
+
+ log_info("/* %s */", __func__);
+
+ for (unsigned i = 0; i < 60; i++) {
+ _cleanup_free_ char *t = NULL, *q = NULL;
+
+ assert_se(t = xescape_full("abc\\\"\b\f\n\r\t\v\a\003\177\234\313", "b", i, flags));
+
+ log_info("%02u: <%s>", i, t);
+
+ if (i >= full_fit)
+ assert_se(streq(t, escaped));
+ else if (i >= 3) {
+ /* We need up to four columns, so up to three columns may be wasted */
+ assert_se(strlen(t) == i || strlen(t) == i - 1 || strlen(t) == i - 2 || strlen(t) == i - 3);
+ assert_se(strneq(t, escaped, i - 3) || strneq(t, escaped, i - 4) ||
+ strneq(t, escaped, i - 5) || strneq(t, escaped, i - 6));
+ assert_se(endswith(t, "..."));
+ } else {
+ assert_se(strlen(t) == i);
+ assert_se(strneq(t, "...", i));
+ }
+
+ assert_se(q = xescape_full("abc\\\"\b\f\n\r\t\v\a\003\177\234\313", "b", i,
+ flags | XESCAPE_FORCE_ELLIPSIS));
+
+ log_info("%02u: <%s>", i, q);
+ if (i > 0)
+ assert_se(endswith(q, "."));
+ assert_se(strlen(q) <= i);
+ assert_se(strlen(q) + 3 >= strlen(t));
+ }
+}
+
+TEST(xescape_full) {
+ test_xescape_full_one(false);
+ test_xescape_full_one(true);
+}
+
+TEST(cunescape) {
+ _cleanup_free_ char *unescaped = NULL;
+
+ assert_se(cunescape("abc\\\\\\\"\\b\\f\\a\\n\\r\\t\\v\\003\\177\\234\\313\\000\\x00", 0, &unescaped) < 0);
+ assert_se(cunescape("abc\\\\\\\"\\b\\f\\a\\n\\r\\t\\v\\003\\177\\234\\313\\000\\x00", UNESCAPE_RELAX, &unescaped) >= 0);
+ assert_se(streq_ptr(unescaped, "abc\\\"\b\f\a\n\r\t\v\003\177\234\313\\000\\x00"));
+ unescaped = mfree(unescaped);
+
+ /* incomplete sequences */
+ assert_se(cunescape("\\x0", 0, &unescaped) < 0);
+ assert_se(cunescape("\\x0", UNESCAPE_RELAX, &unescaped) >= 0);
+ assert_se(streq_ptr(unescaped, "\\x0"));
+ unescaped = mfree(unescaped);
+
+ assert_se(cunescape("\\x", 0, &unescaped) < 0);
+ assert_se(cunescape("\\x", UNESCAPE_RELAX, &unescaped) >= 0);
+ assert_se(streq_ptr(unescaped, "\\x"));
+ unescaped = mfree(unescaped);
+
+ assert_se(cunescape("\\", 0, &unescaped) < 0);
+ assert_se(cunescape("\\", UNESCAPE_RELAX, &unescaped) >= 0);
+ assert_se(streq_ptr(unescaped, "\\"));
+ unescaped = mfree(unescaped);
+
+ assert_se(cunescape("\\11", 0, &unescaped) < 0);
+ assert_se(cunescape("\\11", UNESCAPE_RELAX, &unescaped) >= 0);
+ assert_se(streq_ptr(unescaped, "\\11"));
+ unescaped = mfree(unescaped);
+
+ assert_se(cunescape("\\1", 0, &unescaped) < 0);
+ assert_se(cunescape("\\1", UNESCAPE_RELAX, &unescaped) >= 0);
+ assert_se(streq_ptr(unescaped, "\\1"));
+ unescaped = mfree(unescaped);
+
+ assert_se(cunescape("\\u0000", 0, &unescaped) < 0);
+ assert_se(cunescape("\\u00DF\\U000000df\\u03a0\\U00000041", UNESCAPE_RELAX, &unescaped) >= 0);
+ assert_se(streq_ptr(unescaped, "ßßΠA"));
+ unescaped = mfree(unescaped);
+
+ assert_se(cunescape("\\073", 0, &unescaped) >= 0);
+ assert_se(streq_ptr(unescaped, ";"));
+ unescaped = mfree(unescaped);
+
+ assert_se(cunescape("A=A\\\\x0aB", 0, &unescaped) >= 0);
+ assert_se(streq_ptr(unescaped, "A=A\\x0aB"));
+ unescaped = mfree(unescaped);
+
+ assert_se(cunescape("A=A\\\\x0aB", UNESCAPE_RELAX, &unescaped) >= 0);
+ assert_se(streq_ptr(unescaped, "A=A\\x0aB"));
+ unescaped = mfree(unescaped);
+
+ assert_se(cunescape("\\x00\\x00\\x00", UNESCAPE_ACCEPT_NUL, &unescaped) == 3);
+ assert_se(memcmp(unescaped, "\0\0\0", 3) == 0);
+ unescaped = mfree(unescaped);
+
+ assert_se(cunescape("\\u0000\\u0000\\u0000", UNESCAPE_ACCEPT_NUL, &unescaped) == 3);
+ assert_se(memcmp(unescaped, "\0\0\0", 3) == 0);
+ unescaped = mfree(unescaped);
+
+ assert_se(cunescape("\\U00000000\\U00000000\\U00000000", UNESCAPE_ACCEPT_NUL, &unescaped) == 3);
+ assert_se(memcmp(unescaped, "\0\0\0", 3) == 0);
+ unescaped = mfree(unescaped);
+
+ assert_se(cunescape("\\000\\000\\000", UNESCAPE_ACCEPT_NUL, &unescaped) == 3);
+ assert_se(memcmp(unescaped, "\0\0\0", 3) == 0);
+}
+
+static void test_shell_escape_one(const char *s, const char *bad, const char *expected) {
+ _cleanup_free_ char *r = NULL;
+
+ assert_se(r = shell_escape(s, bad));
+ log_debug("%s → %s (expected %s)", s, r, expected);
+ assert_se(streq_ptr(r, expected));
+}
+
+TEST(shell_escape) {
+ test_shell_escape_one("", "", "");
+ test_shell_escape_one("\\", "", "\\\\");
+ test_shell_escape_one("foobar", "", "foobar");
+ test_shell_escape_one("foobar", "o", "f\\o\\obar");
+ test_shell_escape_one("foo:bar,baz", ",:", "foo\\:bar\\,baz");
+ test_shell_escape_one("foo\nbar\nbaz", ",:", "foo\\nbar\\nbaz");
+}
+
+static void test_shell_maybe_quote_one(const char *s, ShellEscapeFlags flags, const char *expected) {
+ _cleanup_free_ char *ret = NULL;
+
+ assert_se(ret = shell_maybe_quote(s, flags));
+ log_debug("[%s] → [%s] (%s)", s, ret, expected);
+ assert_se(streq(ret, expected));
+}
+
+TEST(shell_maybe_quote) {
+ test_shell_maybe_quote_one("", 0, "");
+ test_shell_maybe_quote_one("", SHELL_ESCAPE_EMPTY, "\"\"");
+ test_shell_maybe_quote_one("", SHELL_ESCAPE_POSIX, "");
+ test_shell_maybe_quote_one("", SHELL_ESCAPE_POSIX | SHELL_ESCAPE_EMPTY, "\"\"");
+ test_shell_maybe_quote_one("\\", 0, "\"\\\\\"");
+ test_shell_maybe_quote_one("\\", SHELL_ESCAPE_POSIX, "$'\\\\'");
+ test_shell_maybe_quote_one("\"", 0, "\"\\\"\"");
+ test_shell_maybe_quote_one("\"", SHELL_ESCAPE_POSIX, "$'\"'");
+ test_shell_maybe_quote_one("foobar", 0, "foobar");
+ test_shell_maybe_quote_one("foobar", SHELL_ESCAPE_POSIX, "foobar");
+ test_shell_maybe_quote_one("foo bar", 0, "\"foo bar\"");
+ test_shell_maybe_quote_one("foo bar", SHELL_ESCAPE_POSIX, "$'foo bar'");
+ test_shell_maybe_quote_one("foo\tbar", 0, "\"foo\\tbar\"");
+ test_shell_maybe_quote_one("foo\tbar", SHELL_ESCAPE_POSIX, "$'foo\\tbar'");
+ test_shell_maybe_quote_one("foo\nbar", 0, "\"foo\\nbar\"");
+ test_shell_maybe_quote_one("foo\nbar", SHELL_ESCAPE_POSIX, "$'foo\\nbar'");
+ test_shell_maybe_quote_one("foo \"bar\" waldo", 0, "\"foo \\\"bar\\\" waldo\"");
+ test_shell_maybe_quote_one("foo \"bar\" waldo", SHELL_ESCAPE_POSIX, "$'foo \"bar\" waldo'");
+ test_shell_maybe_quote_one("foo$bar", 0, "\"foo\\$bar\"");
+ test_shell_maybe_quote_one("foo$bar", SHELL_ESCAPE_EMPTY, "\"foo\\$bar\"");
+ test_shell_maybe_quote_one("foo$bar", SHELL_ESCAPE_POSIX, "$'foo$bar'");
+ test_shell_maybe_quote_one("foo$bar", SHELL_ESCAPE_POSIX | SHELL_ESCAPE_EMPTY, "$'foo$bar'");
+
+ /* Exclamation mark is special in the interactive shell, but we don't treat it so. */
+ test_shell_maybe_quote_one("foo!bar", 0, "\"foo!bar\"");
+ test_shell_maybe_quote_one("foo!bar", SHELL_ESCAPE_POSIX, "$'foo!bar'");
+
+ /* Control characters and unicode */
+ test_shell_maybe_quote_one("a\nb\001", 0, "\"a\\nb\\001\"");
+ test_shell_maybe_quote_one("a\nb\001", SHELL_ESCAPE_POSIX, "$'a\\nb\\001'");
+
+ test_shell_maybe_quote_one("głąb", 0, "głąb");
+ test_shell_maybe_quote_one("głąb", SHELL_ESCAPE_POSIX, "głąb");
+
+ test_shell_maybe_quote_one("głąb\002\003", 0, "\"głąb\\002\\003\"");
+ test_shell_maybe_quote_one("głąb\002\003", SHELL_ESCAPE_POSIX, "$'głąb\\002\\003'");
+
+ test_shell_maybe_quote_one("głąb\002\003rząd", 0, "\"głąb\\002\\003rząd\"");
+ test_shell_maybe_quote_one("głąb\002\003rząd", SHELL_ESCAPE_POSIX, "$'głąb\\002\\003rząd'");
+
+ /* Bogus UTF-8 strings */
+ test_shell_maybe_quote_one("\250\350", 0, "\"\\250\\350\"");
+ test_shell_maybe_quote_one("\250\350", SHELL_ESCAPE_POSIX, "$'\\250\\350'");
+}
+
+static void test_quote_command_line_one(char **argv, const char *expected) {
+ _cleanup_free_ char *s = NULL;
+
+ assert_se(s = quote_command_line(argv, SHELL_ESCAPE_EMPTY));
+ log_info("%s", s);
+ assert_se(streq(s, expected));
+}
+
+TEST(quote_command_line) {
+ test_quote_command_line_one(STRV_MAKE("true", "true"),
+ "true true");
+ test_quote_command_line_one(STRV_MAKE("true", "with a space"),
+ "true \"with a space\"");
+ test_quote_command_line_one(STRV_MAKE("true", "with a 'quote'"),
+ "true \"with a 'quote'\"");
+ test_quote_command_line_one(STRV_MAKE("true", "with a \"quote\""),
+ "true \"with a \\\"quote\\\"\"");
+ test_quote_command_line_one(STRV_MAKE("true", "$dollar"),
+ "true \"\\$dollar\"");
+}
+
+static void test_octescape_one(const char *s, const char *expected) {
+ _cleanup_free_ char *ret = NULL;
+
+ assert_se(ret = octescape(s, strlen_ptr(s)));
+ log_debug("octescape(\"%s\") → \"%s\" (expected: \"%s\")", strnull(s), ret, expected);
+ assert_se(streq(ret, expected));
+}
+
+TEST(octescape) {
+ test_octescape_one(NULL, "");
+ test_octescape_one("", "");
+ test_octescape_one("foo", "foo");
+ test_octescape_one("\"\\\"", "\\042\\134\\042");
+ test_octescape_one("\123\213\222", "\123\\213\\222");
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-ether-addr-util.c b/src/test/test-ether-addr-util.c
new file mode 100644
index 0000000..d680f80
--- /dev/null
+++ b/src/test/test-ether-addr-util.c
@@ -0,0 +1,162 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "ether-addr-util.h"
+#include "string-util.h"
+#include "tests.h"
+
+TEST(ether_addr_helpers) {
+ struct ether_addr a;
+
+ a = ETHER_ADDR_NULL;
+ assert_se(ether_addr_is_null(&a));
+ assert_se(!ether_addr_is_broadcast(&a));
+ assert_se(!ether_addr_is_multicast(&a));
+ assert_se(ether_addr_is_unicast(&a));
+ assert_se(!ether_addr_is_local(&a));
+ assert_se(ether_addr_is_global(&a));
+
+ memset(a.ether_addr_octet, 0xff, sizeof(a));
+ assert_se(!ether_addr_is_null(&a));
+ assert_se(ether_addr_is_broadcast(&a));
+ assert_se(ether_addr_is_multicast(&a));
+ assert_se(!ether_addr_is_unicast(&a));
+ assert_se(ether_addr_is_local(&a));
+ assert_se(!ether_addr_is_global(&a));
+
+ a = (struct ether_addr) { { 0x01, 0x23, 0x34, 0x56, 0x78, 0x9a } };
+ assert_se(!ether_addr_is_null(&a));
+ assert_se(!ether_addr_is_broadcast(&a));
+ assert_se(ether_addr_is_multicast(&a));
+ assert_se(!ether_addr_is_unicast(&a));
+ assert_se(!ether_addr_is_local(&a));
+ assert_se(ether_addr_is_global(&a));
+}
+
+#define INFINIBAD_ADDR_1 ((const struct hw_addr_data){ .length = 20, .infiniband = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20} })
+
+TEST(HW_ADDR_TO_STRING) {
+ const char *s = HW_ADDR_TO_STR(&(const struct hw_addr_data){6});
+ log_info("null: %s", s);
+
+ log_info("null×2: %s, %s",
+ HW_ADDR_TO_STR(&(const struct hw_addr_data){6}),
+ HW_ADDR_TO_STR(&(const struct hw_addr_data){6}));
+ log_info("null×3: %s, %s, %s",
+ HW_ADDR_TO_STR(&(const struct hw_addr_data){6}),
+ s,
+ HW_ADDR_TO_STR(&(const struct hw_addr_data){6}));
+
+ log_info("infiniband: %s", HW_ADDR_TO_STR(&INFINIBAD_ADDR_1));
+
+ /* Let's nest function calls in a stupid way. */
+ _cleanup_free_ char *t = NULL;
+ log_info("infiniband×3: %s\n%14s%s\n%14s%s",
+ HW_ADDR_TO_STR(&(const struct hw_addr_data){20}), "",
+ t = strdup(HW_ADDR_TO_STR(&INFINIBAD_ADDR_1)), "",
+ HW_ADDR_TO_STR(&(const struct hw_addr_data){20}));
+
+ const char *p;
+ /* Let's use a separate selection statement */
+ if ((p = HW_ADDR_TO_STR(&(const struct hw_addr_data){6})))
+ log_info("joint: %s, %s", s, p);
+}
+
+static void test_parse_hw_addr_full_one(const char *in, size_t expected_len, const char *expected) {
+ struct hw_addr_data h;
+ int r;
+
+ r = parse_hw_addr_full(in, expected_len, &h);
+ log_debug_errno(r, "parse_hw_addr(\"%s\", len=%zu) → \"%s\" (expected: \"%s\") : %d/%m",
+ in, expected_len, r >= 0 ? HW_ADDR_TO_STR(&h) : "n/a", strna(expected), r);
+ assert_se((r >= 0) == !!expected);
+ if (r >= 0) {
+ if (!IN_SET(expected_len, 0, SIZE_MAX))
+ assert_se(h.length == expected_len);
+ assert_se(streq(HW_ADDR_TO_STR(&h), expected));
+ }
+}
+
+TEST(parse_hw_addr) {
+ /* IPv4 */
+ test_parse_hw_addr_full_one("10.0.0.1", 0, "0a:00:00:01");
+ test_parse_hw_addr_full_one("10.0.0.1", 4, "0a:00:00:01");
+ test_parse_hw_addr_full_one("192.168.0.1", 0, "c0:a8:00:01");
+ test_parse_hw_addr_full_one("192.168.0.1", 4, "c0:a8:00:01");
+ /* IPv6 */
+ test_parse_hw_addr_full_one("::", 0, "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00");
+ test_parse_hw_addr_full_one("::", 16, "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00");
+ test_parse_hw_addr_full_one("::1", 0, "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:01");
+ test_parse_hw_addr_full_one("::1", 16, "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:01");
+ test_parse_hw_addr_full_one("1234::", 0, "12:34:00:00:00:00:00:00:00:00:00:00:00:00:00:00");
+ test_parse_hw_addr_full_one("1234::", 16, "12:34:00:00:00:00:00:00:00:00:00:00:00:00:00:00");
+ test_parse_hw_addr_full_one("12:34::56", 0, "00:12:00:34:00:00:00:00:00:00:00:00:00:00:00:56");
+ test_parse_hw_addr_full_one("12:34::56", 16, "00:12:00:34:00:00:00:00:00:00:00:00:00:00:00:56");
+ test_parse_hw_addr_full_one("12aa:34::56", 0, "12:aa:00:34:00:00:00:00:00:00:00:00:00:00:00:56");
+ test_parse_hw_addr_full_one("12aa:34::56", 16, "12:aa:00:34:00:00:00:00:00:00:00:00:00:00:00:56");
+ test_parse_hw_addr_full_one("1234:5678:90ab:cdef:1234:5678:90ab:cdef", 0, "12:34:56:78:90:ab:cd:ef:12:34:56:78:90:ab:cd:ef");
+ test_parse_hw_addr_full_one("1234:5678:90ab:cdef:1234:5678:90ab:cdef", 16, "12:34:56:78:90:ab:cd:ef:12:34:56:78:90:ab:cd:ef");
+ /* Dot */
+ test_parse_hw_addr_full_one("12.34", 0, "00:12:00:34");
+ test_parse_hw_addr_full_one("12.34", 4, "00:12:00:34");
+ test_parse_hw_addr_full_one("12.34", SIZE_MAX, "00:12:00:34");
+ test_parse_hw_addr_full_one("12.34.56", 0, "00:12:00:34:00:56");
+ test_parse_hw_addr_full_one("12.34.56", 6, "00:12:00:34:00:56");
+ test_parse_hw_addr_full_one("12.34.56", SIZE_MAX, "00:12:00:34:00:56");
+ test_parse_hw_addr_full_one("12.34.56.78", 0, "0c:22:38:4e"); /* IPv4 address */
+ test_parse_hw_addr_full_one("12.34.56.78", 4, "0c:22:38:4e"); /* IPv4 address */
+ test_parse_hw_addr_full_one("12.34.56.78", 8, "00:12:00:34:00:56:00:78");
+ test_parse_hw_addr_full_one("12.34.56.78", SIZE_MAX, "00:12:00:34:00:56:00:78");
+ test_parse_hw_addr_full_one("12.34.56.78.90", 0, NULL);
+ test_parse_hw_addr_full_one("12.34.56.78.90", 10, "00:12:00:34:00:56:00:78:00:90");
+ test_parse_hw_addr_full_one("12.34.56.78.90", SIZE_MAX, "00:12:00:34:00:56:00:78:00:90");
+ test_parse_hw_addr_full_one("aabb.ccdd", 0, "aa:bb:cc:dd");
+ test_parse_hw_addr_full_one("aabb.ccdd", 4, "aa:bb:cc:dd");
+ test_parse_hw_addr_full_one("aabb.ccdd", SIZE_MAX, "aa:bb:cc:dd");
+ test_parse_hw_addr_full_one("aabb.ccdd.eeff", 0, "aa:bb:cc:dd:ee:ff");
+ test_parse_hw_addr_full_one("aabb.ccdd.eeff", 6, "aa:bb:cc:dd:ee:ff");
+ test_parse_hw_addr_full_one("aabb.ccdd.eeff", SIZE_MAX, "aa:bb:cc:dd:ee:ff");
+ /* Colon */
+ test_parse_hw_addr_full_one("12:34", 0, NULL);
+ test_parse_hw_addr_full_one("12:34", 2, "12:34");
+ test_parse_hw_addr_full_one("12:34", SIZE_MAX, "12:34");
+ test_parse_hw_addr_full_one("12:34:56:78:90:ab", 0, "12:34:56:78:90:ab");
+ test_parse_hw_addr_full_one("12:34:56:78:90:ab", 6, "12:34:56:78:90:ab");
+ test_parse_hw_addr_full_one("12:34:56:78:90:ab", SIZE_MAX, "12:34:56:78:90:ab");
+ test_parse_hw_addr_full_one("12:34:56:78:90:ab:cd:ef", 0, "00:12:00:34:00:56:00:78:00:90:00:ab:00:cd:00:ef"); /* IPv6 */
+ test_parse_hw_addr_full_one("12:34:56:78:90:ab:cd:ef", 8, "12:34:56:78:90:ab:cd:ef");
+ test_parse_hw_addr_full_one("12:34:56:78:90:ab:cd:ef", 16, "00:12:00:34:00:56:00:78:00:90:00:ab:00:cd:00:ef"); /* IPv6 */
+ test_parse_hw_addr_full_one("12:34:56:78:90:ab:cd:ef", SIZE_MAX, "12:34:56:78:90:ab:cd:ef");
+ test_parse_hw_addr_full_one("12:34:56:78:90:AB:CD:EF", 0, "00:12:00:34:00:56:00:78:00:90:00:ab:00:cd:00:ef"); /* IPv6 */
+ test_parse_hw_addr_full_one("12:34:56:78:90:AB:CD:EF", 8, "12:34:56:78:90:ab:cd:ef");
+ test_parse_hw_addr_full_one("12:34:56:78:90:AB:CD:EF", 16, "00:12:00:34:00:56:00:78:00:90:00:ab:00:cd:00:ef"); /* IPv6 */
+ test_parse_hw_addr_full_one("12:34:56:78:90:AB:CD:EF", SIZE_MAX, "12:34:56:78:90:ab:cd:ef");
+ /* Hyphen */
+ test_parse_hw_addr_full_one("12-34", 0, NULL);
+ test_parse_hw_addr_full_one("12-34", 2, "12:34");
+ test_parse_hw_addr_full_one("12-34", SIZE_MAX, "12:34");
+ test_parse_hw_addr_full_one("12-34-56-78-90-ab-cd-ef", 0, NULL);
+ test_parse_hw_addr_full_one("12-34-56-78-90-ab-cd-ef", 8, "12:34:56:78:90:ab:cd:ef");
+ test_parse_hw_addr_full_one("12-34-56-78-90-ab-cd-ef", SIZE_MAX, "12:34:56:78:90:ab:cd:ef");
+ test_parse_hw_addr_full_one("12-34-56-78-90-AB-CD-EF", 0, NULL);
+ test_parse_hw_addr_full_one("12-34-56-78-90-AB-CD-EF", 8, "12:34:56:78:90:ab:cd:ef");
+ test_parse_hw_addr_full_one("12-34-56-78-90-AB-CD-EF", SIZE_MAX, "12:34:56:78:90:ab:cd:ef");
+
+ /* Invalid */
+ test_parse_hw_addr_full_one("", SIZE_MAX, NULL);
+ test_parse_hw_addr_full_one("12", SIZE_MAX, NULL);
+ test_parse_hw_addr_full_one("12.", SIZE_MAX, NULL);
+ test_parse_hw_addr_full_one("12.34.", SIZE_MAX, NULL);
+ test_parse_hw_addr_full_one(".12", SIZE_MAX, NULL);
+ test_parse_hw_addr_full_one(".12.34", SIZE_MAX, NULL);
+ test_parse_hw_addr_full_one("12.34:56", SIZE_MAX, NULL);
+ test_parse_hw_addr_full_one("1234:56", SIZE_MAX, NULL);
+ test_parse_hw_addr_full_one("1234:56", SIZE_MAX, NULL);
+ test_parse_hw_addr_full_one("12:34:", SIZE_MAX, NULL);
+ test_parse_hw_addr_full_one(":12:34", SIZE_MAX, NULL);
+ test_parse_hw_addr_full_one("::1", SIZE_MAX, NULL);
+ test_parse_hw_addr_full_one("aa:bb-cc", SIZE_MAX, NULL);
+ test_parse_hw_addr_full_one("aa:xx", SIZE_MAX, NULL);
+ test_parse_hw_addr_full_one("aa bb", SIZE_MAX, NULL);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-exec-util.c b/src/test/test-exec-util.c
new file mode 100644
index 0000000..2304f6a
--- /dev/null
+++ b/src/test/test-exec-util.c
@@ -0,0 +1,456 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "copy.h"
+#include "constants.h"
+#include "env-util.h"
+#include "exec-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "log.h"
+#include "macro.h"
+#include "path-util.h"
+#include "rm-rf.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+static int here = 0, here2 = 0, here3 = 0;
+static void *ignore_stdout_args[] = { &here, &here2, &here3 };
+
+/* noop handlers, just check that arguments are passed correctly */
+static int ignore_stdout_func(int fd, void *arg) {
+ assert_se(fd >= 0);
+ assert_se(arg == &here);
+ safe_close(fd);
+
+ return 0;
+}
+static int ignore_stdout_func2(int fd, void *arg) {
+ assert_se(fd >= 0);
+ assert_se(arg == &here2);
+ safe_close(fd);
+
+ return 0;
+}
+static int ignore_stdout_func3(int fd, void *arg) {
+ assert_se(fd >= 0);
+ assert_se(arg == &here3);
+ safe_close(fd);
+
+ return 0;
+}
+
+static const gather_stdout_callback_t ignore_stdout[] = {
+ ignore_stdout_func,
+ ignore_stdout_func2,
+ ignore_stdout_func3,
+};
+
+static void test_execute_directory_one(bool gather_stdout) {
+ _cleanup_(rm_rf_physical_and_freep) char *tmp_lo = NULL, *tmp_hi = NULL;
+ const char *name, *name2, *name3,
+ *overridden, *override,
+ *masked, *mask,
+ *masked2, *mask2, /* the mask is non-executable */
+ *masked2e, *mask2e; /* the mask is executable */
+
+ log_info("/* %s (%s) */", __func__, gather_stdout ? "gathering stdout" : "asynchronous");
+
+ assert_se(mkdtemp_malloc("/tmp/test-exec-util.lo.XXXXXXX", &tmp_lo) >= 0);
+ assert_se(mkdtemp_malloc("/tmp/test-exec-util.hi.XXXXXXX", &tmp_hi) >= 0);
+
+ const char * dirs[] = { tmp_hi, tmp_lo, NULL };
+
+ name = strjoina(tmp_lo, "/script");
+ name2 = strjoina(tmp_hi, "/script2");
+ name3 = strjoina(tmp_lo, "/useless");
+ overridden = strjoina(tmp_lo, "/overridden");
+ override = strjoina(tmp_hi, "/overridden");
+ masked = strjoina(tmp_lo, "/masked");
+ mask = strjoina(tmp_hi, "/masked");
+ masked2 = strjoina(tmp_lo, "/masked2");
+ mask2 = strjoina(tmp_hi, "/masked2");
+ masked2e = strjoina(tmp_lo, "/masked2e");
+ mask2e = strjoina(tmp_hi, "/masked2e");
+
+ assert_se(write_string_file(name,
+ "#!/bin/sh\necho 'Executing '$0\ntouch $(dirname $0)/it_works",
+ WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(write_string_file(name2,
+ "#!/bin/sh\necho 'Executing '$0\ntouch $(dirname $0)/it_works2",
+ WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(write_string_file(overridden,
+ "#!/bin/sh\necho 'Executing '$0\ntouch $(dirname $0)/failed",
+ WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(write_string_file(override,
+ "#!/bin/sh\necho 'Executing '$0",
+ WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(write_string_file(masked,
+ "#!/bin/sh\necho 'Executing '$0\ntouch $(dirname $0)/failed",
+ WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(write_string_file(masked2,
+ "#!/bin/sh\necho 'Executing '$0\ntouch $(dirname $0)/failed",
+ WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(write_string_file(masked2e,
+ "#!/bin/sh\necho 'Executing '$0\ntouch $(dirname $0)/failed",
+ WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(symlink("/dev/null", mask) == 0);
+ assert_se(touch(mask2) == 0);
+ assert_se(touch(mask2e) == 0);
+ assert_se(touch(name3) >= 0);
+
+ assert_se(chmod(name, 0755) == 0);
+ assert_se(chmod(name2, 0755) == 0);
+ assert_se(chmod(overridden, 0755) == 0);
+ assert_se(chmod(override, 0755) == 0);
+ assert_se(chmod(masked, 0755) == 0);
+ assert_se(chmod(masked2, 0755) == 0);
+ assert_se(chmod(masked2e, 0755) == 0);
+ assert_se(chmod(mask2e, 0755) == 0);
+
+ if (access(name, X_OK) < 0 && ERRNO_IS_PRIVILEGE(errno))
+ return;
+
+ if (gather_stdout)
+ execute_directories(dirs, DEFAULT_TIMEOUT_USEC, ignore_stdout, ignore_stdout_args, NULL, NULL, EXEC_DIR_PARALLEL | EXEC_DIR_IGNORE_ERRORS);
+ else
+ execute_directories(dirs, DEFAULT_TIMEOUT_USEC, NULL, NULL, NULL, NULL, EXEC_DIR_PARALLEL | EXEC_DIR_IGNORE_ERRORS);
+
+ assert_se(chdir(tmp_lo) == 0);
+ assert_se(access("it_works", F_OK) >= 0);
+ assert_se(access("failed", F_OK) < 0);
+
+ assert_se(chdir(tmp_hi) == 0);
+ assert_se(access("it_works2", F_OK) >= 0);
+ assert_se(access("failed", F_OK) < 0);
+}
+
+TEST(execute_directory) {
+ test_execute_directory_one(true);
+ test_execute_directory_one(false);
+}
+
+TEST(execution_order) {
+ _cleanup_(rm_rf_physical_and_freep) char *tmp_lo = NULL, *tmp_hi = NULL;
+ const char *name, *name2, *name3, *overridden, *override, *masked, *mask;
+ const char *output, *t;
+ _cleanup_free_ char *contents = NULL;
+
+ assert_se(mkdtemp_malloc("/tmp/test-exec-util-lo.XXXXXXX", &tmp_lo) >= 0);
+ assert_se(mkdtemp_malloc("/tmp/test-exec-util-hi.XXXXXXX", &tmp_hi) >= 0);
+
+ const char *dirs[] = { tmp_hi, tmp_lo, NULL };
+
+ output = strjoina(tmp_hi, "/output");
+
+ log_info("/* %s >>%s */", __func__, output);
+
+ /* write files in "random" order */
+ name2 = strjoina(tmp_lo, "/90-bar");
+ name = strjoina(tmp_hi, "/80-foo");
+ name3 = strjoina(tmp_lo, "/last");
+ overridden = strjoina(tmp_lo, "/30-override");
+ override = strjoina(tmp_hi, "/30-override");
+ masked = strjoina(tmp_lo, "/10-masked");
+ mask = strjoina(tmp_hi, "/10-masked");
+
+ t = strjoina("#!/bin/sh\necho $(basename $0) >>", output);
+ assert_se(write_string_file(name, t, WRITE_STRING_FILE_CREATE) == 0);
+
+ t = strjoina("#!/bin/sh\necho $(basename $0) >>", output);
+ assert_se(write_string_file(name2, t, WRITE_STRING_FILE_CREATE) == 0);
+
+ t = strjoina("#!/bin/sh\necho $(basename $0) >>", output);
+ assert_se(write_string_file(name3, t, WRITE_STRING_FILE_CREATE) == 0);
+
+ t = strjoina("#!/bin/sh\necho OVERRIDDEN >>", output);
+ assert_se(write_string_file(overridden, t, WRITE_STRING_FILE_CREATE) == 0);
+
+ t = strjoina("#!/bin/sh\necho $(basename $0) >>", output);
+ assert_se(write_string_file(override, t, WRITE_STRING_FILE_CREATE) == 0);
+
+ t = strjoina("#!/bin/sh\necho MASKED >>", output);
+ assert_se(write_string_file(masked, t, WRITE_STRING_FILE_CREATE) == 0);
+
+ assert_se(symlink("/dev/null", mask) == 0);
+
+ assert_se(chmod(name, 0755) == 0);
+ assert_se(chmod(name2, 0755) == 0);
+ assert_se(chmod(name3, 0755) == 0);
+ assert_se(chmod(overridden, 0755) == 0);
+ assert_se(chmod(override, 0755) == 0);
+ assert_se(chmod(masked, 0755) == 0);
+
+ if (access(name, X_OK) < 0 && ERRNO_IS_PRIVILEGE(errno))
+ return;
+
+ execute_directories(dirs, DEFAULT_TIMEOUT_USEC, ignore_stdout, ignore_stdout_args, NULL, NULL, EXEC_DIR_PARALLEL | EXEC_DIR_IGNORE_ERRORS);
+
+ assert_se(read_full_file(output, &contents, NULL) >= 0);
+ assert_se(streq(contents, "30-override\n80-foo\n90-bar\nlast\n"));
+}
+
+static int gather_stdout_one(int fd, void *arg) {
+ char ***s = arg, *t;
+ char buf[128] = {};
+
+ assert_se(s);
+ assert_se(read(fd, buf, sizeof buf) >= 0);
+ safe_close(fd);
+
+ assert_se(t = strndup(buf, sizeof buf));
+ assert_se(strv_push(s, t) >= 0);
+
+ return 0;
+}
+static int gather_stdout_two(int fd, void *arg) {
+ char ***s = arg;
+
+ STRV_FOREACH(t, *s)
+ assert_se(write(fd, *t, strlen(*t)) == (ssize_t) strlen(*t));
+ safe_close(fd);
+
+ return 0;
+}
+static int gather_stdout_three(int fd, void *arg) {
+ char **s = arg;
+ char buf[128] = {};
+
+ assert_se(read(fd, buf, sizeof buf - 1) > 0);
+ safe_close(fd);
+ assert_se(*s = strndup(buf, sizeof buf));
+
+ return 0;
+}
+
+const gather_stdout_callback_t gather_stdouts[] = {
+ gather_stdout_one,
+ gather_stdout_two,
+ gather_stdout_three,
+};
+
+TEST(stdout_gathering) {
+ _cleanup_(rm_rf_physical_and_freep) char *tmpdir = NULL;
+ const char *name, *name2, *name3;
+ int r;
+
+ char **tmp = NULL; /* this is only used in the forked process, no cleanup here */
+ _cleanup_free_ char *output = NULL;
+
+ void* args[] = {&tmp, &tmp, &output};
+
+ assert_se(mkdtemp_malloc("/tmp/test-exec-util.XXXXXXX", &tmpdir) >= 0);
+
+ const char *dirs[] = { tmpdir, NULL };
+
+ /* write files */
+ name = strjoina(tmpdir, "/10-foo");
+ name2 = strjoina(tmpdir, "/20-bar");
+ name3 = strjoina(tmpdir, "/30-last");
+
+ assert_se(write_string_file(name,
+ "#!/bin/sh\necho a\necho b\necho c\n",
+ WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(write_string_file(name2,
+ "#!/bin/sh\necho d\n",
+ WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(write_string_file(name3,
+ "#!/bin/sh\nsleep 1",
+ WRITE_STRING_FILE_CREATE) == 0);
+
+ assert_se(chmod(name, 0755) == 0);
+ assert_se(chmod(name2, 0755) == 0);
+ assert_se(chmod(name3, 0755) == 0);
+
+ if (access(name, X_OK) < 0 && ERRNO_IS_PRIVILEGE(errno))
+ return;
+
+ r = execute_directories(dirs, DEFAULT_TIMEOUT_USEC, gather_stdouts, args, NULL, NULL,
+ EXEC_DIR_PARALLEL | EXEC_DIR_IGNORE_ERRORS);
+ assert_se(r >= 0);
+
+ log_info("got: %s", output);
+
+ assert_se(streq(output, "a\nb\nc\nd\n"));
+}
+
+TEST(environment_gathering) {
+ _cleanup_(rm_rf_physical_and_freep) char *tmpdir = NULL;
+ const char *name, *name2, *name3, *old;
+ int r;
+
+ char **tmp = NULL; /* this is only used in the forked process, no cleanup here */
+ _cleanup_strv_free_ char **env = NULL;
+
+ void* const args[] = { &tmp, &tmp, &env };
+
+ assert_se(mkdtemp_malloc("/tmp/test-exec-util.XXXXXXX", &tmpdir) >= 0);
+
+ const char *dirs[] = { tmpdir, NULL };
+
+ /* write files */
+ name = strjoina(tmpdir, "/10-foo");
+ name2 = strjoina(tmpdir, "/20-bar");
+ name3 = strjoina(tmpdir, "/30-last");
+
+ assert_se(write_string_file(name,
+ "#!/bin/sh\n"
+ "echo A=23\n",
+ WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(write_string_file(name2,
+ "#!/bin/sh\n"
+ "echo A=22:$A\n\n\n", /* substitution from previous generator */
+ WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(write_string_file(name3,
+ "#!/bin/sh\n"
+ "echo A=$A:24\n"
+ "echo B=12\n"
+ "echo C=000\n"
+ "echo C=001\n" /* variable overwriting */
+ /* various invalid entries */
+ "echo unset A\n"
+ "echo unset A=\n"
+ "echo unset A=B\n"
+ "echo unset \n"
+ "echo A B=C\n"
+ "echo A\n"
+ /* test variable assignment without newline */
+ "echo PATH=$PATH:/no/such/file", /* no newline */
+ WRITE_STRING_FILE_CREATE) == 0);
+
+ assert_se(chmod(name, 0755) == 0);
+ assert_se(chmod(name2, 0755) == 0);
+ assert_se(chmod(name3, 0755) == 0);
+
+ /* When booting in containers or without initrd there might not be any PATH in the environment and if
+ * there is no PATH /bin/sh built-in PATH may leak and override systemd's DEFAULT_PATH which is not
+ * good. Force our own PATH in environment, to prevent expansion of sh built-in $PATH */
+ old = getenv("PATH");
+ r = setenv("PATH", "no-sh-built-in-path", 1);
+ assert_se(r >= 0);
+
+ if (access(name, X_OK) < 0 && ERRNO_IS_PRIVILEGE(errno))
+ return;
+
+ r = execute_directories(dirs, DEFAULT_TIMEOUT_USEC, gather_environment, args, NULL, NULL, EXEC_DIR_PARALLEL | EXEC_DIR_IGNORE_ERRORS);
+ assert_se(r >= 0);
+
+ STRV_FOREACH(p, env)
+ log_info("got env: \"%s\"", *p);
+
+ assert_se(streq(strv_env_get(env, "A"), "22:23:24"));
+ assert_se(streq(strv_env_get(env, "B"), "12"));
+ assert_se(streq(strv_env_get(env, "C"), "001"));
+ assert_se(streq(strv_env_get(env, "PATH"), "no-sh-built-in-path:/no/such/file"));
+
+ /* now retest with "default" path passed in, as created by
+ * manager_default_environment */
+ env = strv_free(env);
+ env = strv_new("PATH=" DEFAULT_PATH);
+ assert_se(env);
+
+ r = execute_directories(dirs, DEFAULT_TIMEOUT_USEC, gather_environment, args, NULL, env, EXEC_DIR_PARALLEL | EXEC_DIR_IGNORE_ERRORS);
+ assert_se(r >= 0);
+
+ STRV_FOREACH(p, env)
+ log_info("got env: \"%s\"", *p);
+
+ assert_se(streq(strv_env_get(env, "A"), "22:23:24"));
+ assert_se(streq(strv_env_get(env, "B"), "12"));
+ assert_se(streq(strv_env_get(env, "C"), "001"));
+ assert_se(streq(strv_env_get(env, "PATH"), DEFAULT_PATH ":/no/such/file"));
+
+ /* reset environ PATH */
+ assert_se(set_unset_env("PATH", old, true) == 0);
+}
+
+TEST(error_catching) {
+ _cleanup_(rm_rf_physical_and_freep) char *tmpdir = NULL;
+ const char *name, *name2, *name3;
+ int r;
+
+ assert_se(mkdtemp_malloc("/tmp/test-exec-util.XXXXXXX", &tmpdir) >= 0);
+
+ const char *dirs[] = { tmpdir, NULL };
+
+ /* write files */
+ name = strjoina(tmpdir, "/10-foo");
+ name2 = strjoina(tmpdir, "/20-bar");
+ name3 = strjoina(tmpdir, "/30-last");
+
+ assert_se(write_string_file(name,
+ "#!/bin/sh\necho a\necho b\necho c\n",
+ WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(write_string_file(name2,
+ "#!/bin/sh\nexit 42\n",
+ WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(write_string_file(name3,
+ "#!/bin/sh\nexit 12",
+ WRITE_STRING_FILE_CREATE) == 0);
+
+ assert_se(chmod(name, 0755) == 0);
+ assert_se(chmod(name2, 0755) == 0);
+ assert_se(chmod(name3, 0755) == 0);
+
+ if (access(name, X_OK) < 0 && ERRNO_IS_PRIVILEGE(errno))
+ return;
+
+ r = execute_directories(dirs, DEFAULT_TIMEOUT_USEC, NULL, NULL, NULL, NULL, EXEC_DIR_NONE);
+
+ /* we should exit with the error code of the first script that failed */
+ assert_se(r == 42);
+}
+
+TEST(exec_command_flags_from_strv) {
+ ExecCommandFlags flags = 0;
+ char **valid_strv = STRV_MAKE("no-env-expand", "no-setuid", "ignore-failure");
+ char **invalid_strv = STRV_MAKE("no-env-expand", "no-setuid", "nonexistent-option", "ignore-failure");
+ int r;
+
+ r = exec_command_flags_from_strv(valid_strv, &flags);
+
+ assert_se(r == 0);
+ assert_se(FLAGS_SET(flags, EXEC_COMMAND_NO_ENV_EXPAND));
+ assert_se(FLAGS_SET(flags, EXEC_COMMAND_NO_SETUID));
+ assert_se(FLAGS_SET(flags, EXEC_COMMAND_IGNORE_FAILURE));
+ assert_se(!FLAGS_SET(flags, EXEC_COMMAND_AMBIENT_MAGIC));
+ assert_se(!FLAGS_SET(flags, EXEC_COMMAND_FULLY_PRIVILEGED));
+
+ r = exec_command_flags_from_strv(invalid_strv, &flags);
+
+ assert_se(r == -EINVAL);
+}
+
+TEST(exec_command_flags_to_strv) {
+ _cleanup_strv_free_ char **opts = NULL, **empty_opts = NULL, **invalid_opts = NULL;
+ ExecCommandFlags flags = 0;
+ int r;
+
+ flags |= (EXEC_COMMAND_AMBIENT_MAGIC|EXEC_COMMAND_NO_ENV_EXPAND|EXEC_COMMAND_IGNORE_FAILURE);
+
+ r = exec_command_flags_to_strv(flags, &opts);
+
+ assert_se(r == 0);
+ assert_se(strv_equal(opts, STRV_MAKE("ignore-failure", "ambient", "no-env-expand")));
+
+ r = exec_command_flags_to_strv(0, &empty_opts);
+
+ assert_se(r == 0);
+ assert_se(strv_equal(empty_opts, STRV_MAKE_EMPTY));
+
+ flags = _EXEC_COMMAND_FLAGS_INVALID;
+
+ r = exec_command_flags_to_strv(flags, &invalid_opts);
+
+ assert_se(r == -EINVAL);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-execute.c b/src/test/test-execute.c
new file mode 100644
index 0000000..4f6ad5d
--- /dev/null
+++ b/src/test/test-execute.c
@@ -0,0 +1,1550 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+#include <sys/mount.h>
+#include <sys/prctl.h>
+#include <sys/types.h>
+
+#include "sd-event.h"
+
+#include "capability-util.h"
+#include "cpu-set-util.h"
+#include "copy.h"
+#include "dropin.h"
+#include "errno-list.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "macro.h"
+#include "manager.h"
+#include "missing_prctl.h"
+#include "mkdir.h"
+#include "mount-util.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "rm-rf.h"
+#include "seccomp-util.h"
+#include "service.h"
+#include "signal-util.h"
+#include "static-destruct.h"
+#include "stat-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+#include "unit.h"
+#include "user-util.h"
+#include "virt.h"
+
+#define PRIVATE_UNIT_DIR "/run/test-execute-unit-dir"
+
+static char *user_runtime_unit_dir = NULL;
+static bool can_unshare;
+static bool have_net_dummy;
+static bool have_netns;
+static unsigned n_ran_tests = 0;
+
+STATIC_DESTRUCTOR_REGISTER(user_runtime_unit_dir, freep);
+
+typedef void (*test_function_t)(Manager *m);
+
+static int cld_dumped_to_killed(int code) {
+ /* Depending on the system, seccomp version, … some signals might result in dumping, others in plain
+ * killing. Let's ignore the difference here, and map both cases to CLD_KILLED */
+ return code == CLD_DUMPED ? CLD_KILLED : code;
+}
+
+static void wait_for_service_finish(Manager *m, Unit *unit) {
+ Service *service = NULL;
+ usec_t ts;
+ usec_t timeout = 2 * USEC_PER_MINUTE;
+
+ assert_se(m);
+ assert_se(unit);
+
+ service = SERVICE(unit);
+ printf("%s\n", unit->id);
+ exec_context_dump(&service->exec_context, stdout, "\t");
+ ts = now(CLOCK_MONOTONIC);
+ while (!IN_SET(service->state, SERVICE_DEAD, SERVICE_FAILED)) {
+ int r;
+ usec_t n;
+
+ r = sd_event_run(m->event, 100 * USEC_PER_MSEC);
+ assert_se(r >= 0);
+
+ n = now(CLOCK_MONOTONIC);
+ if (ts + timeout < n) {
+ log_error("Test timeout when testing %s", unit->id);
+ r = unit_kill(unit, KILL_ALL, SIGKILL, SI_USER, 0, NULL);
+ if (r < 0)
+ log_error_errno(r, "Failed to kill %s: %m", unit->id);
+ exit(EXIT_FAILURE);
+ }
+ }
+}
+
+static void check_main_result(const char *file, unsigned line, const char *func,
+ Manager *m, Unit *unit, int status_expected, int code_expected) {
+ Service *service = NULL;
+
+ assert_se(m);
+ assert_se(unit);
+
+ wait_for_service_finish(m, unit);
+
+ service = SERVICE(unit);
+ exec_status_dump(&service->main_exec_status, stdout, "\t");
+
+ if (cld_dumped_to_killed(service->main_exec_status.code) != cld_dumped_to_killed(code_expected)) {
+ log_error("%s:%u:%s %s: can_unshare=%s: exit code %d, expected %d",
+ file, line, func, unit->id, yes_no(can_unshare),
+ service->main_exec_status.code, code_expected);
+ abort();
+ }
+
+ if (service->main_exec_status.status != status_expected) {
+ log_error("%s:%u:%s: %s: can_unshare=%s: exit status %d, expected %d",
+ file, line, func, unit->id, yes_no(can_unshare),
+ service->main_exec_status.status, status_expected);
+ abort();
+ }
+}
+
+static void check_service_result(const char *file, unsigned line, const char *func,
+ Manager *m, Unit *unit, ServiceResult result_expected) {
+ Service *service = NULL;
+
+ assert_se(m);
+ assert_se(unit);
+
+ wait_for_service_finish(m, unit);
+
+ service = SERVICE(unit);
+
+ if (service->result != result_expected) {
+ log_error("%s:%u:%s: %s: can_unshare=%s: service end result %s, expected %s",
+ file, line, func, unit->id, yes_no(can_unshare),
+ service_result_to_string(service->result),
+ service_result_to_string(result_expected));
+ abort();
+ }
+}
+
+static bool check_nobody_user_and_group(void) {
+ static int cache = -1;
+ struct passwd *p;
+ struct group *g;
+
+ if (cache >= 0)
+ return !!cache;
+
+ if (!synthesize_nobody())
+ goto invalid;
+
+ p = getpwnam(NOBODY_USER_NAME);
+ if (!p ||
+ !streq(p->pw_name, NOBODY_USER_NAME) ||
+ p->pw_uid != UID_NOBODY ||
+ p->pw_gid != GID_NOBODY)
+ goto invalid;
+
+ p = getpwuid(UID_NOBODY);
+ if (!p ||
+ !streq(p->pw_name, NOBODY_USER_NAME) ||
+ p->pw_uid != UID_NOBODY ||
+ p->pw_gid != GID_NOBODY)
+ goto invalid;
+
+ g = getgrnam(NOBODY_GROUP_NAME);
+ if (!g ||
+ !streq(g->gr_name, NOBODY_GROUP_NAME) ||
+ g->gr_gid != GID_NOBODY)
+ goto invalid;
+
+ g = getgrgid(GID_NOBODY);
+ if (!g ||
+ !streq(g->gr_name, NOBODY_GROUP_NAME) ||
+ g->gr_gid != GID_NOBODY)
+ goto invalid;
+
+ cache = 1;
+ return true;
+
+invalid:
+ cache = 0;
+ return false;
+}
+
+static bool check_user_has_group_with_same_name(const char *name) {
+ struct passwd *p;
+ struct group *g;
+
+ assert_se(name);
+
+ p = getpwnam(name);
+ if (!p ||
+ !streq(p->pw_name, name))
+ return false;
+
+ g = getgrgid(p->pw_gid);
+ if (!g ||
+ !streq(g->gr_name, name))
+ return false;
+
+ return true;
+}
+
+static bool is_inaccessible_available(void) {
+ FOREACH_STRING(p,
+ "/run/systemd/inaccessible/reg",
+ "/run/systemd/inaccessible/dir",
+ "/run/systemd/inaccessible/chr",
+ "/run/systemd/inaccessible/blk",
+ "/run/systemd/inaccessible/fifo",
+ "/run/systemd/inaccessible/sock")
+ if (access(p, F_OK) < 0)
+ return false;
+
+ return true;
+}
+
+static void start_parent_slices(Unit *unit) {
+ Unit *slice;
+
+ slice = UNIT_GET_SLICE(unit);
+ if (slice) {
+ start_parent_slices(slice);
+ int r = unit_start(slice, NULL);
+ assert_se(r >= 0 || r == -EALREADY);
+ }
+}
+
+static bool have_userns_privileges(void) {
+ pid_t pid;
+ int r;
+
+ r = safe_fork("(sd-test-check-userns)",
+ FORK_RESET_SIGNALS |
+ FORK_CLOSE_ALL_FDS |
+ FORK_DEATHSIG_SIGKILL,
+ &pid);
+ assert(r >= 0);
+ if (r == 0) {
+ /* Keep CAP_SYS_ADMIN if we have it to ensure we give an
+ * accurate result to the caller. Some kernels have a
+ * kernel.unprivileged_userns_clone sysctl which can be
+ * configured to make CLONE_NEWUSER require CAP_SYS_ADMIN.
+ * Additionally, AppArmor may restrict unprivileged user
+ * namespace creation. */
+ r = capability_bounding_set_drop(UINT64_C(1) << CAP_SYS_ADMIN, /* right_now = */ true);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to drop capabilities: %m");
+ _exit(2);
+ }
+
+ r = RET_NERRNO(unshare(CLONE_NEWUSER));
+ if (r < 0 && !ERRNO_IS_NEG_PRIVILEGE(r))
+ log_debug_errno(r, "Failed to create user namespace: %m");
+
+ _exit(r >= 0 ? EXIT_SUCCESS : ERRNO_IS_NEG_PRIVILEGE(r) ? EXIT_FAILURE : 2);
+ }
+
+ /* The exit code records the result of the check:
+ * EXIT_SUCCESS => we can use user namespaces
+ * EXIT_FAILURE => we can NOT use user namespaces
+ * 2 => some other error occurred */
+ r = wait_for_terminate_and_check("(sd-test-check-userns)", pid, 0);
+ if (!IN_SET(r, EXIT_SUCCESS, EXIT_FAILURE))
+ log_debug("Failed to check if user namespaces can be used, assuming not.");
+
+ return r == EXIT_SUCCESS;
+}
+
+static void _test(const char *file, unsigned line, const char *func,
+ Manager *m, const char *unit_name, int status_expected, int code_expected) {
+ Unit *unit;
+
+ assert_se(unit_name);
+
+ assert_se(manager_load_startable_unit_or_warn(m, unit_name, NULL, &unit) >= 0);
+ /* We need to start the slices as well otherwise the slice cgroups might be pruned
+ * in on_cgroup_empty_event. */
+ start_parent_slices(unit);
+ assert_se(unit_start(unit, NULL) >= 0);
+ check_main_result(file, line, func, m, unit, status_expected, code_expected);
+
+ ++n_ran_tests;
+}
+#define test(m, unit_name, status_expected, code_expected) \
+ _test(PROJECT_FILE, __LINE__, __func__, m, unit_name, status_expected, code_expected)
+
+static void _test_service(const char *file, unsigned line, const char *func,
+ Manager *m, const char *unit_name, ServiceResult result_expected) {
+ Unit *unit;
+
+ assert_se(unit_name);
+
+ assert_se(manager_load_startable_unit_or_warn(m, unit_name, NULL, &unit) >= 0);
+ assert_se(unit_start(unit, NULL) >= 0);
+ check_service_result(file, line, func, m, unit, result_expected);
+}
+#define test_service(m, unit_name, result_expected) \
+ _test_service(PROJECT_FILE, __LINE__, __func__, m, unit_name, result_expected)
+
+static void test_exec_bindpaths(Manager *m) {
+ assert_se(mkdir_p("/tmp/test-exec-bindpaths", 0755) >= 0);
+ assert_se(mkdir_p("/tmp/test-exec-bindreadonlypaths", 0755) >= 0);
+
+ test(m, "exec-bindpaths.service", can_unshare ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+
+ (void) rm_rf("/tmp/test-exec-bindpaths", REMOVE_ROOT|REMOVE_PHYSICAL);
+ (void) rm_rf("/tmp/test-exec-bindreadonlypaths", REMOVE_ROOT|REMOVE_PHYSICAL);
+}
+
+static void test_exec_cpuaffinity(Manager *m) {
+ _cleanup_(cpu_set_reset) CPUSet c = {};
+
+ assert_se(cpu_set_realloc(&c, 8192) >= 0); /* just allocate the maximum possible size */
+ assert_se(sched_getaffinity(0, c.allocated, c.set) >= 0);
+
+ if (!CPU_ISSET_S(0, c.allocated, c.set)) {
+ log_notice("Cannot use CPU 0, skipping %s", __func__);
+ return;
+ }
+
+ test(m, "exec-cpuaffinity1.service", 0, CLD_EXITED);
+ test(m, "exec-cpuaffinity2.service", 0, CLD_EXITED);
+
+ if (!CPU_ISSET_S(1, c.allocated, c.set) ||
+ !CPU_ISSET_S(2, c.allocated, c.set)) {
+ log_notice("Cannot use CPU 1 or 2, skipping remaining tests in %s", __func__);
+ return;
+ }
+
+ test(m, "exec-cpuaffinity3.service", 0, CLD_EXITED);
+}
+
+static void test_exec_credentials(Manager *m) {
+ test(m, "exec-set-credential.service", 0, CLD_EXITED);
+ test(m, "exec-load-credential.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_CREDENTIALS, CLD_EXITED);
+ test(m, "exec-credentials-dir-specifier.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_CREDENTIALS, CLD_EXITED);
+}
+
+static void test_exec_workingdirectory(Manager *m) {
+ assert_se(mkdir_p("/tmp/test-exec_workingdirectory", 0755) >= 0);
+
+ test(m, "exec-workingdirectory.service", 0, CLD_EXITED);
+ test(m, "exec-workingdirectory-trailing-dot.service", 0, CLD_EXITED);
+
+ (void) rm_rf("/tmp/test-exec_workingdirectory", REMOVE_ROOT|REMOVE_PHYSICAL);
+}
+
+static void test_exec_execsearchpath(Manager *m) {
+ assert_se(mkdir_p("/tmp/test-exec_execsearchpath", 0755) >= 0);
+
+ assert_se(copy_file("/bin/ls", "/tmp/test-exec_execsearchpath/ls_temp", 0, 0777, COPY_REPLACE) >= 0);
+
+ test(m, "exec-execsearchpath.service", 0, CLD_EXITED);
+
+ assert_se(rm_rf("/tmp/test-exec_execsearchpath", REMOVE_ROOT|REMOVE_PHYSICAL) >= 0);
+
+ test(m, "exec-execsearchpath.service", EXIT_EXEC, CLD_EXITED);
+}
+
+static void test_exec_execsearchpath_specifier(Manager *m) {
+ test(m, "exec-execsearchpath-unit-specifier.service", 0, CLD_EXITED);
+}
+
+static void test_exec_execsearchpath_environment(Manager *m) {
+ test(m, "exec-execsearchpath-environment.service", 0, CLD_EXITED);
+ test(m, "exec-execsearchpath-environment-path-set.service", 0, CLD_EXITED);
+}
+
+static void test_exec_execsearchpath_environment_files(Manager *m) {
+ static const char path_not_set[] =
+ "VAR1='word1 word2'\n"
+ "VAR2=word3 \n"
+ "# comment1\n"
+ "\n"
+ "; comment2\n"
+ " ; # comment3\n"
+ "line without an equal\n"
+ "VAR3='$word 5 6'\n"
+ "VAR4='new\nline'\n"
+ "VAR5=password\\with\\backslashes";
+
+ static const char path_set[] =
+ "VAR1='word1 word2'\n"
+ "VAR2=word3 \n"
+ "# comment1\n"
+ "\n"
+ "; comment2\n"
+ " ; # comment3\n"
+ "line without an equal\n"
+ "VAR3='$word 5 6'\n"
+ "VAR4='new\nline'\n"
+ "VAR5=password\\with\\backslashes\n"
+ "PATH=/usr";
+
+ int r;
+
+ r = write_string_file("/tmp/test-exec_execsearchpath_environmentfile.conf", path_not_set, WRITE_STRING_FILE_CREATE);
+
+ assert_se(r == 0);
+
+ test(m, "exec-execsearchpath-environmentfile.service", 0, CLD_EXITED);
+
+ (void) unlink("/tmp/test-exec_environmentfile.conf");
+
+
+ r = write_string_file("/tmp/test-exec_execsearchpath_environmentfile-set.conf", path_set, WRITE_STRING_FILE_CREATE);
+
+ assert_se(r == 0);
+
+ test(m, "exec-execsearchpath-environmentfile-set.service", 0, CLD_EXITED);
+
+ (void) unlink("/tmp/test-exec_environmentfile-set.conf");
+}
+
+static void test_exec_execsearchpath_passenvironment(Manager *m) {
+ assert_se(setenv("VAR1", "word1 word2", 1) == 0);
+ assert_se(setenv("VAR2", "word3", 1) == 0);
+ assert_se(setenv("VAR3", "$word 5 6", 1) == 0);
+ assert_se(setenv("VAR4", "new\nline", 1) == 0);
+ assert_se(setenv("VAR5", "passwordwithbackslashes", 1) == 0);
+
+ test(m, "exec-execsearchpath-passenvironment.service", 0, CLD_EXITED);
+
+ assert_se(setenv("PATH", "/usr", 1) == 0);
+ test(m, "exec-execsearchpath-passenvironment-set.service", 0, CLD_EXITED);
+
+ assert_se(unsetenv("VAR1") == 0);
+ assert_se(unsetenv("VAR2") == 0);
+ assert_se(unsetenv("VAR3") == 0);
+ assert_se(unsetenv("VAR4") == 0);
+ assert_se(unsetenv("VAR5") == 0);
+ assert_se(unsetenv("PATH") == 0);
+}
+
+static void test_exec_personality(Manager *m) {
+#if defined(__x86_64__)
+ test(m, "exec-personality-x86-64.service", 0, CLD_EXITED);
+
+#elif defined(__s390__)
+ test(m, "exec-personality-s390.service", 0, CLD_EXITED);
+
+#elif defined(__powerpc64__)
+# if __BYTE_ORDER == __BIG_ENDIAN
+ test(m, "exec-personality-ppc64.service", 0, CLD_EXITED);
+# else
+ test(m, "exec-personality-ppc64le.service", 0, CLD_EXITED);
+# endif
+
+#elif defined(__aarch64__)
+ test(m, "exec-personality-aarch64.service", 0, CLD_EXITED);
+
+#elif defined(__i386__)
+ test(m, "exec-personality-x86.service", 0, CLD_EXITED);
+#elif defined(__loongarch_lp64)
+ test(m, "exec-personality-loongarch64.service", 0, CLD_EXITED);
+#else
+ log_notice("Unknown personality, skipping %s", __func__);
+#endif
+}
+
+static void test_exec_ignoresigpipe(Manager *m) {
+ test(m, "exec-ignoresigpipe-yes.service", 0, CLD_EXITED);
+ test(m, "exec-ignoresigpipe-no.service", SIGPIPE, CLD_KILLED);
+}
+
+static void test_exec_privatetmp(Manager *m) {
+ assert_se(touch("/tmp/test-exec_privatetmp") >= 0);
+
+ if (MANAGER_IS_SYSTEM(m) || have_userns_privileges()) {
+ test(m, "exec-privatetmp-yes.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ test(m, "exec-privatetmp-disabled-by-prefix.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ }
+
+ test(m, "exec-privatetmp-no.service", 0, CLD_EXITED);
+
+ (void) unlink("/tmp/test-exec_privatetmp");
+}
+
+static void test_exec_privatedevices(Manager *m) {
+ int r;
+
+ if (detect_container() > 0) {
+ log_notice("Testing in container, skipping %s", __func__);
+ return;
+ }
+ if (!is_inaccessible_available()) {
+ log_notice("Testing without inaccessible, skipping %s", __func__);
+ return;
+ }
+
+ if (MANAGER_IS_SYSTEM(m) || have_userns_privileges()) {
+ test(m, "exec-privatedevices-yes.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ if (access("/dev/kmsg", F_OK) >= 0)
+ test(m, "exec-privatedevices-bind.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ test(m, "exec-privatedevices-disabled-by-prefix.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ test(m, "exec-privatedevices-yes-with-group.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ }
+
+ test(m, "exec-privatedevices-no.service", 0, CLD_EXITED);
+
+ /* We use capsh to test if the capabilities are
+ * properly set, so be sure that it exists */
+ r = find_executable("capsh", NULL);
+ if (r < 0) {
+ log_notice_errno(r, "Could not find capsh binary, skipping remaining tests in %s: %m", __func__);
+ return;
+ }
+
+ if (MANAGER_IS_SYSTEM(m) || have_userns_privileges()) {
+ test(m, "exec-privatedevices-yes-capability-mknod.service", can_unshare || MANAGER_IS_SYSTEM(m) ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+ test(m, "exec-privatedevices-yes-capability-sys-rawio.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+ }
+
+ test(m, "exec-privatedevices-no-capability-mknod.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_FAILURE, CLD_EXITED);
+ test(m, "exec-privatedevices-no-capability-sys-rawio.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_FAILURE, CLD_EXITED);
+}
+
+static void test_exec_protecthome(Manager *m) {
+ if (!can_unshare) {
+ log_notice("Cannot reliably unshare, skipping %s", __func__);
+ return;
+ }
+
+ test(m, "exec-protecthome-tmpfs-vs-protectsystem-strict.service", 0, CLD_EXITED);
+}
+
+static void test_exec_protectkernelmodules(Manager *m) {
+ int r;
+
+ if (detect_container() > 0) {
+ log_notice("Testing in container, skipping %s", __func__);
+ return;
+ }
+ if (!is_inaccessible_available()) {
+ log_notice("Testing without inaccessible, skipping %s", __func__);
+ return;
+ }
+
+ r = find_executable("capsh", NULL);
+ if (r < 0) {
+ log_notice_errno(r, "Skipping %s, could not find capsh binary: %m", __func__);
+ return;
+ }
+
+ test(m, "exec-protectkernelmodules-no-capabilities.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_FAILURE, CLD_EXITED);
+
+ if (MANAGER_IS_SYSTEM(m) || have_userns_privileges()) {
+ test(m, "exec-protectkernelmodules-yes-capabilities.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+ test(m, "exec-protectkernelmodules-yes-mount-propagation.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ }
+}
+
+static void test_exec_readonlypaths(Manager *m) {
+
+ if (MANAGER_IS_SYSTEM(m) || have_userns_privileges())
+ test(m, "exec-readonlypaths-simple.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+
+ if (path_is_read_only_fs("/var") > 0) {
+ log_notice("Directory /var is readonly, skipping remaining tests in %s", __func__);
+ return;
+ }
+
+ test(m, "exec-readonlypaths.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ test(m, "exec-readonlypaths-with-bindpaths.service", can_unshare ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+ test(m, "exec-readonlypaths-mount-propagation.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+}
+
+static void test_exec_readwritepaths(Manager *m) {
+
+ if (path_is_read_only_fs("/") > 0) {
+ log_notice("Root directory is readonly, skipping %s", __func__);
+ return;
+ }
+
+ test(m, "exec-readwritepaths-mount-propagation.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+}
+
+static void test_exec_inaccessiblepaths(Manager *m) {
+
+ if (!is_inaccessible_available()) {
+ log_notice("Testing without inaccessible, skipping %s", __func__);
+ return;
+ }
+
+ if (MANAGER_IS_SYSTEM(m) || have_userns_privileges())
+ test(m, "exec-inaccessiblepaths-sys.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+
+ if (path_is_read_only_fs("/") > 0) {
+ log_notice("Root directory is readonly, skipping remaining tests in %s", __func__);
+ return;
+ }
+
+ test(m, "exec-inaccessiblepaths-mount-propagation.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+}
+
+static int on_spawn_io(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ char **result = userdata;
+ char buf[4096];
+ ssize_t l;
+
+ assert_se(s);
+ assert_se(fd >= 0);
+
+ l = read(fd, buf, sizeof(buf) - 1);
+ if (l < 0) {
+ if (errno == EAGAIN)
+ goto reenable;
+
+ return 0;
+ }
+ if (l == 0)
+ return 0;
+
+ buf[l] = '\0';
+ if (result)
+ assert_se(strextend(result, buf));
+ else
+ log_error("ldd: %s", buf);
+
+reenable:
+ /* Re-enable the event source if we did not encounter EOF */
+ assert_se(sd_event_source_set_enabled(s, SD_EVENT_ONESHOT) >= 0);
+ return 0;
+}
+
+static int on_spawn_timeout(sd_event_source *s, uint64_t usec, void *userdata) {
+ pid_t *pid = userdata;
+
+ assert_se(pid);
+
+ (void) kill(*pid, SIGKILL);
+
+ return 1;
+}
+
+static int on_spawn_sigchld(sd_event_source *s, const siginfo_t *si, void *userdata) {
+ int ret = -EIO;
+
+ assert_se(si);
+
+ if (si->si_code == CLD_EXITED)
+ ret = si->si_status;
+
+ sd_event_exit(sd_event_source_get_event(s), ret);
+ return 1;
+}
+
+static int find_libraries(const char *exec, char ***ret) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ _cleanup_(sd_event_source_unrefp) sd_event_source *sigchld_source = NULL;
+ _cleanup_(sd_event_source_unrefp) sd_event_source *stdout_source = NULL;
+ _cleanup_(sd_event_source_unrefp) sd_event_source *stderr_source = NULL;
+ _cleanup_close_pair_ int outpipe[2] = EBADF_PAIR, errpipe[2] = EBADF_PAIR;
+ _cleanup_strv_free_ char **libraries = NULL;
+ _cleanup_free_ char *result = NULL;
+ pid_t pid;
+ int r;
+
+ assert_se(exec);
+ assert_se(ret);
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, -1) >= 0);
+
+ assert_se(pipe2(outpipe, O_NONBLOCK|O_CLOEXEC) == 0);
+ assert_se(pipe2(errpipe, O_NONBLOCK|O_CLOEXEC) == 0);
+
+ r = safe_fork_full("(spawn-ldd)",
+ (int[]) { -EBADF, outpipe[1], errpipe[1] },
+ NULL, 0,
+ FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_REARRANGE_STDIO|FORK_LOG, &pid);
+ assert_se(r >= 0);
+ if (r == 0) {
+ execlp("ldd", "ldd", exec, NULL);
+ _exit(EXIT_FAILURE);
+ }
+
+ outpipe[1] = safe_close(outpipe[1]);
+ errpipe[1] = safe_close(errpipe[1]);
+
+ assert_se(sd_event_new(&e) >= 0);
+
+ assert_se(sd_event_add_time_relative(e, NULL, CLOCK_MONOTONIC,
+ 10 * USEC_PER_SEC, USEC_PER_SEC, on_spawn_timeout, &pid) >= 0);
+ assert_se(sd_event_add_io(e, &stdout_source, outpipe[0], EPOLLIN, on_spawn_io, &result) >= 0);
+ assert_se(sd_event_source_set_enabled(stdout_source, SD_EVENT_ONESHOT) >= 0);
+ assert_se(sd_event_add_io(e, &stderr_source, errpipe[0], EPOLLIN, on_spawn_io, NULL) >= 0);
+ assert_se(sd_event_source_set_enabled(stderr_source, SD_EVENT_ONESHOT) >= 0);
+ assert_se(sd_event_add_child(e, &sigchld_source, pid, WEXITED, on_spawn_sigchld, NULL) >= 0);
+ /* SIGCHLD should be processed after IO is complete */
+ assert_se(sd_event_source_set_priority(sigchld_source, SD_EVENT_PRIORITY_NORMAL + 1) >= 0);
+
+ assert_se(sd_event_loop(e) >= 0);
+
+ _cleanup_strv_free_ char **v = NULL;
+ assert_se(strv_split_newlines_full(&v, result, 0) >= 0);
+
+ STRV_FOREACH(q, v) {
+ _cleanup_free_ char *word = NULL;
+ const char *p = *q;
+
+ r = extract_first_word(&p, &word, NULL, 0);
+ assert_se(r >= 0);
+ if (r == 0)
+ continue;
+
+ if (path_is_absolute(word)) {
+ assert_se(strv_consume(&libraries, TAKE_PTR(word)) >= 0);
+ continue;
+ }
+
+ word = mfree(word);
+ r = extract_first_word(&p, &word, NULL, 0);
+ assert_se(r >= 0);
+ if (r == 0)
+ continue;
+
+ if (!streq_ptr(word, "=>"))
+ continue;
+
+ word = mfree(word);
+ r = extract_first_word(&p, &word, NULL, 0);
+ assert_se(r >= 0);
+ if (r == 0)
+ continue;
+
+ if (path_is_absolute(word)) {
+ assert_se(strv_consume(&libraries, TAKE_PTR(word)) >= 0);
+ continue;
+ }
+ }
+
+ *ret = TAKE_PTR(libraries);
+ return 0;
+}
+
+static void test_exec_mount_apivfs(Manager *m) {
+ _cleanup_free_ char *fullpath_touch = NULL, *fullpath_test = NULL, *data = NULL;
+ _cleanup_strv_free_ char **libraries = NULL, **libraries_test = NULL;
+ int r;
+
+ assert_se(user_runtime_unit_dir);
+
+ r = find_executable("ldd", NULL);
+ if (r < 0) {
+ log_notice_errno(r, "Skipping %s, could not find 'ldd' command: %m", __func__);
+ return;
+ }
+ r = find_executable("touch", &fullpath_touch);
+ if (r < 0) {
+ log_notice_errno(r, "Skipping %s, could not find 'touch' command: %m", __func__);
+ return;
+ }
+ r = find_executable("test", &fullpath_test);
+ if (r < 0) {
+ log_notice_errno(r, "Skipping %s, could not find 'test' command: %m", __func__);
+ return;
+ }
+
+ if (MANAGER_IS_USER(m) && !have_userns_privileges())
+ return (void)log_notice("Skipping %s, do not have user namespace privileges", __func__);
+
+ assert_se(find_libraries(fullpath_touch, &libraries) >= 0);
+ assert_se(find_libraries(fullpath_test, &libraries_test) >= 0);
+ assert_se(strv_extend_strv(&libraries, libraries_test, true) >= 0);
+
+ assert_se(strextend(&data, "[Service]\n"));
+ assert_se(strextend(&data, "ExecStart=", fullpath_touch, " /aaa\n"));
+ assert_se(strextend(&data, "ExecStart=", fullpath_test, " -f /aaa\n"));
+ assert_se(strextend(&data, "BindReadOnlyPaths=", fullpath_touch, "\n"));
+ assert_se(strextend(&data, "BindReadOnlyPaths=", fullpath_test, "\n"));
+
+ STRV_FOREACH(p, libraries)
+ assert_se(strextend(&data, "BindReadOnlyPaths=", *p, "\n"));
+
+ assert_se(write_drop_in(user_runtime_unit_dir, "exec-mount-apivfs-no.service", 10, "bind-mount", data) >= 0);
+
+ assert_se(mkdir_p("/tmp/test-exec-mount-apivfs-no/root", 0755) >= 0);
+
+ test(m, "exec-mount-apivfs-no.service", can_unshare || !MANAGER_IS_SYSTEM(m) ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+
+ (void) rm_rf("/tmp/test-exec-mount-apivfs-no/root", REMOVE_ROOT|REMOVE_PHYSICAL);
+}
+
+static void test_exec_noexecpaths(Manager *m) {
+
+ if (MANAGER_IS_SYSTEM(m) || have_userns_privileges())
+ test(m, "exec-noexecpaths-simple.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+ else
+ return (void)log_notice("Skipping %s, do not have user namespace privileges", __func__);
+}
+
+static void test_exec_temporaryfilesystem(Manager *m) {
+
+ test(m, "exec-temporaryfilesystem-options.service", can_unshare ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+ test(m, "exec-temporaryfilesystem-ro.service", can_unshare ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+ test(m, "exec-temporaryfilesystem-rw.service", can_unshare ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+ test(m, "exec-temporaryfilesystem-usr.service", can_unshare ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+}
+
+static void test_exec_systemcallfilter(Manager *m) {
+#if HAVE_SECCOMP
+ int r;
+
+ if (!is_seccomp_available()) {
+ log_notice("Seccomp not available, skipping %s", __func__);
+ return;
+ }
+
+ test(m, "exec-systemcallfilter-not-failing.service", 0, CLD_EXITED);
+ test(m, "exec-systemcallfilter-not-failing2.service", 0, CLD_EXITED);
+ test(m, "exec-systemcallfilter-not-failing3.service", 0, CLD_EXITED);
+ test(m, "exec-systemcallfilter-failing.service", SIGSYS, CLD_KILLED);
+ test(m, "exec-systemcallfilter-failing2.service", SIGSYS, CLD_KILLED);
+ test(m, "exec-systemcallfilter-failing3.service", SIGSYS, CLD_KILLED);
+
+ r = find_executable("python3", NULL);
+ if (r < 0) {
+ log_notice_errno(r, "Skipping remaining tests in %s, could not find python3 binary: %m", __func__);
+ return;
+ }
+
+ test(m, "exec-systemcallfilter-with-errno-name.service", errno_from_name("EILSEQ"), CLD_EXITED);
+ test(m, "exec-systemcallfilter-with-errno-number.service", 255, CLD_EXITED);
+ test(m, "exec-systemcallfilter-with-errno-multi.service", errno_from_name("EILSEQ"), CLD_EXITED);
+ test(m, "exec-systemcallfilter-with-errno-in-allow-list.service", errno_from_name("EILSEQ"), CLD_EXITED);
+ test(m, "exec-systemcallfilter-override-error-action.service", SIGSYS, CLD_KILLED);
+ test(m, "exec-systemcallfilter-override-error-action2.service", errno_from_name("EILSEQ"), CLD_EXITED);
+
+ test(m, "exec-systemcallfilter-nonewprivileges.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_GROUP, CLD_EXITED);
+ test(m, "exec-systemcallfilter-nonewprivileges-protectclock.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_GROUP, CLD_EXITED);
+
+ r = find_executable("capsh", NULL);
+ if (r < 0) {
+ log_notice_errno(r, "Skipping %s, could not find capsh binary: %m", __func__);
+ return;
+ }
+
+ test(m, "exec-systemcallfilter-nonewprivileges-bounding1.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_GROUP, CLD_EXITED);
+ test(m, "exec-systemcallfilter-nonewprivileges-bounding2.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_GROUP, CLD_EXITED);
+#endif
+}
+
+static void test_exec_systemcallerrornumber(Manager *m) {
+#if HAVE_SECCOMP
+ int r;
+
+ if (!is_seccomp_available()) {
+ log_notice("Seccomp not available, skipping %s", __func__);
+ return;
+ }
+
+ r = find_executable("python3", NULL);
+ if (r < 0) {
+ log_notice_errno(r, "Skipping %s, could not find python3 binary: %m", __func__);
+ return;
+ }
+
+ test(m, "exec-systemcallerrornumber-name.service", errno_from_name("EACCES"), CLD_EXITED);
+ test(m, "exec-systemcallerrornumber-number.service", 255, CLD_EXITED);
+#endif
+}
+
+static void test_exec_restrictnamespaces(Manager *m) {
+#if HAVE_SECCOMP
+ if (!is_seccomp_available()) {
+ log_notice("Seccomp not available, skipping %s", __func__);
+ return;
+ }
+
+ test(m, "exec-restrictnamespaces-no.service", can_unshare ? 0 : EXIT_FAILURE, CLD_EXITED);
+ test(m, "exec-restrictnamespaces-yes.service", 1, CLD_EXITED);
+ test(m, "exec-restrictnamespaces-mnt.service", can_unshare ? 0 : EXIT_FAILURE, CLD_EXITED);
+ test(m, "exec-restrictnamespaces-mnt-deny-list.service", 1, CLD_EXITED);
+ test(m, "exec-restrictnamespaces-merge-and.service", can_unshare ? 0 : EXIT_FAILURE, CLD_EXITED);
+ test(m, "exec-restrictnamespaces-merge-or.service", can_unshare ? 0 : EXIT_FAILURE, CLD_EXITED);
+ test(m, "exec-restrictnamespaces-merge-all.service", can_unshare ? 0 : EXIT_FAILURE, CLD_EXITED);
+#endif
+}
+
+static void test_exec_systemcallfilter_system(Manager *m) {
+/* Skip this particular test case when running under ASan, as
+ * LSan intermittently segfaults when accessing memory right
+ * after the test finishes. Generally, ASan & LSan don't like
+ * the seccomp stuff.
+ */
+#if HAVE_SECCOMP && !HAS_FEATURE_ADDRESS_SANITIZER
+ if (!is_seccomp_available()) {
+ log_notice("Seccomp not available, skipping %s", __func__);
+ return;
+ }
+
+ test(m, "exec-systemcallfilter-system-user.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_GROUP, CLD_EXITED);
+
+ if (!check_nobody_user_and_group()) {
+ log_notice("nobody user/group is not synthesized or may conflict to other entries, skipping remaining tests in %s", __func__);
+ return;
+ }
+
+ if (!STR_IN_SET(NOBODY_USER_NAME, "nobody", "nfsnobody")) {
+ log_notice("Unsupported nobody user name '%s', skipping remaining tests in %s", NOBODY_USER_NAME, __func__);
+ return;
+ }
+
+ test(m, "exec-systemcallfilter-system-user-" NOBODY_USER_NAME ".service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_GROUP, CLD_EXITED);
+#endif
+}
+
+static void test_exec_user(Manager *m) {
+ test(m, "exec-user.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_GROUP, CLD_EXITED);
+
+ if (!check_nobody_user_and_group()) {
+ log_notice("nobody user/group is not synthesized or may conflict to other entries, skipping remaining tests in %s", __func__);
+ return;
+ }
+
+ if (!STR_IN_SET(NOBODY_USER_NAME, "nobody", "nfsnobody")) {
+ log_notice("Unsupported nobody user name '%s', skipping remaining tests in %s", NOBODY_USER_NAME, __func__);
+ return;
+ }
+
+ test(m, "exec-user-" NOBODY_USER_NAME ".service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_GROUP, CLD_EXITED);
+}
+
+static void test_exec_group(Manager *m) {
+ test(m, "exec-group.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_GROUP, CLD_EXITED);
+
+ if (!check_nobody_user_and_group()) {
+ log_notice("nobody user/group is not synthesized or may conflict to other entries, skipping remaining tests in %s", __func__);
+ return;
+ }
+
+ if (!STR_IN_SET(NOBODY_GROUP_NAME, "nobody", "nfsnobody", "nogroup")) {
+ log_notice("Unsupported nobody group name '%s', skipping remaining tests in %s", NOBODY_GROUP_NAME, __func__);
+ return;
+ }
+
+ test(m, "exec-group-" NOBODY_GROUP_NAME ".service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_GROUP, CLD_EXITED);
+}
+
+static void test_exec_supplementarygroups(Manager *m) {
+ int status = MANAGER_IS_SYSTEM(m) ? 0 : EXIT_GROUP;
+ test(m, "exec-supplementarygroups.service", status, CLD_EXITED);
+ test(m, "exec-supplementarygroups-single-group.service", status, CLD_EXITED);
+ test(m, "exec-supplementarygroups-single-group-user.service", status, CLD_EXITED);
+ test(m, "exec-supplementarygroups-multiple-groups-default-group-user.service", status, CLD_EXITED);
+ test(m, "exec-supplementarygroups-multiple-groups-withgid.service", status, CLD_EXITED);
+ test(m, "exec-supplementarygroups-multiple-groups-withuid.service", status, CLD_EXITED);
+}
+
+static char* private_directory_bad(Manager *m) {
+ /* This mirrors setup_exec_directory(). */
+
+ for (ExecDirectoryType dt = 0; dt < _EXEC_DIRECTORY_TYPE_MAX; dt++) {
+ _cleanup_free_ char *p = NULL;
+ struct stat st;
+
+ assert_se(p = path_join(m->prefix[dt], "private"));
+
+ if (stat(p, &st) >= 0 &&
+ (st.st_mode & (S_IRWXG|S_IRWXO)))
+ return TAKE_PTR(p);
+ }
+
+ return NULL;
+}
+
+static void test_exec_dynamicuser(Manager *m) {
+ _cleanup_free_ char *bad = private_directory_bad(m);
+ if (bad) {
+ log_warning("%s: %s has bad permissions, skipping test.", __func__, bad);
+ return;
+ }
+
+ if (strstr_ptr(ci_environment(), "github-actions")) {
+ log_notice("%s: skipping test on GH Actions because of systemd/systemd#10337", __func__);
+ return;
+ }
+
+ int status = can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_NAMESPACE : EXIT_GROUP;
+
+ test(m, "exec-dynamicuser-fixeduser.service", status, CLD_EXITED);
+ if (check_user_has_group_with_same_name("adm"))
+ test(m, "exec-dynamicuser-fixeduser-adm.service", status, CLD_EXITED);
+ if (check_user_has_group_with_same_name("games"))
+ test(m, "exec-dynamicuser-fixeduser-games.service", status, CLD_EXITED);
+ test(m, "exec-dynamicuser-fixeduser-one-supplementarygroup.service", status, CLD_EXITED);
+ test(m, "exec-dynamicuser-supplementarygroups.service", status, CLD_EXITED);
+ test(m, "exec-dynamicuser-statedir.service", status, CLD_EXITED);
+
+ (void) rm_rf("/var/lib/quux", REMOVE_ROOT|REMOVE_PHYSICAL);
+ (void) rm_rf("/var/lib/test-dynamicuser-migrate", REMOVE_ROOT|REMOVE_PHYSICAL);
+ (void) rm_rf("/var/lib/test-dynamicuser-migrate2", REMOVE_ROOT|REMOVE_PHYSICAL);
+ (void) rm_rf("/var/lib/waldo", REMOVE_ROOT|REMOVE_PHYSICAL);
+ (void) rm_rf("/var/lib/private/quux", REMOVE_ROOT|REMOVE_PHYSICAL);
+ (void) rm_rf("/var/lib/private/test-dynamicuser-migrate", REMOVE_ROOT|REMOVE_PHYSICAL);
+ (void) rm_rf("/var/lib/private/test-dynamicuser-migrate2", REMOVE_ROOT|REMOVE_PHYSICAL);
+ (void) rm_rf("/var/lib/private/waldo", REMOVE_ROOT|REMOVE_PHYSICAL);
+
+ test(m, "exec-dynamicuser-statedir-migrate-step1.service", 0, CLD_EXITED);
+ test(m, "exec-dynamicuser-statedir-migrate-step2.service", status, CLD_EXITED);
+ test(m, "exec-dynamicuser-statedir-migrate-step1.service", 0, CLD_EXITED);
+
+ (void) rm_rf("/var/lib/test-dynamicuser-migrate", REMOVE_ROOT|REMOVE_PHYSICAL);
+ (void) rm_rf("/var/lib/test-dynamicuser-migrate2", REMOVE_ROOT|REMOVE_PHYSICAL);
+ (void) rm_rf("/var/lib/private/test-dynamicuser-migrate", REMOVE_ROOT|REMOVE_PHYSICAL);
+ (void) rm_rf("/var/lib/private/test-dynamicuser-migrate2", REMOVE_ROOT|REMOVE_PHYSICAL);
+
+ test(m, "exec-dynamicuser-runtimedirectory1.service", status, CLD_EXITED);
+ test(m, "exec-dynamicuser-runtimedirectory2.service", status, CLD_EXITED);
+ test(m, "exec-dynamicuser-runtimedirectory3.service", status, CLD_EXITED);
+}
+
+static void test_exec_environment(Manager *m) {
+ test(m, "exec-environment-no-substitute.service", 0, CLD_EXITED);
+ test(m, "exec-environment.service", 0, CLD_EXITED);
+ test(m, "exec-environment-multiple.service", 0, CLD_EXITED);
+ test(m, "exec-environment-empty.service", 0, CLD_EXITED);
+}
+
+static void test_exec_environmentfile(Manager *m) {
+ static const char e[] =
+ "VAR1='word1 word2'\n"
+ "VAR2=word3 \n"
+ "# comment1\n"
+ "\n"
+ "; comment2\n"
+ " ; # comment3\n"
+ "line without an equal\n"
+ "VAR3='$word 5 6'\n"
+ "VAR4='new\nline'\n"
+ "VAR5=password\\with\\backslashes";
+ int r;
+
+ r = write_string_file("/tmp/test-exec_environmentfile.conf", e, WRITE_STRING_FILE_CREATE);
+ assert_se(r == 0);
+
+ test(m, "exec-environmentfile.service", 0, CLD_EXITED);
+
+ (void) unlink("/tmp/test-exec_environmentfile.conf");
+}
+
+static void test_exec_passenvironment(Manager *m) {
+ /* test-execute runs under MANAGER_USER which, by default, forwards all
+ * variables present in the environment, but only those that are
+ * present _at the time it is created_!
+ *
+ * So these PassEnvironment checks are still expected to work, since we
+ * are ensuring the variables are not present at manager creation (they
+ * are unset explicitly in main) and are only set here.
+ *
+ * This is still a good approximation of how a test for MANAGER_SYSTEM
+ * would work.
+ */
+ assert_se(setenv("VAR1", "word1 word2", 1) == 0);
+ assert_se(setenv("VAR2", "word3", 1) == 0);
+ assert_se(setenv("VAR3", "$word 5 6", 1) == 0);
+ assert_se(setenv("VAR4", "new\nline", 1) == 0);
+ assert_se(setenv("VAR5", "passwordwithbackslashes", 1) == 0);
+ test(m, "exec-passenvironment.service", 0, CLD_EXITED);
+ test(m, "exec-passenvironment-repeated.service", 0, CLD_EXITED);
+ test(m, "exec-passenvironment-empty.service", 0, CLD_EXITED);
+ assert_se(unsetenv("VAR1") == 0);
+ assert_se(unsetenv("VAR2") == 0);
+ assert_se(unsetenv("VAR3") == 0);
+ assert_se(unsetenv("VAR4") == 0);
+ assert_se(unsetenv("VAR5") == 0);
+ test(m, "exec-passenvironment-absent.service", 0, CLD_EXITED);
+}
+
+static void test_exec_umask(Manager *m) {
+ if (MANAGER_IS_SYSTEM(m) || have_userns_privileges()) {
+ test(m, "exec-umask-default.service", can_unshare || MANAGER_IS_SYSTEM(m) ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+ test(m, "exec-umask-0177.service", can_unshare || MANAGER_IS_SYSTEM(m) ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+ } else
+ return (void)log_notice("Skipping %s, do not have user namespace privileges", __func__);
+}
+
+static void test_exec_runtimedirectory(Manager *m) {
+ (void) rm_rf("/run/test-exec_runtimedirectory2", REMOVE_ROOT|REMOVE_PHYSICAL);
+ test(m, "exec-runtimedirectory.service", 0, CLD_EXITED);
+ (void) rm_rf("/run/test-exec_runtimedirectory2", REMOVE_ROOT|REMOVE_PHYSICAL);
+
+ test(m, "exec-runtimedirectory-mode.service", 0, CLD_EXITED);
+ test(m, "exec-runtimedirectory-owner.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_GROUP, CLD_EXITED);
+
+ if (!check_nobody_user_and_group()) {
+ log_notice("nobody user/group is not synthesized or may conflict to other entries, skipping remaining tests in %s", __func__);
+ return;
+ }
+
+ if (!STR_IN_SET(NOBODY_GROUP_NAME, "nobody", "nfsnobody", "nogroup")) {
+ log_notice("Unsupported nobody group name '%s', skipping remaining tests in %s", NOBODY_GROUP_NAME, __func__);
+ return;
+ }
+
+ test(m, "exec-runtimedirectory-owner-" NOBODY_GROUP_NAME ".service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_GROUP, CLD_EXITED);
+}
+
+static void test_exec_capabilityboundingset(Manager *m) {
+ int r;
+
+ r = find_executable("capsh", NULL);
+ if (r < 0) {
+ log_notice_errno(r, "Skipping %s, could not find capsh binary: %m", __func__);
+ return;
+ }
+
+ if (have_effective_cap(CAP_CHOWN) <= 0 ||
+ have_effective_cap(CAP_FOWNER) <= 0 ||
+ have_effective_cap(CAP_KILL) <= 0) {
+ log_notice("Skipping %s, this process does not have enough capabilities", __func__);
+ return;
+ }
+
+ test(m, "exec-capabilityboundingset-simple.service", 0, CLD_EXITED);
+ test(m, "exec-capabilityboundingset-reset.service", 0, CLD_EXITED);
+ test(m, "exec-capabilityboundingset-merge.service", 0, CLD_EXITED);
+ test(m, "exec-capabilityboundingset-invert.service", 0, CLD_EXITED);
+}
+
+static void test_exec_basic(Manager *m) {
+ if (MANAGER_IS_SYSTEM(m) || have_userns_privileges())
+ test(m, "exec-basic.service", can_unshare || MANAGER_IS_SYSTEM(m) ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+ else
+ return (void)log_notice("Skipping %s, do not have user namespace privileges", __func__);
+}
+
+static void test_exec_ambientcapabilities(Manager *m) {
+ int r;
+
+ /* Check if the kernel has support for ambient capabilities. Run
+ * the tests only if that's the case. Clearing all ambient
+ * capabilities is fine, since we are expecting them to be unset
+ * in the first place for the tests. */
+ r = prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0);
+ if (r < 0 && IN_SET(errno, EINVAL, EOPNOTSUPP, ENOSYS)) {
+ log_notice("Skipping %s, the kernel does not support ambient capabilities", __func__);
+ return;
+ }
+
+ if (have_effective_cap(CAP_CHOWN) <= 0 ||
+ have_effective_cap(CAP_NET_RAW) <= 0) {
+ log_notice("Skipping %s, this process does not have enough capabilities", __func__);
+ return;
+ }
+
+ test(m, "exec-ambientcapabilities.service", 0, CLD_EXITED);
+ test(m, "exec-ambientcapabilities-merge.service", 0, CLD_EXITED);
+
+ if (have_effective_cap(CAP_SETUID) > 0)
+ test(m, "exec-ambientcapabilities-dynuser.service", can_unshare ? 0 : EXIT_NAMESPACE, CLD_EXITED);
+
+ if (!check_nobody_user_and_group()) {
+ log_notice("nobody user/group is not synthesized or may conflict to other entries, skipping remaining tests in %s", __func__);
+ return;
+ }
+
+ if (!STR_IN_SET(NOBODY_USER_NAME, "nobody", "nfsnobody")) {
+ log_notice("Unsupported nobody user name '%s', skipping remaining tests in %s", NOBODY_USER_NAME, __func__);
+ return;
+ }
+
+ test(m, "exec-ambientcapabilities-" NOBODY_USER_NAME ".service", 0, CLD_EXITED);
+ test(m, "exec-ambientcapabilities-merge-" NOBODY_USER_NAME ".service", 0, CLD_EXITED);
+}
+
+static void test_exec_privatenetwork(Manager *m) {
+ int r;
+
+ if (!have_net_dummy)
+ return (void)log_notice("Skipping %s, dummy network interface not available", __func__);
+
+ if (MANAGER_IS_USER(m) && !have_userns_privileges())
+ return (void)log_notice("Skipping %s, do not have user namespace privileges", __func__);
+
+ r = find_executable("ip", NULL);
+ if (r < 0) {
+ log_notice_errno(r, "Skipping %s, could not find ip binary: %m", __func__);
+ return;
+ }
+
+ test(m, "exec-privatenetwork-yes-privatemounts-no.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_NETWORK : EXIT_FAILURE, CLD_EXITED);
+ test(m, "exec-privatenetwork-yes-privatemounts-yes.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_NETWORK : EXIT_NAMESPACE, CLD_EXITED);
+}
+
+static void test_exec_networknamespacepath(Manager *m) {
+ int r;
+
+ if (!have_net_dummy)
+ return (void)log_notice("Skipping %s, dummy network interface not available", __func__);
+
+ if (!have_netns)
+ return (void)log_notice("Skipping %s, network namespace not available", __func__);
+
+ if (MANAGER_IS_USER(m) && !have_userns_privileges())
+ return (void)log_notice("Skipping %s, do not have user namespace privileges", __func__);
+
+ r = find_executable("ip", NULL);
+ if (r < 0) {
+ log_notice_errno(r, "Skipping %s, could not find ip binary: %m", __func__);
+ return;
+ }
+
+ test(m, "exec-networknamespacepath-privatemounts-no.service", MANAGER_IS_SYSTEM(m) ? EXIT_SUCCESS : EXIT_FAILURE, CLD_EXITED);
+ test(m, "exec-networknamespacepath-privatemounts-yes.service", can_unshare ? EXIT_SUCCESS : MANAGER_IS_SYSTEM(m) ? EXIT_FAILURE : EXIT_NAMESPACE, CLD_EXITED);
+}
+
+static void test_exec_oomscoreadjust(Manager *m) {
+ test(m, "exec-oomscoreadjust-positive.service", 0, CLD_EXITED);
+
+ if (detect_container() > 0) {
+ log_notice("Testing in container, skipping remaining tests in %s", __func__);
+ return;
+ }
+ test(m, "exec-oomscoreadjust-negative.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_FAILURE, CLD_EXITED);
+}
+
+static void test_exec_ioschedulingclass(Manager *m) {
+ test(m, "exec-ioschedulingclass-none.service", 0, CLD_EXITED);
+ test(m, "exec-ioschedulingclass-idle.service", 0, CLD_EXITED);
+ test(m, "exec-ioschedulingclass-best-effort.service", 0, CLD_EXITED);
+
+ if (detect_container() > 0) {
+ log_notice("Testing in container, skipping remaining tests in %s", __func__);
+ return;
+ }
+ test(m, "exec-ioschedulingclass-realtime.service", MANAGER_IS_SYSTEM(m) ? 0 : EXIT_IOPRIO, CLD_EXITED);
+}
+
+static void test_exec_unsetenvironment(Manager *m) {
+ test(m, "exec-unsetenvironment.service", 0, CLD_EXITED);
+}
+
+static void test_exec_specifier(Manager *m) {
+ test(m, "exec-specifier.service", 0, CLD_EXITED);
+ if (MANAGER_IS_SYSTEM(m))
+ test(m, "exec-specifier-system.service", 0, CLD_EXITED);
+ else
+ test(m, "exec-specifier-user.service", 0, CLD_EXITED);
+ test(m, "exec-specifier@foo-bar.service", 0, CLD_EXITED);
+ test(m, "exec-specifier-interpolation.service", 0, CLD_EXITED);
+}
+
+static void test_exec_standardinput(Manager *m) {
+ test(m, "exec-standardinput-data.service", 0, CLD_EXITED);
+ test(m, "exec-standardinput-file.service", 0, CLD_EXITED);
+ test(m, "exec-standardinput-file-cat.service", 0, CLD_EXITED);
+}
+
+static void test_exec_standardoutput(Manager *m) {
+ test(m, "exec-standardoutput-file.service", 0, CLD_EXITED);
+}
+
+static void test_exec_standardoutput_append(Manager *m) {
+ test(m, "exec-standardoutput-append.service", 0, CLD_EXITED);
+}
+
+static void test_exec_standardoutput_truncate(Manager *m) {
+ test(m, "exec-standardoutput-truncate.service", 0, CLD_EXITED);
+}
+
+static void test_exec_condition(Manager *m) {
+ test_service(m, "exec-condition-failed.service", SERVICE_FAILURE_EXIT_CODE);
+ test_service(m, "exec-condition-skip.service", SERVICE_SKIP_CONDITION);
+}
+
+static void test_exec_umask_namespace(Manager *m) {
+ /* exec-specifier-credentials-dir.service creates /run/credentials and enables implicit
+ * InaccessiblePath= for the directory for all later services with mount namespace. */
+ if (!is_inaccessible_available()) {
+ log_notice("Testing without inaccessible, skipping %s", __func__);
+ return;
+ }
+ test(m, "exec-umask-namespace.service", can_unshare ? 0 : MANAGER_IS_SYSTEM(m) ? EXIT_NAMESPACE : EXIT_GROUP, CLD_EXITED);
+}
+
+typedef struct test_entry {
+ test_function_t f;
+ const char *name;
+} test_entry;
+
+#define entry(x) {x, #x}
+
+static void run_tests(RuntimeScope scope, char **patterns) {
+ _cleanup_(rm_rf_physical_and_freep) char *runtime_dir = NULL;
+ _cleanup_free_ char *unit_paths = NULL;
+ _cleanup_(manager_freep) Manager *m = NULL;
+ usec_t start, finish;
+ int r;
+
+ static const test_entry tests[] = {
+ entry(test_exec_basic),
+ entry(test_exec_ambientcapabilities),
+ entry(test_exec_bindpaths),
+ entry(test_exec_capabilityboundingset),
+ entry(test_exec_condition),
+ entry(test_exec_cpuaffinity),
+ entry(test_exec_credentials),
+ entry(test_exec_dynamicuser),
+ entry(test_exec_environment),
+ entry(test_exec_environmentfile),
+ entry(test_exec_execsearchpath),
+ entry(test_exec_execsearchpath_environment),
+ entry(test_exec_execsearchpath_environment_files),
+ entry(test_exec_execsearchpath_passenvironment),
+ entry(test_exec_execsearchpath_specifier),
+ entry(test_exec_group),
+ entry(test_exec_ignoresigpipe),
+ entry(test_exec_inaccessiblepaths),
+ entry(test_exec_ioschedulingclass),
+ entry(test_exec_mount_apivfs),
+ entry(test_exec_networknamespacepath),
+ entry(test_exec_noexecpaths),
+ entry(test_exec_oomscoreadjust),
+ entry(test_exec_passenvironment),
+ entry(test_exec_personality),
+ entry(test_exec_privatedevices),
+ entry(test_exec_privatenetwork),
+ entry(test_exec_privatetmp),
+ entry(test_exec_protecthome),
+ entry(test_exec_protectkernelmodules),
+ entry(test_exec_readonlypaths),
+ entry(test_exec_readwritepaths),
+ entry(test_exec_restrictnamespaces),
+ entry(test_exec_runtimedirectory),
+ entry(test_exec_specifier),
+ entry(test_exec_standardinput),
+ entry(test_exec_standardoutput),
+ entry(test_exec_standardoutput_append),
+ entry(test_exec_standardoutput_truncate),
+ entry(test_exec_supplementarygroups),
+ entry(test_exec_systemcallerrornumber),
+ entry(test_exec_systemcallfilter),
+ entry(test_exec_systemcallfilter_system),
+ entry(test_exec_temporaryfilesystem),
+ entry(test_exec_umask),
+ entry(test_exec_umask_namespace),
+ entry(test_exec_unsetenvironment),
+ entry(test_exec_user),
+ entry(test_exec_workingdirectory),
+ {},
+ };
+
+ assert_se(unsetenv("USER") == 0);
+ assert_se(unsetenv("LOGNAME") == 0);
+ assert_se(unsetenv("SHELL") == 0);
+ assert_se(unsetenv("HOME") == 0);
+ assert_se(unsetenv("TMPDIR") == 0);
+
+ /* Unset VARx, especially, VAR1, VAR2 and VAR3, which are used in the PassEnvironment test cases,
+ * otherwise (and if they are present in the environment), `manager_default_environment` will copy
+ * them into the default environment which is passed to each created job, which will make the tests
+ * that expect those not to be present to fail. */
+ assert_se(unsetenv("VAR1") == 0);
+ assert_se(unsetenv("VAR2") == 0);
+ assert_se(unsetenv("VAR3") == 0);
+ assert_se(unsetenv("VAR4") == 0);
+ assert_se(unsetenv("VAR5") == 0);
+
+ assert_se(runtime_dir = setup_fake_runtime_dir());
+ assert_se(user_runtime_unit_dir = path_join(runtime_dir, "systemd/user"));
+ assert_se(unit_paths = strjoin(PRIVATE_UNIT_DIR, ":", user_runtime_unit_dir));
+ assert_se(set_unit_path(unit_paths) >= 0);
+
+ r = manager_new(scope, MANAGER_TEST_RUN_BASIC, &m);
+ if (manager_errno_skip_test(r))
+ return (void) log_tests_skipped_errno(r, "manager_new");
+ assert_se(r >= 0);
+
+ m->defaults.std_output = EXEC_OUTPUT_NULL; /* don't rely on host journald */
+ assert_se(manager_startup(m, NULL, NULL, NULL) >= 0);
+
+ /* Uncomment below if you want to make debugging logs stored to journal. */
+ //manager_override_log_target(m, LOG_TARGET_AUTO);
+ //manager_override_log_level(m, LOG_DEBUG);
+
+ /* Measure and print the time that it takes to run tests, excluding startup of the manager object,
+ * to try and measure latency of spawning services */
+ n_ran_tests = 0;
+ start = now(CLOCK_MONOTONIC);
+
+ for (const test_entry *test = tests; test->f; test++)
+ if (strv_fnmatch_or_empty(patterns, test->name, FNM_NOESCAPE))
+ test->f(m);
+ else
+ log_info("Skipping %s because it does not match any pattern.", test->name);
+
+ finish = now(CLOCK_MONOTONIC);
+
+ log_info("ran %u tests with %s manager + unshare=%s in: %s",
+ n_ran_tests,
+ scope == RUNTIME_SCOPE_SYSTEM ? "system" : "user",
+ yes_no(can_unshare),
+ FORMAT_TIMESPAN(finish - start, USEC_PER_MSEC));
+}
+
+static int prepare_ns(const char *process_name) {
+ int r;
+
+ r = safe_fork(process_name,
+ FORK_RESET_SIGNALS |
+ FORK_CLOSE_ALL_FDS |
+ FORK_DEATHSIG_SIGTERM |
+ FORK_WAIT |
+ FORK_REOPEN_LOG |
+ FORK_LOG |
+ FORK_NEW_MOUNTNS |
+ FORK_MOUNTNS_SLAVE,
+ NULL);
+ assert_se(r >= 0);
+ if (r == 0) {
+ _cleanup_free_ char *unit_dir = NULL;
+
+ /* Make "/" read-only. */
+ assert_se(mount_nofollow_verbose(LOG_DEBUG, NULL, "/", NULL, MS_BIND|MS_REMOUNT|MS_RDONLY, NULL) >= 0);
+
+ /* Creating a new user namespace in the above means all MS_SHARED mounts become MS_SLAVE.
+ * Let's put them back to MS_SHARED here, since that's what we want as defaults. (This will
+ * not reconnect propagation, but simply create new peer groups for all our mounts). */
+ assert_se(mount_follow_verbose(LOG_DEBUG, NULL, "/", NULL, MS_SHARED|MS_REC, NULL) >= 0);
+
+ assert_se(mkdir_p(PRIVATE_UNIT_DIR, 0755) >= 0);
+ assert_se(mount_nofollow_verbose(LOG_DEBUG, "tmpfs", PRIVATE_UNIT_DIR, "tmpfs", MS_NOSUID|MS_NODEV, NULL) >= 0);
+
+ /* Copy unit files to make them accessible even when unprivileged. */
+ assert_se(get_testdata_dir("test-execute/", &unit_dir) >= 0);
+ assert_se(copy_directory_at(AT_FDCWD, unit_dir, AT_FDCWD, PRIVATE_UNIT_DIR, COPY_MERGE_EMPTY) >= 0);
+
+ /* Mount tmpfs on the following directories to make not StateDirectory= or friends disturb the host. */
+ FOREACH_STRING(p, "/dev/shm", "/root", "/tmp", "/var/tmp", "/var/lib")
+ assert_se(mount_nofollow_verbose(LOG_DEBUG, "tmpfs", p, "tmpfs", MS_NOSUID|MS_NODEV, NULL) >= 0);
+
+ /* Prepare credstore like tmpfiles.d/credstore.conf for LoadCredential= tests. */
+ FOREACH_STRING(p, "/run/credstore", "/run/credstore.encrypted") {
+ assert_se(mkdir_p(p, 0) >= 0);
+ assert_se(mount_nofollow_verbose(LOG_DEBUG, "tmpfs", p, "tmpfs", MS_NOSUID|MS_NODEV, "mode=0000") >= 0);
+ }
+
+ assert_se(write_string_file("/run/credstore/test-execute.load-credential", "foo", WRITE_STRING_FILE_CREATE) >= 0);
+ }
+
+ return r;
+}
+
+TEST(run_tests_root) {
+ _cleanup_strv_free_ char **filters = NULL;
+
+ if (!have_namespaces())
+ return (void) log_tests_skipped("unshare() is disabled");
+
+ /* safe_fork() clears saved_argv in the child process. Let's copy it. */
+ assert_se(filters = strv_copy(strv_skip(saved_argv, 1)));
+
+ if (prepare_ns("(test-execute-root)") == 0) {
+ can_unshare = true;
+ run_tests(RUNTIME_SCOPE_SYSTEM, filters);
+ _exit(EXIT_SUCCESS);
+ }
+}
+
+TEST(run_tests_without_unshare) {
+ if (!have_namespaces()) {
+ /* unshare() is already filtered. */
+ can_unshare = false;
+ run_tests(RUNTIME_SCOPE_SYSTEM, strv_skip(saved_argv, 1));
+ return;
+ }
+
+#if HAVE_SECCOMP
+ _cleanup_strv_free_ char **filters = NULL;
+ int r;
+
+ /* The following tests are for 1beab8b0d0ff2d7d1436b52d4a0c3d56dc908962. */
+ if (!is_seccomp_available())
+ return (void) log_tests_skipped("Seccomp not available, cannot run unshare() filtered tests");
+
+ /* safe_fork() clears saved_argv in the child process. Let's copy it. */
+ assert_se(filters = strv_copy(strv_skip(saved_argv, 1)));
+
+ if (prepare_ns("(test-execute-without-unshare)") == 0) {
+ _cleanup_hashmap_free_ Hashmap *s = NULL;
+
+ r = seccomp_syscall_resolve_name("unshare");
+ assert_se(r != __NR_SCMP_ERROR);
+ assert_se(hashmap_ensure_put(&s, NULL, UINT32_TO_PTR(r + 1), INT_TO_PTR(-1)) >= 0);
+ assert_se(seccomp_load_syscall_filter_set_raw(SCMP_ACT_ALLOW, s, SCMP_ACT_ERRNO(EOPNOTSUPP), true) >= 0);
+
+ /* Check unshare() is actually filtered. */
+ assert_se(unshare(CLONE_NEWNS) < 0);
+ assert_se(errno == EOPNOTSUPP);
+
+ can_unshare = false;
+ run_tests(RUNTIME_SCOPE_SYSTEM, filters);
+ _exit(EXIT_SUCCESS);
+ }
+#else
+ log_tests_skipped("Built without seccomp support, cannot run unshare() filtered tests");
+#endif
+}
+
+TEST(run_tests_unprivileged) {
+ _cleanup_strv_free_ char **filters = NULL;
+
+ if (!have_namespaces())
+ return (void) log_tests_skipped("unshare() is disabled");
+
+ /* safe_fork() clears saved_argv in the child process. Let's copy it. */
+ assert_se(filters = strv_copy(strv_skip(saved_argv, 1)));
+
+ if (prepare_ns("(test-execute-unprivileged)") == 0) {
+ assert_se(capability_bounding_set_drop(0, /* right_now = */ true) >= 0);
+
+ can_unshare = false;
+ run_tests(RUNTIME_SCOPE_USER, filters);
+ _exit(EXIT_SUCCESS);
+ }
+}
+
+static int intro(void) {
+#if HAS_FEATURE_ADDRESS_SANITIZER
+ if (strstr_ptr(ci_environment(), "travis") || strstr_ptr(ci_environment(), "github-actions"))
+ return log_tests_skipped("Running on Travis CI/GH Actions under ASan, see https://github.com/systemd/systemd/issues/10696");
+#endif
+ /* It is needed otherwise cgroup creation fails */
+ if (geteuid() != 0 || have_effective_cap(CAP_SYS_ADMIN) <= 0)
+ return log_tests_skipped("not privileged");
+
+ if (enter_cgroup_subroot(NULL) == -ENOMEDIUM)
+ return log_tests_skipped("cgroupfs not available");
+
+ if (path_is_read_only_fs("/sys") > 0)
+ return log_tests_skipped("/sys is mounted read-only");
+
+ /* Create dummy network interface for testing PrivateNetwork=yes */
+ have_net_dummy = system("ip link add dummy-test-exec type dummy") == 0;
+
+ if (have_net_dummy) {
+ /* Create a network namespace and a dummy interface in it for NetworkNamespacePath= */
+ have_netns = system("ip netns add test-execute-netns") == 0;
+ have_netns = have_netns && system("ip netns exec test-execute-netns ip link add dummy-test-ns type dummy") == 0;
+ }
+
+ return EXIT_SUCCESS;
+}
+
+static int outro(void) {
+ if (have_net_dummy) {
+ (void) system("ip link del dummy-test-exec");
+ (void) system("ip netns del test-execute-netns");
+ }
+
+ (void) rmdir(PRIVATE_UNIT_DIR);
+
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_FULL(LOG_DEBUG, intro, outro);
diff --git a/src/test/test-execve.c b/src/test/test-execve.c
new file mode 100644
index 0000000..e7a9a51
--- /dev/null
+++ b/src/test/test-execve.c
@@ -0,0 +1,40 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "exec-util.h"
+#include "fd-util.h"
+#include "log.h"
+#include "main-func.h"
+#include "strv.h"
+#include "tests.h"
+
+/* This program can be used to call programs through fexecve / execveat(…, "", …, AT_EMPTY_PATH),
+ * when compiled with -Dfexecve=true, and the fallback paths, when -Dfexecve=false.
+ *
+ * Example:
+ * $ strace -e execveat build/test-execve /bin/grep Name /proc/self/status
+ * execveat(3, "", ["/bin/grep", "Name", "/proc/self/status"], NULL, AT_EMPTY_PATH) = 0
+ * Name: 3
+ *
+ * FIXME: use the new kernel api to set COMM properly when the kernel makes that available.
+ * C.f. ceedbf8185fc7593366679f02d31da63af8c4bd1.
+ */
+
+static int run(int argc, char **argv) {
+ _cleanup_close_ int fd = -EBADF;
+ char **args = strv_skip(argv, 1);
+ int r;
+
+ test_setup_logging(LOG_DEBUG);
+
+ args = !strv_isempty(args) ? args : STRV_MAKE("/bin/true");
+
+ fd = open(args[0], O_RDONLY | O_CLOEXEC);
+ if (fd < 0)
+ return log_error_errno(errno, "open(%s) failed: %m", args[0]);
+
+ r = fexecve_or_execve(fd, args[0], args, NULL);
+ assert_se(r < 0);
+ return log_error_errno(r, "fexecve_or_execve(%s) failed: %m", args[0]);
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/test/test-exit-status.c b/src/test/test-exit-status.c
new file mode 100644
index 0000000..86d3976
--- /dev/null
+++ b/src/test/test-exit-status.c
@@ -0,0 +1,38 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "exit-status.h"
+#include "string-util.h"
+#include "tests.h"
+
+TEST(exit_status_to_string) {
+ for (int i = -1; i <= 256; i++) {
+ const char *s, *class;
+
+ s = exit_status_to_string(i, EXIT_STATUS_FULL);
+ class = exit_status_class(i);
+ log_info("%d: %s%s%s%s",
+ i, s ?: "-",
+ class ? " (" : "", strempty(class), class ? ")" : "");
+
+ if (s)
+ assert_se(exit_status_from_string(s) == i);
+ }
+}
+
+TEST(exit_status_from_string) {
+ assert_se(exit_status_from_string("11") == 11);
+ assert_se(exit_status_from_string("-1") == -ERANGE);
+ assert_se(exit_status_from_string("256") == -ERANGE);
+ assert_se(exit_status_from_string("foo") == -EINVAL);
+ assert_se(exit_status_from_string("SUCCESS") == 0);
+ assert_se(exit_status_from_string("FAILURE") == 1);
+}
+
+TEST(exit_status_NUMA_POLICY) {
+ assert_se(streq(exit_status_to_string(EXIT_NUMA_POLICY, EXIT_STATUS_FULL), "NUMA_POLICY"));
+ assert_se(streq(exit_status_to_string(EXIT_NUMA_POLICY, EXIT_STATUS_SYSTEMD), "NUMA_POLICY"));
+ assert_se(!exit_status_to_string(EXIT_NUMA_POLICY, EXIT_STATUS_BSD));
+ assert_se(!exit_status_to_string(EXIT_NUMA_POLICY, EXIT_STATUS_LSB));
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-extract-word.c b/src/test/test-extract-word.c
new file mode 100644
index 0000000..6e12fbe
--- /dev/null
+++ b/src/test/test-extract-word.c
@@ -0,0 +1,763 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <stdlib.h>
+
+#include "extract-word.h"
+#include "log.h"
+#include "string-util.h"
+#include "tests.h"
+
+TEST(extract_first_word) {
+ const char *p, *original;
+ char *t;
+
+ p = original = "foobar waldo";
+ assert_se(extract_first_word(&p, &t, NULL, 0) > 0);
+ assert_se(streq(t, "foobar"));
+ free(t);
+ assert_se(p == original + 7);
+
+ assert_se(extract_first_word(&p, &t, NULL, 0) > 0);
+ assert_se(streq(t, "waldo"));
+ free(t);
+ assert_se(isempty(p));
+
+ assert_se(extract_first_word(&p, &t, NULL, 0) == 0);
+ assert_se(!t);
+ assert_se(isempty(p));
+
+ p = original = "\"foobar\" \'waldo\'";
+ assert_se(extract_first_word(&p, &t, NULL, 0) > 0);
+ assert_se(streq(t, "\"foobar\""));
+ free(t);
+ assert_se(p == original + 9);
+
+ assert_se(extract_first_word(&p, &t, NULL, 0) > 0);
+ assert_se(streq(t, "\'waldo\'"));
+ free(t);
+ assert_se(isempty(p));
+
+ assert_se(extract_first_word(&p, &t, NULL, 0) == 0);
+ assert_se(!t);
+ assert_se(isempty(p));
+
+ p = original = "\"foobar\" \'waldo\'";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNQUOTE) > 0);
+ assert_se(streq(t, "foobar"));
+ free(t);
+ assert_se(p == original + 9);
+
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNQUOTE) > 0);
+ assert_se(streq(t, "waldo"));
+ free(t);
+ assert_se(isempty(p));
+
+ assert_se(extract_first_word(&p, &t, NULL, 0) == 0);
+ assert_se(!t);
+ assert_se(isempty(p));
+
+ p = original = "\"";
+ assert_se(extract_first_word(&p, &t, NULL, 0) == 1);
+ assert_se(streq(t, "\""));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "\"";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNQUOTE) == -EINVAL);
+ assert_se(p == original + 1);
+
+ p = original = "\'";
+ assert_se(extract_first_word(&p, &t, NULL, 0) == 1);
+ assert_se(streq(t, "\'"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "\'";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNQUOTE) == -EINVAL);
+ assert_se(p == original + 1);
+
+ p = original = "\'fooo";
+ assert_se(extract_first_word(&p, &t, NULL, 0) == 1);
+ assert_se(streq(t, "\'fooo"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "KEY=val \"KEY2=val with space\" \"KEY3=val with \\\"quotation\\\"\"";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNQUOTE) == 1);
+ assert_se(streq(t, "KEY=val"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNQUOTE) == 1);
+ assert_se(streq(t, "KEY2=val with space"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNQUOTE) == 1);
+ assert_se(streq(t, "KEY3=val with \"quotation\""));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "KEY=val \"KEY2=val space\" \"KEY3=val with \\\"quotation\\\"\"";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_RETAIN_ESCAPE) == 1);
+ assert_se(streq(t, "KEY=val"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_RETAIN_ESCAPE) == 1);
+ assert_se(streq(t, "\"KEY2=val"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_RETAIN_ESCAPE) == 1);
+ assert_se(streq(t, "space\""));
+ free(t);
+ assert_se(startswith(p, "\"KEY3="));
+
+ p = original = "\'fooo";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNQUOTE) == -EINVAL);
+ assert_se(p == original + 5);
+
+ p = original = "\'fooo";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNQUOTE|EXTRACT_RELAX) > 0);
+ assert_se(streq(t, "fooo"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "\"fooo";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNQUOTE|EXTRACT_RELAX) > 0);
+ assert_se(streq(t, "fooo"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "yay\'foo\'bar";
+ assert_se(extract_first_word(&p, &t, NULL, 0) > 0);
+ assert_se(streq(t, "yay\'foo\'bar"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "yay\'foo\'bar";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNQUOTE) > 0);
+ assert_se(streq(t, "yayfoobar"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = " foobar ";
+ assert_se(extract_first_word(&p, &t, NULL, 0) > 0);
+ assert_se(streq(t, "foobar"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = " foo\\ba\\x6ar ";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_CUNESCAPE) > 0);
+ assert_se(streq(t, "foo\ba\x6ar"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = " foo\\ba\\x6ar ";
+ assert_se(extract_first_word(&p, &t, NULL, 0) > 0);
+ assert_se(streq(t, "foobax6ar"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = " f\\u00f6o \"pi\\U0001F4A9le\" ";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_CUNESCAPE) > 0);
+ assert_se(streq(t, "föo"));
+ free(t);
+ assert_se(p == original + 13);
+
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNQUOTE|EXTRACT_CUNESCAPE) > 0);
+ assert_se(streq(t, "pi\360\237\222\251le"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "fooo\\";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_RELAX) > 0);
+ assert_se(streq(t, "fooo"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "fooo\\";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNESCAPE_RELAX) > 0);
+ assert_se(streq(t, "fooo\\"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "fooo\\";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNESCAPE_RELAX|EXTRACT_RELAX) > 0);
+ assert_se(streq(t, "fooo\\"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "fooo\\";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_RELAX) > 0);
+ assert_se(streq(t, "fooo\\"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "\"foo\\";
+ assert_se(extract_first_word(&p, &t, NULL, 0) == -EINVAL);
+ assert_se(p == original + 5);
+
+ p = original = "\"foo\\";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNQUOTE|EXTRACT_RELAX) > 0);
+ assert_se(streq(t, "foo"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "foo::bar";
+ assert_se(extract_first_word(&p, &t, ":", 0) == 1);
+ assert_se(streq(t, "foo"));
+ free(t);
+ assert_se(p == original + 5);
+
+ assert_se(extract_first_word(&p, &t, ":", 0) == 1);
+ assert_se(streq(t, "bar"));
+ free(t);
+ assert_se(isempty(p));
+
+ assert_se(extract_first_word(&p, &t, ":", 0) == 0);
+ assert_se(!t);
+ assert_se(isempty(p));
+
+ p = original = "foo\\:bar::waldo";
+ assert_se(extract_first_word(&p, &t, ":", 0) == 1);
+ assert_se(streq(t, "foo:bar"));
+ free(t);
+ assert_se(p == original + 10);
+
+ assert_se(extract_first_word(&p, &t, ":", 0) == 1);
+ assert_se(streq(t, "waldo"));
+ free(t);
+ assert_se(isempty(p));
+
+ assert_se(extract_first_word(&p, &t, ":", 0) == 0);
+ assert_se(!t);
+ assert_se(isempty(p));
+
+ p = original = "\"foo\\";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNQUOTE|EXTRACT_UNESCAPE_RELAX) == -EINVAL);
+ assert_se(p == original + 5);
+
+ p = original = "\"foo\\";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNQUOTE|EXTRACT_UNESCAPE_RELAX|EXTRACT_RELAX) > 0);
+ assert_se(streq(t, "foo\\"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "\"foo\\";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNQUOTE|EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_RELAX|EXTRACT_RELAX) > 0);
+ assert_se(streq(t, "foo\\"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "fooo\\ bar quux";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_RELAX) > 0);
+ assert_se(streq(t, "fooo bar"));
+ free(t);
+ assert_se(p == original + 10);
+
+ p = original = "fooo\\ bar quux";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNESCAPE_RELAX) > 0);
+ assert_se(streq(t, "fooo bar"));
+ free(t);
+ assert_se(p == original + 10);
+
+ p = original = "fooo\\ bar quux";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNESCAPE_RELAX|EXTRACT_RELAX) > 0);
+ assert_se(streq(t, "fooo bar"));
+ free(t);
+ assert_se(p == original + 10);
+
+ p = original = "fooo\\ bar quux";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_CUNESCAPE) == -EINVAL);
+ assert_se(p == original + 5);
+
+ p = original = "fooo\\ bar quux";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_RELAX) > 0);
+ assert_se(streq(t, "fooo\\ bar"));
+ free(t);
+ assert_se(p == original + 10);
+
+ p = original = "\\w+@\\K[\\d.]+";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_CUNESCAPE) == -EINVAL);
+ assert_se(p == original + 1);
+
+ p = original = "\\w+@\\K[\\d.]+";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_RELAX) > 0);
+ assert_se(streq(t, "\\w+@\\K[\\d.]+"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "\\w+\\b";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_RELAX) > 0);
+ assert_se(streq(t, "\\w+\b"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "-N ''";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNQUOTE) > 0);
+ assert_se(streq(t, "-N"));
+ free(t);
+ assert_se(p == original + 3);
+
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_UNQUOTE) > 0);
+ assert_se(streq(t, ""));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = ":foo\\:bar::waldo:";
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_DONT_COALESCE_SEPARATORS) == 1);
+ assert_se(t);
+ assert_se(streq(t, ""));
+ free(t);
+ assert_se(p == original + 1);
+
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_DONT_COALESCE_SEPARATORS) == 1);
+ assert_se(streq(t, "foo:bar"));
+ free(t);
+ assert_se(p == original + 10);
+
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_DONT_COALESCE_SEPARATORS) == 1);
+ assert_se(t);
+ assert_se(streq(t, ""));
+ free(t);
+ assert_se(p == original + 11);
+
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_DONT_COALESCE_SEPARATORS) == 1);
+ assert_se(streq(t, "waldo"));
+ free(t);
+ assert_se(p == original + 17);
+
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_DONT_COALESCE_SEPARATORS) == 1);
+ assert_se(streq(t, ""));
+ free(t);
+ assert_se(p == NULL);
+
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_DONT_COALESCE_SEPARATORS) == 0);
+ assert_se(!t);
+ assert_se(!p);
+
+ p = "foo\\xbar";
+ assert_se(extract_first_word(&p, &t, NULL, 0) > 0);
+ assert_se(streq(t, "fooxbar"));
+ free(t);
+ assert_se(p == NULL);
+
+ p = "foo\\xbar";
+ assert_se(extract_first_word(&p, &t, NULL, EXTRACT_RETAIN_ESCAPE) > 0);
+ assert_se(streq(t, "foo\\xbar"));
+ free(t);
+ assert_se(p == NULL);
+
+ p = "\\:";
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_UNESCAPE_SEPARATORS) == 1);
+ assert_se(streq(t, ":"));
+ free(t);
+ assert_se(p == NULL);
+
+ p = "a\\:b";
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_UNESCAPE_SEPARATORS) == 1);
+ assert_se(streq(t, "a:b"));
+ free(t);
+ assert_se(p == NULL);
+
+ p = "a\\ b:c";
+ assert_se(extract_first_word(&p, &t, WHITESPACE ":", EXTRACT_UNESCAPE_SEPARATORS) == 1);
+ assert_se(streq(t, "a b"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, WHITESPACE ":", EXTRACT_UNESCAPE_SEPARATORS) == 1);
+ assert_se(streq(t, "c"));
+ free(t);
+ assert_se(p == NULL);
+
+ p = "a\\ b:c\\x";
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_UNESCAPE_SEPARATORS) == -EINVAL);
+
+ p = "a\\\\ b:c\\\\x";
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_UNESCAPE_SEPARATORS) == 1);
+ assert_se(streq(t, "a\\ b"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_UNESCAPE_SEPARATORS) == 1);
+ assert_se(streq(t, "c\\x"));
+ free(t);
+ assert_se(p == NULL);
+
+ p = "\\:";
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_SEPARATORS) == 1);
+ assert_se(streq(t, ":"));
+ free(t);
+ assert_se(p == NULL);
+
+ p = "a\\:b";
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_SEPARATORS) == 1);
+ assert_se(streq(t, "a:b"));
+ free(t);
+ assert_se(p == NULL);
+
+ p = "a\\ b:c";
+ assert_se(extract_first_word(&p, &t, WHITESPACE ":", EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_SEPARATORS) == 1);
+ assert_se(streq(t, "a b"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, WHITESPACE ":", EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_SEPARATORS) == 1);
+ assert_se(streq(t, "c"));
+ free(t);
+ assert_se(p == NULL);
+
+ p = "a\\ b:c\\x";
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_SEPARATORS) == -EINVAL);
+
+ p = "a\\\\ b:c\\\\x";
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_SEPARATORS) == 1);
+ assert_se(streq(t, "a\\ b"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_SEPARATORS) == 1);
+ assert_se(streq(t, "c\\x"));
+ free(t);
+ assert_se(p == NULL);
+
+ p = "\\:";
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_CUNESCAPE) == -EINVAL);
+
+ p = "a\\:b";
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_CUNESCAPE) == -EINVAL);
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_CUNESCAPE) == 1);
+ assert_se(streq(t, "b"));
+ free(t);
+
+ p = "a\\ b:c";
+ assert_se(extract_first_word(&p, &t, WHITESPACE ":", EXTRACT_CUNESCAPE) == -EINVAL);
+ assert_se(extract_first_word(&p, &t, WHITESPACE ":", EXTRACT_CUNESCAPE) == 1);
+ assert_se(streq(t, "b"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, WHITESPACE ":", EXTRACT_CUNESCAPE) == 1);
+ assert_se(streq(t, "c"));
+ free(t);
+ assert_se(p == NULL);
+
+ p = original = "foobar=\"waldo\"maldo, baldo";
+ assert_se(extract_first_word(&p, &t, "=\", ", 0) > 0);
+ assert_se(streq(t, "foobar"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, "=\", ", 0) > 0);
+ assert_se(streq(t, "waldo"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, "=\", ", 0) > 0);
+ assert_se(streq(t, "maldo"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, "=\", ", 0) > 0);
+ assert_se(streq(t, "baldo"));
+ free(t);
+
+ p = original = "mode=\"1777\",size=\"10%\",nr_inodes=\"400\"k,uid=\"496,,107\"520,gi\"\"'d=49610,'\"\"7520,context=\"system_u:object_r:svirt_sandbox_file_t:s0:c0,c1\"";
+ assert_se(extract_first_word(&p, &t, ",", EXTRACT_KEEP_QUOTE) > 0);
+ assert_se(streq(t, "mode=\"1777\""));
+ free(t);
+ assert_se(extract_first_word(&p, &t, ",", EXTRACT_KEEP_QUOTE) > 0);
+ assert_se(streq(t, "size=\"10%\""));
+ free(t);
+ assert_se(extract_first_word(&p, &t, ",", EXTRACT_KEEP_QUOTE) > 0);
+ assert_se(streq(t, "nr_inodes=\"400\"k"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, ",", EXTRACT_KEEP_QUOTE) > 0);
+ assert_se(streq(t, "uid=\"496,,107\"520"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, ",", EXTRACT_KEEP_QUOTE) > 0);
+ assert_se(streq(t, "gi\"\"'d=49610,'\"\"7520"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, ",", EXTRACT_KEEP_QUOTE) > 0);
+ assert_se(streq(t, "context=\"system_u:object_r:svirt_sandbox_file_t:s0:c0,c1\""));
+ free(t);
+
+ p = original = "mode=\"1777\",size=\"10%\",nr_inodes=\"400\"k,uid=\"496,,107\"520,gi\"\"'d=49610,'\"\"7520,context=\"system_u:object_r:svirt_sandbox_file_t:s0:c0,c1\"";
+ assert_se(extract_first_word(&p, &t, ",", EXTRACT_UNQUOTE) > 0);
+ assert_se(streq(t, "mode=1777"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, ",", EXTRACT_UNQUOTE) > 0);
+ assert_se(streq(t, "size=10%"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, ",", EXTRACT_UNQUOTE) > 0);
+ assert_se(streq(t, "nr_inodes=400k"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, ",", EXTRACT_UNQUOTE) > 0);
+ assert_se(streq(t, "uid=496,,107520"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, ",", EXTRACT_UNQUOTE) > 0);
+ assert_se(streq(t, "gid=49610,7520"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, ",", EXTRACT_UNQUOTE) > 0);
+ assert_se(streq(t, "context=system_u:object_r:svirt_sandbox_file_t:s0:c0,c1"));
+ free(t);
+
+ p = "a:b";
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_RETAIN_SEPARATORS) == 1);
+ assert_se(streq(t, "a"));
+ assert_se(streq(p, ":b"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_RETAIN_SEPARATORS) == 1);
+ assert_se(streq(t, "b"));
+ free(t);
+
+ p = "a>:b";
+ assert_se(extract_first_word(&p, &t, ">:", EXTRACT_RETAIN_SEPARATORS) == 1);
+ assert_se(streq(t, "a"));
+ assert_se(streq(p, ">:b"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, ">:", EXTRACT_RETAIN_SEPARATORS) == 1);
+ assert_se(streq(t, "b"));
+ free(t);
+
+ p = "a>:b";
+ assert_se(extract_first_word(&p, &t, ">:", EXTRACT_RETAIN_SEPARATORS|EXTRACT_DONT_COALESCE_SEPARATORS) == 1);
+ assert_se(streq(t, "a"));
+ assert_se(streq(p, ">:b"));
+ free(t);
+ assert_se(extract_first_word(&p, &t, ">:", EXTRACT_RETAIN_SEPARATORS|EXTRACT_DONT_COALESCE_SEPARATORS) == 1);
+ assert_se(streq(t, ""));
+ assert_se(streq(p, ">:b"));
+ free(t);
+
+ p = "a\\:b";
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_RETAIN_SEPARATORS|EXTRACT_RETAIN_ESCAPE) == 1);
+ assert_se(streq(t, "a\\"));
+ assert_se(streq(p, ":b"));
+ free(t);
+
+ p = "a\\:b";
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_RETAIN_SEPARATORS) == 1);
+ assert_se(streq(t, "a:b"));
+ assert_se(!p);
+ free(t);
+
+ p = "a\\:b";
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_RETAIN_SEPARATORS|EXTRACT_UNESCAPE_SEPARATORS) == 1);
+ assert_se(streq(t, "a:b"));
+ assert_se(!p);
+ free(t);
+
+ p = "a\\:a:b";
+ assert_se(extract_first_word(&p, &t, ":", EXTRACT_RETAIN_SEPARATORS|EXTRACT_UNESCAPE_SEPARATORS) == 1);
+ assert_se(streq(t, "a:a"));
+ assert_se(streq(p, ":b"));
+ free(t);
+}
+
+TEST(extract_first_word_and_warn) {
+ const char *p, *original;
+ char *t;
+
+ p = original = "foobar waldo";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, 0, NULL, "fake", 1, original) > 0);
+ assert_se(streq(t, "foobar"));
+ free(t);
+ assert_se(p == original + 7);
+
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, 0, NULL, "fake", 1, original) > 0);
+ assert_se(streq(t, "waldo"));
+ free(t);
+ assert_se(isempty(p));
+
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, 0, NULL, "fake", 1, original) == 0);
+ assert_se(!t);
+ assert_se(isempty(p));
+
+ p = original = "\"foobar\" \'waldo\'";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, EXTRACT_UNQUOTE, NULL, "fake", 1, original) > 0);
+ assert_se(streq(t, "foobar"));
+ free(t);
+ assert_se(p == original + 9);
+
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, EXTRACT_UNQUOTE, NULL, "fake", 1, original) > 0);
+ assert_se(streq(t, "waldo"));
+ free(t);
+ assert_se(isempty(p));
+
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, 0, NULL, "fake", 1, original) == 0);
+ assert_se(!t);
+ assert_se(isempty(p));
+
+ p = original = "\"";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, EXTRACT_UNQUOTE, NULL, "fake", 1, original) == -EINVAL);
+ assert_se(p == original + 1);
+
+ p = original = "\'";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, EXTRACT_UNQUOTE, NULL, "fake", 1, original) == -EINVAL);
+ assert_se(p == original + 1);
+
+ p = original = "\'fooo";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, EXTRACT_UNQUOTE, NULL, "fake", 1, original) == -EINVAL);
+ assert_se(p == original + 5);
+
+ p = original = "\'fooo";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, EXTRACT_UNQUOTE|EXTRACT_RELAX, NULL, "fake", 1, original) > 0);
+ assert_se(streq(t, "fooo"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = " foo\\ba\\x6ar ";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, EXTRACT_CUNESCAPE, NULL, "fake", 1, original) > 0);
+ assert_se(streq(t, "foo\ba\x6ar"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = " foo\\ba\\x6ar ";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, 0, NULL, "fake", 1, original) > 0);
+ assert_se(streq(t, "foobax6ar"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = " f\\u00f6o \"pi\\U0001F4A9le\" ";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, EXTRACT_CUNESCAPE, NULL, "fake", 1, original) > 0);
+ assert_se(streq(t, "föo"));
+ free(t);
+ assert_se(p == original + 13);
+
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, EXTRACT_UNQUOTE|EXTRACT_CUNESCAPE, NULL, "fake", 1, original) > 0);
+ assert_se(streq(t, "pi\360\237\222\251le"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "fooo\\";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, EXTRACT_RELAX, NULL, "fake", 1, original) > 0);
+ assert_se(streq(t, "fooo"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "fooo\\";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, 0, NULL, "fake", 1, original) > 0);
+ assert_se(streq(t, "fooo\\"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "fooo\\";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, EXTRACT_CUNESCAPE, NULL, "fake", 1, original) > 0);
+ assert_se(streq(t, "fooo\\"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "\"foo\\";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, EXTRACT_UNQUOTE, NULL, "fake", 1, original) == -EINVAL);
+ assert_se(p == original + 5);
+
+ p = original = "\"foo\\";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, EXTRACT_UNQUOTE|EXTRACT_RELAX, NULL, "fake", 1, original) > 0);
+ assert_se(streq(t, "foo"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "\"foo\\";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, EXTRACT_UNQUOTE|EXTRACT_CUNESCAPE, NULL, "fake", 1, original) == -EINVAL);
+ assert_se(p == original + 5);
+
+ p = original = "\"foo\\";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, EXTRACT_UNQUOTE|EXTRACT_CUNESCAPE|EXTRACT_RELAX, NULL, "fake", 1, original) > 0);
+ assert_se(streq(t, "foo"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "fooo\\ bar quux";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, EXTRACT_RELAX, NULL, "fake", 1, original) > 0);
+ assert_se(streq(t, "fooo bar"));
+ free(t);
+ assert_se(p == original + 10);
+
+ p = original = "fooo\\ bar quux";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, 0, NULL, "fake", 1, original) > 0);
+ assert_se(streq(t, "fooo bar"));
+ free(t);
+ assert_se(p == original + 10);
+
+ p = original = "fooo\\ bar quux";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, EXTRACT_CUNESCAPE, NULL, "fake", 1, original) > 0);
+ assert_se(streq(t, "fooo\\ bar"));
+ free(t);
+ assert_se(p == original + 10);
+
+ p = original = "\\w+@\\K[\\d.]+";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, EXTRACT_CUNESCAPE, NULL, "fake", 1, original) > 0);
+ assert_se(streq(t, "\\w+@\\K[\\d.]+"));
+ free(t);
+ assert_se(isempty(p));
+
+ p = original = "\\w+\\b";
+ assert_se(extract_first_word_and_warn(&p, &t, NULL, EXTRACT_CUNESCAPE, NULL, "fake", 1, original) > 0);
+ assert_se(streq(t, "\\w+\b"));
+ free(t);
+ assert_se(isempty(p));
+}
+
+TEST(extract_many_words) {
+ const char *p, *original;
+ char *a, *b, *c, *d, *e, *f;
+
+ p = original = "foobar waldi piep";
+ assert_se(extract_many_words(&p, NULL, 0, &a, &b, &c, NULL) == 3);
+ assert_se(isempty(p));
+ assert_se(streq_ptr(a, "foobar"));
+ assert_se(streq_ptr(b, "waldi"));
+ assert_se(streq_ptr(c, "piep"));
+ free(a);
+ free(b);
+ free(c);
+
+ p = original = "foobar:waldi:piep ba1:ba2";
+ assert_se(extract_many_words(&p, ":" WHITESPACE, 0, &a, &b, &c, NULL) == 3);
+ assert_se(!isempty(p));
+ assert_se(streq_ptr(a, "foobar"));
+ assert_se(streq_ptr(b, "waldi"));
+ assert_se(streq_ptr(c, "piep"));
+ assert_se(extract_many_words(&p, ":" WHITESPACE, 0, &d, &e, &f, NULL) == 2);
+ assert_se(isempty(p));
+ assert_se(streq_ptr(d, "ba1"));
+ assert_se(streq_ptr(e, "ba2"));
+ assert_se(isempty(f));
+ free(a);
+ free(b);
+ free(c);
+ free(d);
+ free(e);
+ free(f);
+
+ p = original = "'foobar' wa\"ld\"i ";
+ assert_se(extract_many_words(&p, NULL, 0, &a, &b, &c, NULL) == 2);
+ assert_se(isempty(p));
+ assert_se(streq_ptr(a, "'foobar'"));
+ assert_se(streq_ptr(b, "wa\"ld\"i"));
+ assert_se(streq_ptr(c, NULL));
+ free(a);
+ free(b);
+
+ p = original = "'foobar' wa\"ld\"i ";
+ assert_se(extract_many_words(&p, NULL, EXTRACT_UNQUOTE, &a, &b, &c, NULL) == 2);
+ assert_se(isempty(p));
+ assert_se(streq_ptr(a, "foobar"));
+ assert_se(streq_ptr(b, "waldi"));
+ assert_se(streq_ptr(c, NULL));
+ free(a);
+ free(b);
+
+ p = original = "";
+ assert_se(extract_many_words(&p, NULL, 0, &a, &b, &c, NULL) == 0);
+ assert_se(isempty(p));
+ assert_se(streq_ptr(a, NULL));
+ assert_se(streq_ptr(b, NULL));
+ assert_se(streq_ptr(c, NULL));
+
+ p = original = " ";
+ assert_se(extract_many_words(&p, NULL, 0, &a, &b, &c, NULL) == 0);
+ assert_se(isempty(p));
+ assert_se(streq_ptr(a, NULL));
+ assert_se(streq_ptr(b, NULL));
+ assert_se(streq_ptr(c, NULL));
+
+ p = original = "foobar";
+ assert_se(extract_many_words(&p, NULL, 0, NULL) == 0);
+ assert_se(p == original);
+
+ p = original = "foobar waldi";
+ assert_se(extract_many_words(&p, NULL, 0, &a, NULL) == 1);
+ assert_se(p == original+7);
+ assert_se(streq_ptr(a, "foobar"));
+ free(a);
+
+ p = original = " foobar ";
+ assert_se(extract_many_words(&p, NULL, 0, &a, NULL) == 1);
+ assert_se(isempty(p));
+ assert_se(streq_ptr(a, "foobar"));
+ free(a);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-fd-util.c b/src/test/test-fd-util.c
new file mode 100644
index 0000000..021d4b4
--- /dev/null
+++ b/src/test/test-fd-util.c
@@ -0,0 +1,765 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <sys/eventfd.h>
+#include <sys/mount.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "data-fd-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "macro.h"
+#include "memory-util.h"
+#include "missing_syscall.h"
+#include "mkdir.h"
+#include "mount-util.h"
+#include "namespace-util.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "random-util.h"
+#include "rlimit-util.h"
+#include "rm-rf.h"
+#include "seccomp-util.h"
+#include "serialize.h"
+#include "stat-util.h"
+#include "string-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+TEST(close_many) {
+ int fds[3];
+ _cleanup_(unlink_tempfilep) char name0[] = "/tmp/test-close-many.XXXXXX";
+ _cleanup_(unlink_tempfilep) char name1[] = "/tmp/test-close-many.XXXXXX";
+ _cleanup_(unlink_tempfilep) char name2[] = "/tmp/test-close-many.XXXXXX";
+
+ fds[0] = mkostemp_safe(name0);
+ fds[1] = mkostemp_safe(name1);
+ fds[2] = mkostemp_safe(name2);
+
+ close_many(fds, 2);
+
+ assert_se(fcntl(fds[0], F_GETFD) == -1);
+ assert_se(fcntl(fds[1], F_GETFD) == -1);
+ assert_se(fcntl(fds[2], F_GETFD) >= 0);
+
+ safe_close(fds[2]);
+}
+
+TEST(close_nointr) {
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-test-close_nointr.XXXXXX";
+ int fd;
+
+ fd = mkostemp_safe(name);
+ assert_se(fd >= 0);
+ assert_se(close_nointr(fd) >= 0);
+ assert_se(close_nointr(fd) < 0);
+}
+
+TEST(same_fd) {
+ _cleanup_close_pair_ int p[2];
+ _cleanup_close_ int a, b, c;
+
+ assert_se(pipe2(p, O_CLOEXEC) >= 0);
+ assert_se((a = fcntl(p[0], F_DUPFD, 3)) >= 0);
+ assert_se((b = open("/dev/null", O_RDONLY|O_CLOEXEC)) >= 0);
+ assert_se((c = fcntl(a, F_DUPFD, 3)) >= 0);
+
+ assert_se(same_fd(p[0], p[0]) > 0);
+ assert_se(same_fd(p[1], p[1]) > 0);
+ assert_se(same_fd(a, a) > 0);
+ assert_se(same_fd(b, b) > 0);
+
+ assert_se(same_fd(a, p[0]) > 0);
+ assert_se(same_fd(p[0], a) > 0);
+ assert_se(same_fd(c, p[0]) > 0);
+ assert_se(same_fd(p[0], c) > 0);
+ assert_se(same_fd(a, c) > 0);
+ assert_se(same_fd(c, a) > 0);
+
+ assert_se(same_fd(p[0], p[1]) == 0);
+ assert_se(same_fd(p[1], p[0]) == 0);
+ assert_se(same_fd(p[0], b) == 0);
+ assert_se(same_fd(b, p[0]) == 0);
+ assert_se(same_fd(p[1], a) == 0);
+ assert_se(same_fd(a, p[1]) == 0);
+ assert_se(same_fd(p[1], b) == 0);
+ assert_se(same_fd(b, p[1]) == 0);
+
+ assert_se(same_fd(a, b) == 0);
+ assert_se(same_fd(b, a) == 0);
+}
+
+TEST(open_serialization_fd) {
+ _cleanup_close_ int fd = -EBADF;
+
+ fd = open_serialization_fd("test");
+ assert_se(fd >= 0);
+
+ assert_se(write(fd, "test\n", 5) == 5);
+}
+
+TEST(open_serialization_file) {
+ _cleanup_fclose_ FILE *f = NULL;
+ int r;
+
+ r = open_serialization_file("test", &f);
+ assert_se(r >= 0);
+ assert_se(f);
+
+ assert_se(fwrite("test\n", 1, 5, f) == 5);
+}
+
+TEST(fd_move_above_stdio) {
+ int original_stdin, new_fd;
+
+ original_stdin = fcntl(0, F_DUPFD, 3);
+ assert_se(original_stdin >= 3);
+ assert_se(close_nointr(0) != EBADF);
+
+ new_fd = open("/dev/null", O_RDONLY);
+ assert_se(new_fd == 0);
+
+ new_fd = fd_move_above_stdio(new_fd);
+ assert_se(new_fd >= 3);
+
+ assert_se(dup(original_stdin) == 0);
+ assert_se(close_nointr(original_stdin) != EBADF);
+ assert_se(close_nointr(new_fd) != EBADF);
+}
+
+TEST(rearrange_stdio) {
+ pid_t pid;
+ int r;
+
+ r = safe_fork("rearrange", FORK_WAIT|FORK_LOG, &pid);
+ assert_se(r >= 0);
+
+ if (r == 0) {
+ _cleanup_free_ char *path = NULL;
+ char buffer[10];
+
+ /* Child */
+
+ safe_close(STDERR_FILENO); /* Let's close an fd < 2, to make it more interesting */
+
+ assert_se(rearrange_stdio(-EBADF, -EBADF, -EBADF) >= 0);
+
+ assert_se(fd_get_path(STDIN_FILENO, &path) >= 0);
+ assert_se(path_equal(path, "/dev/null"));
+ path = mfree(path);
+
+ assert_se(fd_get_path(STDOUT_FILENO, &path) >= 0);
+ assert_se(path_equal(path, "/dev/null"));
+ path = mfree(path);
+
+ assert_se(fd_get_path(STDOUT_FILENO, &path) >= 0);
+ assert_se(path_equal(path, "/dev/null"));
+ path = mfree(path);
+
+ safe_close(STDIN_FILENO);
+ safe_close(STDOUT_FILENO);
+ safe_close(STDERR_FILENO);
+
+ {
+ int pair[2];
+ assert_se(pipe(pair) >= 0);
+ assert_se(pair[0] == 0);
+ assert_se(pair[1] == 1);
+ assert_se(fd_move_above_stdio(0) == 3);
+ }
+ assert_se(open("/dev/full", O_WRONLY|O_CLOEXEC) == 0);
+ assert_se(acquire_data_fd("foobar", 6, 0) == 2);
+
+ assert_se(rearrange_stdio(2, 0, 1) >= 0);
+
+ assert_se(write(1, "x", 1) < 0 && errno == ENOSPC);
+ assert_se(write(2, "z", 1) == 1);
+ assert_se(read(3, buffer, sizeof(buffer)) == 1);
+ assert_se(buffer[0] == 'z');
+ assert_se(read(0, buffer, sizeof(buffer)) == 6);
+ assert_se(memcmp(buffer, "foobar", 6) == 0);
+
+ assert_se(rearrange_stdio(-EBADF, 1, 2) >= 0);
+ assert_se(write(1, "a", 1) < 0 && errno == ENOSPC);
+ assert_se(write(2, "y", 1) == 1);
+ assert_se(read(3, buffer, sizeof(buffer)) == 1);
+ assert_se(buffer[0] == 'y');
+
+ assert_se(fd_get_path(0, &path) >= 0);
+ assert_se(path_equal(path, "/dev/null"));
+ path = mfree(path);
+
+ _exit(EXIT_SUCCESS);
+ }
+}
+
+TEST(read_nr_open) {
+ log_info("nr-open: %i", read_nr_open());
+}
+
+static size_t validate_fds(
+ bool opened,
+ const int *fds,
+ size_t n_fds) {
+
+ size_t c = 0;
+
+ /* Validates that fds in the specified array are one of the following three:
+ *
+ * 1. < 0 (test is skipped) or
+ * 2. opened (if 'opened' param is true) or
+ * 3. closed (if 'opened' param is false)
+ */
+
+ for (size_t i = 0; i < n_fds; i++) {
+ if (fds[i] < 0)
+ continue;
+
+ if (opened)
+ assert_se(fcntl(fds[i], F_GETFD) >= 0);
+ else
+ assert_se(fcntl(fds[i], F_GETFD) < 0 && errno == EBADF);
+
+ c++;
+ }
+
+ return c; /* Return number of fds >= 0 in the array */
+}
+
+static void test_close_all_fds_inner(void) {
+ _cleanup_free_ int *fds = NULL, *keep = NULL;
+ size_t n_fds, n_keep;
+ int max_fd;
+
+ log_info("/* %s */", __func__);
+
+ rlimit_nofile_bump(-1);
+
+ max_fd = get_max_fd();
+ assert_se(max_fd > 10);
+
+ if (max_fd > 7000) {
+ /* If the worst fallback is activated we need to iterate through all possible fds, hence,
+ * let's lower the limit a small bit, so that we don't run for too long. Yes, this undoes the
+ * rlimit_nofile_bump() call above partially. */
+
+ (void) setrlimit_closest(RLIMIT_NOFILE, &(struct rlimit) { 7000, 7000 });
+ max_fd = 7000;
+ }
+
+ /* Try to use 5000 fds, but when we can't bump the rlimit to make that happen use the whole limit minus 10 */
+ n_fds = MIN(((size_t) max_fd & ~1U) - 10U, 5000U);
+ assert_se((n_fds & 1U) == 0U); /* make sure even number of fds */
+
+ /* Allocate the determined number of fds, always two at a time */
+ assert_se(fds = new(int, n_fds));
+ for (size_t i = 0; i < n_fds; i += 2)
+ assert_se(pipe2(fds + i, O_CLOEXEC) >= 0);
+
+ /* Validate this worked */
+ assert_se(validate_fds(true, fds, n_fds) == n_fds);
+
+ /* Randomized number of fds to keep, but at most every second */
+ n_keep = (random_u64() % (n_fds / 2));
+
+ /* Now randomly select a number of fds from the array above to keep */
+ assert_se(keep = new(int, n_keep));
+ for (size_t k = 0; k < n_keep; k++) {
+ for (;;) {
+ size_t p;
+
+ p = random_u64() % n_fds;
+ if (fds[p] >= 0) {
+ keep[k] = TAKE_FD(fds[p]);
+ break;
+ }
+ }
+ }
+
+ /* Check that all fds from both arrays are still open, and test how many in each are >= 0 */
+ assert_se(validate_fds(true, fds, n_fds) == n_fds - n_keep);
+ assert_se(validate_fds(true, keep, n_keep) == n_keep);
+
+ /* Close logging fd first, so that we don't confuse it by closing its fd */
+ log_close();
+ log_set_open_when_needed(true);
+ log_settle_target();
+
+ /* Close all but the ones to keep */
+ assert_se(close_all_fds(keep, n_keep) >= 0);
+
+ assert_se(validate_fds(false, fds, n_fds) == n_fds - n_keep);
+ assert_se(validate_fds(true, keep, n_keep) == n_keep);
+
+ /* Close everything else too! */
+ assert_se(close_all_fds(NULL, 0) >= 0);
+
+ assert_se(validate_fds(false, fds, n_fds) == n_fds - n_keep);
+ assert_se(validate_fds(false, keep, n_keep) == n_keep);
+
+ log_set_open_when_needed(false);
+ log_open();
+}
+
+static int seccomp_prohibit_close_range(void) {
+#if HAVE_SECCOMP && defined(__SNR_close_range)
+ _cleanup_(seccomp_releasep) scmp_filter_ctx seccomp = NULL;
+ int r;
+
+ r = seccomp_init_for_arch(&seccomp, SCMP_ARCH_NATIVE, SCMP_ACT_ALLOW);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to acquire seccomp context, ignoring: %m");
+
+ r = seccomp_rule_add_exact(
+ seccomp,
+ SCMP_ACT_ERRNO(EPERM),
+ SCMP_SYS(close_range),
+ 0);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to add close_range() rule, ignoring: %m");
+
+ r = seccomp_load(seccomp);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to apply close_range() restrictions, ignoring: %m");
+
+ return 0;
+#else
+ return log_warning_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Seccomp support or close_range() syscall definition not available.");
+#endif
+}
+
+TEST(close_all_fds) {
+ int r;
+
+ /* Runs the test four times. Once as is. Once with close_range() syscall blocked via seccomp, once
+ * with /proc/ overmounted, and once with the combination of both. This should trigger all fallbacks
+ * in the close_range_all() function. */
+
+ r = safe_fork("(caf-plain)", FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT, NULL);
+ if (r == 0) {
+ test_close_all_fds_inner();
+ _exit(EXIT_SUCCESS);
+ }
+ assert_se(r >= 0);
+
+ if (geteuid() != 0)
+ return (void) log_tests_skipped("Lacking privileges for test with close_range() blocked and /proc/ overmounted");
+
+ r = safe_fork("(caf-noproc)", FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT|FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE, NULL);
+ if (r == 0) {
+ r = mount_nofollow_verbose(LOG_WARNING, "tmpfs", "/proc", "tmpfs", 0, NULL);
+ if (r < 0)
+ log_notice("Overmounting /proc/ didn't work, skipping close_all_fds() with masked /proc/.");
+ else
+ test_close_all_fds_inner();
+ _exit(EXIT_SUCCESS);
+ }
+ assert_se(r >= 0);
+
+ if (!is_seccomp_available())
+ return (void) log_tests_skipped("Seccomp not available");
+
+ r = safe_fork("(caf-seccomp)", FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT, NULL);
+ if (r == 0) {
+ r = seccomp_prohibit_close_range();
+ if (r < 0)
+ log_notice("Applying seccomp filter didn't work, skipping close_all_fds() test with masked close_range().");
+ else
+ test_close_all_fds_inner();
+
+ _exit(EXIT_SUCCESS);
+ }
+ assert_se(r >= 0);
+
+ r = safe_fork("(caf-scnp)", FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT|FORK_NEW_MOUNTNS|FORK_MOUNTNS_SLAVE, NULL);
+ if (r == 0) {
+ r = seccomp_prohibit_close_range();
+ if (r < 0)
+ log_notice("Applying seccomp filter didn't work, skipping close_all_fds() test with masked close_range().");
+ else {
+ r = mount_nofollow_verbose(LOG_WARNING, "tmpfs", "/proc", "tmpfs", 0, NULL);
+ if (r < 0)
+ log_notice("Overmounting /proc/ didn't work, skipping close_all_fds() with masked /proc/.");
+ else
+ test_close_all_fds_inner();
+ }
+
+ test_close_all_fds_inner();
+ _exit(EXIT_SUCCESS);
+ }
+ assert_se(r >= 0);
+}
+
+TEST(format_proc_fd_path) {
+ assert_se(streq_ptr(FORMAT_PROC_FD_PATH(0), "/proc/self/fd/0"));
+ assert_se(streq_ptr(FORMAT_PROC_FD_PATH(1), "/proc/self/fd/1"));
+ assert_se(streq_ptr(FORMAT_PROC_FD_PATH(2), "/proc/self/fd/2"));
+ assert_se(streq_ptr(FORMAT_PROC_FD_PATH(3), "/proc/self/fd/3"));
+ assert_se(streq_ptr(FORMAT_PROC_FD_PATH(2147483647), "/proc/self/fd/2147483647"));
+}
+
+TEST(fd_reopen) {
+ _cleanup_close_ int fd1 = -EBADF, fd2 = -EBADF;
+ struct stat st1, st2;
+ int fl;
+
+ /* Test this with a directory */
+ fd1 = open("/proc", O_DIRECTORY|O_PATH|O_CLOEXEC);
+ assert_se(fd1 >= 0);
+
+ assert_se(fstat(fd1, &st1) >= 0);
+ assert_se(S_ISDIR(st1.st_mode));
+
+ fl = fcntl(fd1, F_GETFL);
+ assert_se(fl >= 0);
+ assert_se(FLAGS_SET(fl, O_DIRECTORY));
+ assert_se(FLAGS_SET(fl, O_PATH));
+
+ /* fd_reopen() with O_NOFOLLOW will systematically fail, since it is implemented via a symlink in /proc/self/fd/ */
+ assert_se(fd_reopen(fd1, O_RDONLY|O_CLOEXEC|O_NOFOLLOW) == -ELOOP);
+ assert_se(fd_reopen(fd1, O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW) == -ELOOP);
+
+ fd2 = fd_reopen(fd1, O_RDONLY|O_DIRECTORY|O_CLOEXEC); /* drop the O_PATH */
+ assert_se(fd2 >= 0);
+
+ assert_se(fstat(fd2, &st2) >= 0);
+ assert_se(S_ISDIR(st2.st_mode));
+ assert_se(stat_inode_same(&st1, &st2));
+
+ fl = fcntl(fd2, F_GETFL);
+ assert_se(fl >= 0);
+ assert_se(FLAGS_SET(fl, O_DIRECTORY));
+ assert_se(!FLAGS_SET(fl, O_PATH));
+
+ safe_close(fd1);
+
+ fd1 = fd_reopen(fd2, O_DIRECTORY|O_PATH|O_CLOEXEC); /* reacquire the O_PATH */
+ assert_se(fd1 >= 0);
+
+ assert_se(fstat(fd1, &st1) >= 0);
+ assert_se(S_ISDIR(st1.st_mode));
+ assert_se(stat_inode_same(&st1, &st2));
+
+ fl = fcntl(fd1, F_GETFL);
+ assert_se(fl >= 0);
+ assert_se(FLAGS_SET(fl, O_DIRECTORY));
+ assert_se(FLAGS_SET(fl, O_PATH));
+
+ safe_close(fd1);
+
+ /* And now, test this with a file. */
+ fd1 = open("/proc/version", O_PATH|O_CLOEXEC);
+ assert_se(fd1 >= 0);
+
+ assert_se(fstat(fd1, &st1) >= 0);
+ assert_se(S_ISREG(st1.st_mode));
+
+ fl = fcntl(fd1, F_GETFL);
+ assert_se(fl >= 0);
+ assert_se(!FLAGS_SET(fl, O_DIRECTORY));
+ assert_se(FLAGS_SET(fl, O_PATH));
+
+ assert_se(fd_reopen(fd1, O_RDONLY|O_DIRECTORY|O_CLOEXEC) == -ENOTDIR);
+ fd2 = fd_reopen(fd1, O_RDONLY|O_CLOEXEC); /* drop the O_PATH */
+ assert_se(fd2 >= 0);
+
+ assert_se(fstat(fd2, &st2) >= 0);
+ assert_se(S_ISREG(st2.st_mode));
+ assert_se(stat_inode_same(&st1, &st2));
+
+ fl = fcntl(fd2, F_GETFL);
+ assert_se(fl >= 0);
+ assert_se(!FLAGS_SET(fl, O_DIRECTORY));
+ assert_se(!FLAGS_SET(fl, O_PATH));
+
+ safe_close(fd1);
+
+ assert_se(fd_reopen(fd2, O_DIRECTORY|O_PATH|O_CLOEXEC) == -ENOTDIR);
+ fd1 = fd_reopen(fd2, O_PATH|O_CLOEXEC); /* reacquire the O_PATH */
+ assert_se(fd1 >= 0);
+
+ assert_se(fstat(fd1, &st1) >= 0);
+ assert_se(S_ISREG(st1.st_mode));
+ assert_se(stat_inode_same(&st1, &st2));
+
+ fl = fcntl(fd1, F_GETFL);
+ assert_se(fl >= 0);
+ assert_se(!FLAGS_SET(fl, O_DIRECTORY));
+ assert_se(FLAGS_SET(fl, O_PATH));
+
+ /* Also check the right error is generated if the fd is already closed */
+ safe_close(fd1);
+ assert_se(fd_reopen(fd1, O_RDONLY|O_CLOEXEC) == -EBADF);
+ fd1 = -EBADF;
+
+ /* Validate what happens if we reopen a symlink */
+ fd1 = open("/proc/self", O_PATH|O_CLOEXEC|O_NOFOLLOW);
+ assert_se(fd1 >= 0);
+ assert_se(fstat(fd1, &st1) >= 0);
+ assert_se(S_ISLNK(st1.st_mode));
+
+ fd2 = fd_reopen(fd1, O_PATH|O_CLOEXEC);
+ assert_se(fd2 >= 0);
+ assert_se(fstat(fd2, &st2) >= 0);
+ assert_se(S_ISLNK(st2.st_mode));
+ assert_se(stat_inode_same(&st1, &st2));
+ fd2 = safe_close(fd2);
+
+ /* So here's the thing: if we have an O_PATH fd to a symlink, we *cannot* convert it to a regular fd
+ * with that. i.e. you cannot have the VFS follow a symlink pinned via an O_PATH fd. */
+ assert_se(fd_reopen(fd1, O_RDONLY|O_CLOEXEC) == -ELOOP);
+}
+
+TEST(fd_reopen_condition) {
+ _cleanup_close_ int fd1 = -EBADF, fd3 = -EBADF;
+ int fd2, fl;
+
+ /* Open without O_PATH */
+ fd1 = open("/usr/", O_RDONLY|O_DIRECTORY|O_CLOEXEC);
+ assert_se(fd1 >= 0);
+
+ fl = fcntl(fd1, F_GETFL);
+ assert_se(FLAGS_SET(fl, O_DIRECTORY));
+ assert_se(!FLAGS_SET(fl, O_PATH));
+
+ fd2 = fd_reopen_condition(fd1, O_DIRECTORY, O_DIRECTORY|O_PATH, &fd3);
+ assert_se(fd2 == fd1);
+ assert_se(fd3 < 0);
+
+ /* Switch on O_PATH */
+ fd2 = fd_reopen_condition(fd1, O_DIRECTORY|O_PATH, O_DIRECTORY|O_PATH, &fd3);
+ assert_se(fd2 != fd1);
+ assert_se(fd3 == fd2);
+
+ fl = fcntl(fd2, F_GETFL);
+ assert_se(FLAGS_SET(fl, O_DIRECTORY));
+ assert_se(FLAGS_SET(fl, O_PATH));
+
+ close_and_replace(fd1, fd3);
+
+ fd2 = fd_reopen_condition(fd1, O_DIRECTORY|O_PATH, O_DIRECTORY|O_PATH, &fd3);
+ assert_se(fd2 == fd1);
+ assert_se(fd3 < 0);
+
+ /* Switch off O_PATH again */
+ fd2 = fd_reopen_condition(fd1, O_DIRECTORY, O_DIRECTORY|O_PATH, &fd3);
+ assert_se(fd2 != fd1);
+ assert_se(fd3 == fd2);
+
+ fl = fcntl(fd2, F_GETFL);
+ assert_se(FLAGS_SET(fl, O_DIRECTORY));
+ assert_se(!FLAGS_SET(fl, O_PATH));
+
+ close_and_replace(fd1, fd3);
+
+ fd2 = fd_reopen_condition(fd1, O_DIRECTORY, O_DIRECTORY|O_PATH, &fd3);
+ assert_se(fd2 == fd1);
+ assert_se(fd3 < 0);
+}
+
+TEST(take_fd) {
+ _cleanup_close_ int fd1 = -EBADF, fd2 = -EBADF;
+ int array[2] = EBADF_PAIR, i = 0;
+
+ assert_se(fd1 == -EBADF);
+ assert_se(fd2 == -EBADF);
+
+ fd1 = eventfd(0, EFD_CLOEXEC);
+ assert_se(fd1 >= 0);
+
+ fd2 = TAKE_FD(fd1);
+ assert_se(fd1 == -EBADF);
+ assert_se(fd2 >= 0);
+
+ assert_se(array[0] == -EBADF);
+ assert_se(array[1] == -EBADF);
+
+ array[0] = TAKE_FD(fd2);
+ assert_se(fd1 == -EBADF);
+ assert_se(fd2 == -EBADF);
+ assert_se(array[0] >= 0);
+ assert_se(array[1] == -EBADF);
+
+ array[1] = TAKE_FD(array[i]);
+ assert_se(array[0] == -EBADF);
+ assert_se(array[1] >= 0);
+
+ i = 1 - i;
+ array[0] = TAKE_FD(*(array + i));
+ assert_se(array[0] >= 0);
+ assert_se(array[1] == -EBADF);
+
+ i = 1 - i;
+ fd1 = TAKE_FD(array[i]);
+ assert_se(fd1 >= 0);
+ assert_se(array[0] == -EBADF);
+ assert_se(array[1] == -EBADF);
+}
+
+TEST(dir_fd_is_root) {
+ _cleanup_close_ int fd = -EBADF;
+ int r;
+
+ assert_se(dir_fd_is_root_or_cwd(AT_FDCWD) > 0);
+
+ assert_se((fd = open("/", O_CLOEXEC|O_PATH|O_DIRECTORY|O_NOFOLLOW)) >= 0);
+ assert_se(dir_fd_is_root(fd) > 0);
+ assert_se(dir_fd_is_root_or_cwd(fd) > 0);
+
+ fd = safe_close(fd);
+
+ assert_se((fd = open("/usr", O_CLOEXEC|O_PATH|O_DIRECTORY|O_NOFOLLOW)) >= 0);
+ assert_se(dir_fd_is_root(fd) == 0);
+ assert_se(dir_fd_is_root_or_cwd(fd) == 0);
+
+ r = detach_mount_namespace();
+ if (r < 0)
+ return (void) log_tests_skipped_errno(r, "Failed to detach mount namespace");
+
+ _cleanup_(rm_rf_physical_and_freep) char *tmp = NULL;
+ _cleanup_free_ char *x = NULL, *y = NULL;
+
+ assert_se(mkdtemp_malloc("/tmp/test-mkdir-XXXXXX", &tmp) >= 0);
+ assert_se(x = path_join(tmp, "x"));
+ assert_se(y = path_join(tmp, "x/y"));
+ assert_se(mkdir_p(y, 0755) >= 0);
+ assert_se(mount_nofollow_verbose(LOG_DEBUG, x, y, NULL, MS_BIND, NULL) >= 0);
+
+ fd = safe_close(fd);
+
+ assert_se((fd = open(tmp, O_CLOEXEC|O_PATH|O_DIRECTORY|O_NOFOLLOW)) >= 0);
+ assert_se(dir_fd_is_root(fd) == 0);
+ assert_se(dir_fd_is_root_or_cwd(fd) == 0);
+
+ fd = safe_close(fd);
+
+ assert_se((fd = open(x, O_CLOEXEC|O_PATH|O_DIRECTORY|O_NOFOLLOW)) >= 0);
+ assert_se(dir_fd_is_root(fd) == 0);
+ assert_se(dir_fd_is_root_or_cwd(fd) == 0);
+
+ fd = safe_close(fd);
+
+ assert_se((fd = open(y, O_CLOEXEC|O_PATH|O_DIRECTORY|O_NOFOLLOW)) >= 0);
+ assert_se(dir_fd_is_root(fd) == 0);
+ assert_se(dir_fd_is_root_or_cwd(fd) == 0);
+}
+
+TEST(fd_get_path) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ _cleanup_close_ int tfd = -EBADF, fd = -EBADF;
+ _cleanup_free_ char *p = NULL, *q = NULL, *saved_cwd = NULL;
+
+ tfd = mkdtemp_open(NULL, O_PATH, &t);
+ assert_se(tfd >= 0);
+ assert_se(fd_get_path(tfd, &p) >= 0);
+ assert_se(streq(p, t));
+
+ p = mfree(p);
+
+ assert_se(safe_getcwd(&saved_cwd) >= 0);
+ assert_se(chdir(t) >= 0);
+
+ assert_se(fd_get_path(AT_FDCWD, &p) >= 0);
+ assert_se(streq(p, t));
+
+ p = mfree(p);
+
+ assert_se(q = path_join(t, "regular"));
+ assert_se(touch(q) >= 0);
+ assert_se(mkdirat_parents(tfd, "subdir/symlink", 0755) >= 0);
+ assert_se(symlinkat("../regular", tfd, "subdir/symlink") >= 0);
+ assert_se(symlinkat("subdir", tfd, "symdir") >= 0);
+
+ fd = openat(tfd, "regular", O_CLOEXEC|O_PATH);
+ assert_se(fd >= 0);
+ assert_se(fd_get_path(fd, &p) >= 0);
+ assert_se(streq(p, q));
+
+ p = mfree(p);
+ fd = safe_close(fd);
+
+ fd = openat(AT_FDCWD, "regular", O_CLOEXEC|O_PATH);
+ assert_se(fd >= 0);
+ assert_se(fd_get_path(fd, &p) >= 0);
+ assert_se(streq(p, q));
+
+ p = mfree(p);
+ fd = safe_close(fd);
+
+ fd = openat(tfd, "subdir/symlink", O_CLOEXEC|O_PATH);
+ assert_se(fd >= 0);
+ assert_se(fd_verify_regular(fd) >= 0);
+ assert_se(fd_get_path(fd, &p) >= 0);
+ assert_se(streq(p, q));
+
+ p = mfree(p);
+ fd = safe_close(fd);
+
+ fd = openat(AT_FDCWD, "subdir/symlink", O_CLOEXEC|O_PATH);
+ assert_se(fd >= 0);
+ assert_se(fd_verify_regular(fd) >= 0);
+ assert_se(fd_get_path(fd, &p) >= 0);
+ assert_se(streq(p, q));
+
+ p = mfree(p);
+ fd = safe_close(fd);
+
+ fd = openat(tfd, "symdir//./symlink", O_CLOEXEC|O_PATH);
+ assert_se(fd >= 0);
+ assert_se(fd_verify_regular(fd) >= 0);
+ assert_se(fd_get_path(fd, &p) >= 0);
+ assert_se(streq(p, q));
+
+ p = mfree(p);
+ fd = safe_close(fd);
+
+ fd = openat(AT_FDCWD, "symdir//./symlink", O_CLOEXEC|O_PATH);
+ assert_se(fd >= 0);
+ assert_se(fd_verify_regular(fd) >= 0);
+ assert_se(fd_get_path(fd, &p) >= 0);
+ assert_se(streq(p, q));
+
+ p = mfree(p);
+ q = mfree(q);
+ fd = safe_close(fd);
+
+ assert_se(q = path_join(t, "subdir/symlink"));
+ fd = openat(tfd, "subdir/symlink", O_CLOEXEC|O_PATH|O_NOFOLLOW);
+ assert_se(fd >= 0);
+ assert_se(fd_verify_regular(fd) == -ELOOP);
+ assert_se(fd_get_path(fd, &p) >= 0);
+ assert_se(streq(p, q));
+
+ p = mfree(p);
+ fd = safe_close(fd);
+
+ fd = openat(AT_FDCWD, "subdir/symlink", O_CLOEXEC|O_PATH|O_NOFOLLOW);
+ assert_se(fd >= 0);
+ assert_se(fd_verify_regular(fd) == -ELOOP);
+ assert_se(fd_get_path(fd, &p) >= 0);
+ assert_se(streq(p, q));
+
+ p = mfree(p);
+ fd = safe_close(fd);
+
+ fd = openat(tfd, "symdir//./symlink", O_CLOEXEC|O_PATH|O_NOFOLLOW);
+ assert_se(fd >= 0);
+ assert_se(fd_verify_regular(fd) == -ELOOP);
+ assert_se(fd_get_path(fd, &p) >= 0);
+ assert_se(streq(p, q));
+
+ p = mfree(p);
+ fd = safe_close(fd);
+
+ fd = openat(AT_FDCWD, "symdir//./symlink", O_CLOEXEC|O_PATH|O_NOFOLLOW);
+ assert_se(fd >= 0);
+ assert_se(fd_verify_regular(fd) == -ELOOP);
+ assert_se(fd_get_path(fd, &p) >= 0);
+ assert_se(streq(p, q));
+
+ assert_se(chdir(saved_cwd) >= 0);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-fdset.c b/src/test/test-fdset.c
new file mode 100644
index 0000000..8f00e59
--- /dev/null
+++ b/src/test/test-fdset.c
@@ -0,0 +1,212 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "fd-util.h"
+#include "fdset.h"
+#include "fs-util.h"
+#include "macro.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+TEST(fdset_new_fill) {
+ _cleanup_fdset_free_ FDSet *fdset = NULL;
+ int fd = -EBADF, flags;
+
+ log_close();
+ log_set_open_when_needed(true);
+
+ fd = open("/dev/null", O_CLOEXEC|O_RDONLY);
+ assert_se(fd >= 0);
+
+ assert_se(fdset_new_fill(/* filter_cloexec= */ -1, &fdset) >= 0);
+ assert_se(fdset_contains(fdset, fd));
+ fdset = fdset_free(fdset);
+ assert_se(fcntl(fd, F_GETFD) < 0);
+ assert_se(errno == EBADF);
+
+ fd = open("/dev/null", O_CLOEXEC|O_RDONLY);
+ assert_se(fd >= 0);
+
+ assert_se(fdset_new_fill(/* filter_cloexec= */ 0, &fdset) >= 0);
+ assert_se(!fdset_contains(fdset, fd));
+ fdset = fdset_free(fdset);
+ assert_se(fcntl(fd, F_GETFD) >= 0);
+
+ assert_se(fdset_new_fill(/* filter_cloexec= */ 1, &fdset) >= 0);
+ assert_se(fdset_contains(fdset, fd));
+ fdset = fdset_free(fdset);
+ assert_se(fcntl(fd, F_GETFD) < 0);
+ assert_se(errno == EBADF);
+
+ fd = open("/dev/null", O_RDONLY);
+ assert_se(fd >= 0);
+
+ assert_se(fdset_new_fill(/* filter_cloexec= */ 1, &fdset) >= 0);
+ assert_se(!fdset_contains(fdset, fd));
+ fdset = fdset_free(fdset);
+ assert_se(fcntl(fd, F_GETFD) >= 0);
+
+ assert_se(fdset_new_fill(/* filter_cloexec= */ 0, &fdset) >= 0);
+ assert_se(fdset_contains(fdset, fd));
+ flags = fcntl(fd, F_GETFD);
+ assert_se(flags >= 0);
+ assert_se(FLAGS_SET(flags, FD_CLOEXEC));
+ fdset = fdset_free(fdset);
+ assert_se(fcntl(fd, F_GETFD) < 0);
+ assert_se(errno == EBADF);
+
+ log_open();
+}
+
+TEST(fdset_put_dup) {
+ _cleanup_close_ int fd = -EBADF;
+ int copyfd = -EBADF;
+ _cleanup_fdset_free_ FDSet *fdset = NULL;
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-fdset_put_dup.XXXXXX";
+
+ fd = mkostemp_safe(name);
+ assert_se(fd >= 0);
+
+ fdset = fdset_new();
+ assert_se(fdset);
+ copyfd = fdset_put_dup(fdset, fd);
+ assert_se(copyfd >= 0 && copyfd != fd);
+ assert_se(fdset_contains(fdset, copyfd));
+ assert_se(!fdset_contains(fdset, fd));
+}
+
+TEST(fdset_cloexec) {
+ int fd = -EBADF;
+ _cleanup_fdset_free_ FDSet *fdset = NULL;
+ int flags = -1;
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-fdset_cloexec.XXXXXX";
+
+ fd = mkostemp_safe(name);
+ assert_se(fd >= 0);
+
+ fdset = fdset_new();
+ assert_se(fdset);
+ assert_se(fdset_put(fdset, fd));
+
+ assert_se(fdset_cloexec(fdset, false) >= 0);
+ flags = fcntl(fd, F_GETFD);
+ assert_se(flags >= 0);
+ assert_se(!(flags & FD_CLOEXEC));
+
+ assert_se(fdset_cloexec(fdset, true) >= 0);
+ flags = fcntl(fd, F_GETFD);
+ assert_se(flags >= 0);
+ assert_se(flags & FD_CLOEXEC);
+}
+
+TEST(fdset_close_others) {
+ int fd = -EBADF;
+ int copyfd = -EBADF;
+ _cleanup_fdset_free_ FDSet *fdset = NULL;
+ int flags = -1;
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-fdset_close_others.XXXXXX";
+
+ fd = mkostemp_safe(name);
+ assert_se(fd >= 0);
+
+ fdset = fdset_new();
+ assert_se(fdset);
+ copyfd = fdset_put_dup(fdset, fd);
+ assert_se(copyfd >= 0);
+
+ assert_se(fdset_close_others(fdset) >= 0);
+ flags = fcntl(fd, F_GETFD);
+ assert_se(flags < 0);
+ flags = fcntl(copyfd, F_GETFD);
+ assert_se(flags >= 0);
+}
+
+TEST(fdset_remove) {
+ _cleanup_close_ int fd = -EBADF;
+ _cleanup_fdset_free_ FDSet *fdset = NULL;
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-fdset_remove.XXXXXX";
+
+ fd = mkostemp_safe(name);
+ assert_se(fd >= 0);
+
+ fdset = fdset_new();
+ assert_se(fdset);
+ assert_se(fdset_put(fdset, fd) >= 0);
+ assert_se(fdset_remove(fdset, fd) >= 0);
+ assert_se(!fdset_contains(fdset, fd));
+
+ assert_se(fcntl(fd, F_GETFD) >= 0);
+}
+
+TEST(fdset_iterate) {
+ int fd = -EBADF;
+ _cleanup_fdset_free_ FDSet *fdset = NULL;
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-fdset_iterate.XXXXXX";
+ int c = 0;
+ int a;
+
+ fd = mkostemp_safe(name);
+ assert_se(fd >= 0);
+
+ fdset = fdset_new();
+ assert_se(fdset);
+ assert_se(fdset_put(fdset, fd) >= 0);
+ assert_se(fdset_put(fdset, fd) >= 0);
+ assert_se(fdset_put(fdset, fd) >= 0);
+
+ FDSET_FOREACH(a, fdset) {
+ c++;
+ assert_se(a == fd);
+ }
+ assert_se(c == 1);
+}
+
+TEST(fdset_isempty) {
+ int fd;
+ _cleanup_fdset_free_ FDSet *fdset = NULL;
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-fdset_isempty.XXXXXX";
+
+ fd = mkostemp_safe(name);
+ assert_se(fd >= 0);
+
+ fdset = fdset_new();
+ assert_se(fdset);
+
+ assert_se(fdset_isempty(fdset));
+ assert_se(fdset_put(fdset, fd) >= 0);
+ assert_se(!fdset_isempty(fdset));
+}
+
+TEST(fdset_steal_first) {
+ int fd;
+ _cleanup_fdset_free_ FDSet *fdset = NULL;
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-fdset_steal_first.XXXXXX";
+
+ fd = mkostemp_safe(name);
+ assert_se(fd >= 0);
+
+ fdset = fdset_new();
+ assert_se(fdset);
+
+ assert_se(fdset_steal_first(fdset) < 0);
+ assert_se(fdset_put(fdset, fd) >= 0);
+ assert_se(fdset_steal_first(fdset) == fd);
+ assert_se(fdset_steal_first(fdset) < 0);
+ assert_se(fdset_put(fdset, fd) >= 0);
+}
+
+TEST(fdset_new_array) {
+ int fds[] = {10, 11, 12, 13};
+ _cleanup_fdset_free_ FDSet *fdset = NULL;
+
+ assert_se(fdset_new_array(&fdset, fds, 4) >= 0);
+ assert_se(fdset_size(fdset) == 4);
+ assert_se(fdset_contains(fdset, 10));
+ assert_se(fdset_contains(fdset, 11));
+ assert_se(fdset_contains(fdset, 12));
+ assert_se(fdset_contains(fdset, 13));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-fiemap.c b/src/test/test-fiemap.c
new file mode 100644
index 0000000..380638b
--- /dev/null
+++ b/src/test/test-fiemap.c
@@ -0,0 +1,64 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <inttypes.h>
+#include <linux/fiemap.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "errno-util.h"
+#include "fd-util.h"
+#include "log.h"
+#include "hibernate-util.h"
+#include "tests.h"
+
+static int test_fiemap_one(const char *path) {
+ _cleanup_free_ struct fiemap *fiemap = NULL;
+ _cleanup_close_ int fd = -EBADF;
+ int r;
+
+ log_info("/* %s */", __func__);
+
+ fd = open(path, O_RDONLY | O_CLOEXEC | O_NONBLOCK);
+ if (fd < 0)
+ return log_error_errno(errno, "failed to open %s: %m", path);
+ r = read_fiemap(fd, &fiemap);
+ if (r == -EOPNOTSUPP)
+ exit(log_tests_skipped("Not supported"));
+ if (r < 0)
+ return log_error_errno(r, "Unable to read extent map for '%s': %m", path);
+ log_info("extent map information for %s:", path);
+ log_info("\t start: %" PRIu64, (uint64_t) fiemap->fm_start);
+ log_info("\t length: %" PRIu64, (uint64_t) fiemap->fm_length);
+ log_info("\t flags: %" PRIu32, fiemap->fm_flags);
+ log_info("\t number of mapped extents: %" PRIu32, fiemap->fm_mapped_extents);
+ log_info("\t extent count: %" PRIu32, fiemap->fm_extent_count);
+ if (fiemap->fm_extent_count > 0)
+ log_info("\t first extent location: %" PRIu64,
+ (uint64_t) (fiemap->fm_extents[0].fe_physical / page_size()));
+
+ return 0;
+}
+
+TEST_RET(fiemap) {
+ int r = 0;
+
+ assert_se(test_fiemap_one(saved_argv[0]) == 0);
+ for (int i = 1; i < saved_argc; i++) {
+ int k = test_fiemap_one(saved_argv[i]);
+ if (r == 0)
+ r = k;
+ }
+
+ return r;
+}
+
+static int intro(void) {
+ if (getuid() != 0)
+ log_warning("This program is unlikely to work for unprivileged users");
+
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_DEBUG, intro);
diff --git a/src/test/test-fileio.c b/src/test/test-fileio.c
new file mode 100644
index 0000000..ad98a92
--- /dev/null
+++ b/src/test/test-fileio.c
@@ -0,0 +1,1151 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "ctype.h"
+#include "env-file.h"
+#include "env-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "iovec-util.h"
+#include "memfd-util.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "random-util.h"
+#include "rm-rf.h"
+#include "socket-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+TEST(parse_env_file) {
+ _cleanup_(unlink_tempfilep) char
+ t[] = "/tmp/test-fileio-in-XXXXXX",
+ p[] = "/tmp/test-fileio-out-XXXXXX";
+ FILE *f;
+ _cleanup_free_ char *one = NULL, *two = NULL, *three = NULL, *four = NULL, *five = NULL,
+ *six = NULL, *seven = NULL, *eight = NULL, *nine = NULL, *ten = NULL,
+ *eleven = NULL, *twelve = NULL, *thirteen = NULL;
+ _cleanup_strv_free_ char **a = NULL, **b = NULL;
+ unsigned k;
+ int r;
+
+ assert_se(fmkostemp_safe(t, "w", &f) == 0);
+ fputs("one=BAR \n"
+ "# comment\n"
+ " # comment \n"
+ " ; comment \n"
+ " two = bar \n"
+ "invalid line\n"
+ "invalid line #comment\n"
+ "three = \"333\n"
+ "xxxx\"\n"
+ "four = \'44\\\"44\'\n"
+ "five = \"55\\\"55\" \"FIVE\" cinco \n"
+ "six = seis sechs\\\n"
+ " sis\n"
+ "seven=\"sevenval\" #nocomment\n"
+ "eight=eightval #nocomment\n"
+ "export nine=nineval\n"
+ "ten=ignored\n"
+ "ten=ignored\n"
+ "ten=\n"
+ "eleven=\\value\n"
+ "twelve=\"\\value\"\n"
+ "thirteen='\\value'", f);
+
+ fflush(f);
+ fclose(f);
+
+ r = load_env_file(NULL, t, &a);
+ assert_se(r >= 0);
+
+ STRV_FOREACH(i, a)
+ log_info("Got: <%s>", *i);
+
+ assert_se(streq_ptr(a[0], "one=BAR"));
+ assert_se(streq_ptr(a[1], "two=bar"));
+ assert_se(streq_ptr(a[2], "three=333\nxxxx"));
+ assert_se(streq_ptr(a[3], "four=44\\\"44"));
+ assert_se(streq_ptr(a[4], "five=55\"55FIVEcinco"));
+ assert_se(streq_ptr(a[5], "six=seis sechs sis"));
+ assert_se(streq_ptr(a[6], "seven=sevenval#nocomment"));
+ assert_se(streq_ptr(a[7], "eight=eightval #nocomment"));
+ assert_se(streq_ptr(a[8], "export nine=nineval"));
+ assert_se(streq_ptr(a[9], "ten="));
+ assert_se(streq_ptr(a[10], "eleven=value"));
+ assert_se(streq_ptr(a[11], "twelve=\\value"));
+ assert_se(streq_ptr(a[12], "thirteen=\\value"));
+ assert_se(a[13] == NULL);
+
+ strv_env_clean(a);
+
+ k = 0;
+ STRV_FOREACH(i, b) {
+ log_info("Got2: <%s>", *i);
+ assert_se(streq(*i, a[k++]));
+ }
+
+ r = parse_env_file(
+ NULL, t,
+ "one", &one,
+ "two", &two,
+ "three", &three,
+ "four", &four,
+ "five", &five,
+ "six", &six,
+ "seven", &seven,
+ "eight", &eight,
+ "export nine", &nine,
+ "ten", &ten,
+ "eleven", &eleven,
+ "twelve", &twelve,
+ "thirteen", &thirteen);
+ assert_se(r == 0);
+
+ log_info("one=[%s]", strna(one));
+ log_info("two=[%s]", strna(two));
+ log_info("three=[%s]", strna(three));
+ log_info("four=[%s]", strna(four));
+ log_info("five=[%s]", strna(five));
+ log_info("six=[%s]", strna(six));
+ log_info("seven=[%s]", strna(seven));
+ log_info("eight=[%s]", strna(eight));
+ log_info("export nine=[%s]", strna(nine));
+ log_info("ten=[%s]", strna(nine));
+ log_info("eleven=[%s]", strna(eleven));
+ log_info("twelve=[%s]", strna(twelve));
+ log_info("thirteen=[%s]", strna(thirteen));
+
+ assert_se(streq(one, "BAR"));
+ assert_se(streq(two, "bar"));
+ assert_se(streq(three, "333\nxxxx"));
+ assert_se(streq(four, "44\\\"44"));
+ assert_se(streq(five, "55\"55FIVEcinco"));
+ assert_se(streq(six, "seis sechs sis"));
+ assert_se(streq(seven, "sevenval#nocomment"));
+ assert_se(streq(eight, "eightval #nocomment"));
+ assert_se(streq(nine, "nineval"));
+ assert_se(ten == NULL);
+ assert_se(streq(eleven, "value"));
+ assert_se(streq(twelve, "\\value"));
+ assert_se(streq(thirteen, "\\value"));
+
+ {
+ /* prepare a temporary file to write the environment to */
+ _cleanup_close_ int fd = mkostemp_safe(p);
+ assert_se(fd >= 0);
+ }
+
+ r = write_env_file(AT_FDCWD, p, NULL, a);
+ assert_se(r >= 0);
+
+ r = load_env_file(NULL, p, &b);
+ assert_se(r >= 0);
+}
+
+static void test_one_shell_var(const char *file, const char *variable, const char *value) {
+ _cleanup_free_ char *cmd = NULL, *from_shell = NULL;
+ _cleanup_pclose_ FILE *f = NULL;
+ size_t sz;
+
+ assert_se(cmd = strjoin(". ", file, " && /bin/echo -n \"$", variable, "\""));
+ assert_se(f = popen(cmd, "re"));
+ assert_se(read_full_stream(f, &from_shell, &sz) >= 0);
+ assert_se(sz == strlen(value));
+ assert_se(streq(from_shell, value));
+}
+
+TEST(parse_multiline_env_file) {
+ _cleanup_(unlink_tempfilep) char
+ t[] = "/tmp/test-fileio-in-XXXXXX",
+ p[] = "/tmp/test-fileio-out-XXXXXX";
+ FILE *f;
+ _cleanup_strv_free_ char **a = NULL, **b = NULL;
+ int r;
+
+ assert_se(fmkostemp_safe(t, "w", &f) == 0);
+ fputs("one=BAR\\\n"
+ "\\ \\ \\ \\ VAR\\\n"
+ "\\\tGAR\n"
+ "#comment\n"
+ "two=\"bar\\\n"
+ " var\\\n"
+ "\tgar\"\n"
+ "#comment\n"
+ "tri=\"bar \\\n"
+ " var \\\n"
+ "\tgar \"\n", f);
+
+ assert_se(fflush_and_check(f) >= 0);
+ fclose(f);
+
+ test_one_shell_var(t, "one", "BAR VAR\tGAR");
+ test_one_shell_var(t, "two", "bar var\tgar");
+ test_one_shell_var(t, "tri", "bar var \tgar ");
+
+ r = load_env_file(NULL, t, &a);
+ assert_se(r >= 0);
+
+ STRV_FOREACH(i, a)
+ log_info("Got: <%s>", *i);
+
+ assert_se(streq_ptr(a[0], "one=BAR VAR\tGAR"));
+ assert_se(streq_ptr(a[1], "two=bar var\tgar"));
+ assert_se(streq_ptr(a[2], "tri=bar var \tgar "));
+ assert_se(a[3] == NULL);
+
+ {
+ _cleanup_close_ int fd = mkostemp_safe(p);
+ assert_se(fd >= 0);
+ }
+
+ r = write_env_file(AT_FDCWD, p, NULL, a);
+ assert_se(r >= 0);
+
+ r = load_env_file(NULL, p, &b);
+ assert_se(r >= 0);
+}
+
+TEST(merge_env_file) {
+ _cleanup_(unlink_tempfilep) char t[] = "/tmp/test-fileio-XXXXXX";
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_strv_free_ char **a = NULL;
+ int r;
+
+ assert_se(fmkostemp_safe(t, "w", &f) == 0);
+ log_info("/* %s (%s) */", __func__, t);
+
+ r = write_string_stream(f,
+ "one=1 \n"
+ "twelve=${one}2\n"
+ "twentyone=2${one}\n"
+ "one=2\n"
+ "twentytwo=2${one}\n"
+ "xxx_minus_three=$xxx - 3\n"
+ "xxx=0x$one$one$one\n"
+ "yyy=${one:-fallback}\n"
+ "zzz=${one:+replacement}\n"
+ "zzzz=${foobar:-${nothing}}\n"
+ "zzzzz=${nothing:+${nothing}}\n"
+ , WRITE_STRING_FILE_AVOID_NEWLINE);
+ assert_se(r >= 0);
+
+ r = merge_env_file(&a, NULL, t);
+ assert_se(r >= 0);
+ strv_sort(a);
+
+ STRV_FOREACH(i, a)
+ log_info("Got: <%s>", *i);
+
+ assert_se(streq(a[0], "one=2"));
+ assert_se(streq(a[1], "twelve=12"));
+ assert_se(streq(a[2], "twentyone=21"));
+ assert_se(streq(a[3], "twentytwo=22"));
+ assert_se(streq(a[4], "xxx=0x222"));
+ assert_se(streq(a[5], "xxx_minus_three= - 3"));
+ assert_se(streq(a[6], "yyy=2"));
+ assert_se(streq(a[7], "zzz=replacement"));
+ assert_se(streq(a[8], "zzzz="));
+ assert_se(streq(a[9], "zzzzz="));
+ assert_se(a[10] == NULL);
+
+ r = merge_env_file(&a, NULL, t);
+ assert_se(r >= 0);
+ strv_sort(a);
+
+ STRV_FOREACH(i, a)
+ log_info("Got2: <%s>", *i);
+
+ assert_se(streq(a[0], "one=2"));
+ assert_se(streq(a[1], "twelve=12"));
+ assert_se(streq(a[2], "twentyone=21"));
+ assert_se(streq(a[3], "twentytwo=22"));
+ assert_se(streq(a[4], "xxx=0x222"));
+ assert_se(streq(a[5], "xxx_minus_three=0x222 - 3"));
+ assert_se(streq(a[6], "yyy=2"));
+ assert_se(streq(a[7], "zzz=replacement"));
+ assert_se(streq(a[8], "zzzz="));
+ assert_se(streq(a[9], "zzzzz="));
+ assert_se(a[10] == NULL);
+}
+
+TEST(merge_env_file_invalid) {
+ _cleanup_(unlink_tempfilep) char t[] = "/tmp/test-fileio-XXXXXX";
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_strv_free_ char **a = NULL;
+ int r;
+
+ assert_se(fmkostemp_safe(t, "w", &f) == 0);
+ log_info("/* %s (%s) */", __func__, t);
+
+ r = write_string_stream(f,
+ "unset one \n"
+ "unset one= \n"
+ "unset one=1 \n"
+ "one \n"
+ "one = \n"
+ "one two =\n"
+ "\x20two=\n"
+ "#comment=comment\n"
+ ";comment2=comment2\n"
+ "#\n"
+ "\n\n" /* empty line */
+ , WRITE_STRING_FILE_AVOID_NEWLINE);
+ assert_se(r >= 0);
+
+ r = merge_env_file(&a, NULL, t);
+ assert_se(r >= 0);
+
+ STRV_FOREACH(i, a)
+ log_info("Got: <%s>", *i);
+
+ assert_se(strv_isempty(a));
+}
+
+TEST(executable_is_script) {
+ _cleanup_(unlink_tempfilep) char t[] = "/tmp/test-fileio-XXXXXX";
+ _cleanup_fclose_ FILE *f = NULL;
+ char *command;
+ int r;
+
+ assert_se(fmkostemp_safe(t, "w", &f) == 0);
+ fputs("#! /bin/script -a -b \ngoo goo", f);
+ fflush(f);
+
+ r = executable_is_script(t, &command);
+ assert_se(r > 0);
+ assert_se(streq(command, "/bin/script"));
+ free(command);
+
+ r = executable_is_script("/bin/sh", &command);
+ assert_se(r == 0);
+
+ r = executable_is_script("/usr/bin/yum", &command);
+ if (r > 0) {
+ assert_se(startswith(command, "/"));
+ free(command);
+ }
+}
+
+TEST(status_field) {
+ _cleanup_free_ char *p = NULL, *s = NULL, *z = NULL;
+ unsigned long long total = 0, buffers = 0;
+ int r;
+
+ r = get_proc_field("/proc/meminfo", "MemTotal", WHITESPACE, &p);
+ if (r != -ENOENT) {
+ assert_se(r == 0);
+ puts(p);
+ assert_se(safe_atollu(p, &total) == 0);
+ }
+
+ r = get_proc_field("/proc/meminfo", "Buffers", WHITESPACE, &s);
+ if (r != -ENOENT) {
+ assert_se(r == 0);
+ puts(s);
+ assert_se(safe_atollu(s, &buffers) == 0);
+ }
+
+ if (p)
+ assert_se(buffers < total);
+
+ /* Seccomp should be a good test for field full of zeros. */
+ r = get_proc_field("/proc/meminfo", "Seccomp", WHITESPACE, &z);
+ if (r != -ENOENT) {
+ assert_se(r == 0);
+ puts(z);
+ assert_se(safe_atollu(z, &buffers) == 0);
+ }
+}
+
+TEST(capeff) {
+ for (int pid = 0; pid < 2; pid++) {
+ _cleanup_free_ char *capeff = NULL;
+ int r, p;
+
+ r = get_process_capeff(0, &capeff);
+ log_info("capeff: '%s' (r=%d)", capeff, r);
+
+ if (IN_SET(r, -ENOENT, -EPERM))
+ return;
+
+ assert_se(r == 0);
+ assert_se(*capeff);
+ p = capeff[strspn(capeff, HEXDIGITS)];
+ assert_se(!p || isspace(p));
+ }
+}
+
+TEST(read_one_line_file) {
+ _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-fileio-1lf-XXXXXX";
+ int fd;
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *buf, *buf2, *buf3, *buf4, *buf5;
+
+ fd = mkostemp_safe(fn);
+ assert_se(fd >= 0);
+
+ f = fdopen(fd, "we");
+ assert_se(f);
+
+ assert_se(read_one_line_file(fn, &buf) == 0);
+ assert_se(streq_ptr(buf, ""));
+ assert_se(read_one_line_file(fn, &buf2) == 0);
+ assert_se(streq_ptr(buf2, ""));
+
+ assert_se(write_string_stream(f, "x", WRITE_STRING_FILE_AVOID_NEWLINE) >= 0);
+ fflush(f);
+
+ assert_se(read_one_line_file(fn, &buf3) == 1);
+ assert_se(streq_ptr(buf3, "x"));
+
+ assert_se(write_string_stream(f, "\n", WRITE_STRING_FILE_AVOID_NEWLINE) >= 0);
+ fflush(f);
+
+ assert_se(read_one_line_file(fn, &buf4) == 2);
+ assert_se(streq_ptr(buf4, "x"));
+
+ assert_se(write_string_stream(f, "\n", WRITE_STRING_FILE_AVOID_NEWLINE) >= 0);
+ fflush(f);
+
+ assert_se(read_one_line_file(fn, &buf5) == 2);
+ assert_se(streq_ptr(buf5, "x"));
+}
+
+TEST(write_string_stream) {
+ _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-write_string_stream-XXXXXX";
+ _cleanup_fclose_ FILE *f = NULL;
+ int fd;
+ char buf[64];
+
+ fd = mkostemp_safe(fn);
+ assert_se(fd >= 0);
+
+ f = fdopen(fd, "r");
+ assert_se(f);
+ assert_se(write_string_stream(f, "boohoo", 0) < 0);
+ f = safe_fclose(f);
+
+ f = fopen(fn, "r+");
+ assert_se(f);
+
+ assert_se(write_string_stream(f, "boohoo", 0) == 0);
+ rewind(f);
+
+ assert_se(fgets(buf, sizeof(buf), f));
+ assert_se(streq(buf, "boohoo\n"));
+ f = safe_fclose(f);
+
+ f = fopen(fn, "w+");
+ assert_se(f);
+
+ assert_se(write_string_stream(f, "boohoo", WRITE_STRING_FILE_AVOID_NEWLINE) == 0);
+ rewind(f);
+
+ assert_se(fgets(buf, sizeof(buf), f));
+ printf(">%s<", buf);
+ assert_se(streq(buf, "boohoo"));
+}
+
+TEST(write_string_file) {
+ _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-write_string_file-XXXXXX";
+ char buf[64] = {};
+ _cleanup_close_ int fd = -EBADF;
+
+ fd = mkostemp_safe(fn);
+ assert_se(fd >= 0);
+
+ assert_se(write_string_file(fn, "boohoo", WRITE_STRING_FILE_CREATE) == 0);
+
+ assert_se(read(fd, buf, sizeof(buf)) == 7);
+ assert_se(streq(buf, "boohoo\n"));
+}
+
+TEST(write_string_file_no_create) {
+ _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-write_string_file_no_create-XXXXXX";
+ _cleanup_close_ int fd = -EBADF;
+ char buf[64] = {};
+
+ fd = mkostemp_safe(fn);
+ assert_se(fd >= 0);
+
+ assert_se(write_string_file("/a/file/which/does/not/exists/i/guess", "boohoo", 0) < 0);
+ assert_se(write_string_file(fn, "boohoo", 0) == 0);
+
+ assert_se(read(fd, buf, sizeof buf) == (ssize_t) strlen("boohoo\n"));
+ assert_se(streq(buf, "boohoo\n"));
+}
+
+TEST(write_string_file_verify) {
+ _cleanup_free_ char *buf = NULL, *buf2 = NULL;
+ int r;
+
+ r = read_one_line_file("/proc/version", &buf);
+ if (ERRNO_IS_NEG_PRIVILEGE(r))
+ return;
+ assert_se(r >= 0);
+ assert_se(buf2 = strjoin(buf, "\n"));
+
+ r = write_string_file("/proc/version", buf, 0);
+ assert_se(IN_SET(r, -EACCES, -EIO));
+ r = write_string_file("/proc/version", buf2, 0);
+ assert_se(IN_SET(r, -EACCES, -EIO));
+
+ assert_se(write_string_file("/proc/version", buf, WRITE_STRING_FILE_VERIFY_ON_FAILURE) == 0);
+ assert_se(write_string_file("/proc/version", buf2, WRITE_STRING_FILE_VERIFY_ON_FAILURE) == 0);
+
+ r = write_string_file("/proc/version", buf, WRITE_STRING_FILE_VERIFY_ON_FAILURE|WRITE_STRING_FILE_AVOID_NEWLINE);
+ assert_se(IN_SET(r, -EACCES, -EIO));
+ assert_se(write_string_file("/proc/version", buf2, WRITE_STRING_FILE_VERIFY_ON_FAILURE|WRITE_STRING_FILE_AVOID_NEWLINE) == 0);
+}
+
+static void check_file_pairs_one(char **l) {
+ assert_se(l);
+ assert_se(strv_length(l) == 14);
+
+ STRV_FOREACH_PAIR(k, v, l) {
+ assert_se(STR_IN_SET(*k, "NAME", "ID", "PRETTY_NAME", "ANSI_COLOR", "HOME_URL", "SUPPORT_URL", "BUG_REPORT_URL"));
+ printf("%s=%s\n", *k, *v);
+ assert_se(!streq(*k, "NAME") || streq(*v, "Arch Linux"));
+ assert_se(!streq(*k, "ID") || streq(*v, "arch"));
+ assert_se(!streq(*k, "PRETTY_NAME") || streq(*v, "Arch Linux"));
+ assert_se(!streq(*k, "ANSI_COLOR") || streq(*v, "0;36"));
+ assert_se(!streq(*k, "HOME_URL") || streq(*v, "https://www.archlinux.org/"));
+ assert_se(!streq(*k, "SUPPORT_URL") || streq(*v, "https://bbs.archlinux.org/"));
+ assert_se(!streq(*k, "BUG_REPORT_URL") || streq(*v, "https://bugs.archlinux.org/"));
+ }
+}
+
+TEST(load_env_file_pairs) {
+ _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-load_env_file_pairs-XXXXXX";
+ int fd, r;
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_strv_free_ char **l = NULL;
+
+ fd = mkostemp_safe(fn);
+ assert_se(fd >= 0);
+
+ r = write_string_file(fn,
+ "NAME=\"Arch Linux\"\n"
+ "ID=arch\n"
+ "PRETTY_NAME=\"Arch Linux\"\n"
+ "ANSI_COLOR=\"0;36\"\n"
+ "HOME_URL=\"https://www.archlinux.org/\"\n"
+ "SUPPORT_URL=\"https://bbs.archlinux.org/\"\n"
+ "BUG_REPORT_URL=\"https://bugs.archlinux.org/\"\n",
+ WRITE_STRING_FILE_CREATE);
+ assert_se(r == 0);
+
+ r = load_env_file_pairs_fd(fd, fn, &l);
+ assert_se(r >= 0);
+ check_file_pairs_one(l);
+ l = strv_free(l);
+
+ f = fdopen(fd, "r");
+ assert_se(f);
+
+ r = load_env_file_pairs(f, fn, &l);
+ assert_se(r >= 0);
+ check_file_pairs_one(l);
+}
+
+TEST(search_and_fopen) {
+ static const char* const dirs[] = {
+ "/tmp/foo/bar",
+ "/tmp",
+ NULL
+ };
+ char name[] = "/tmp/test-search_and_fopen.XXXXXX";
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *p = NULL;
+ _cleanup_close_ int fd = -EBADF;
+ const char *e;
+ int r;
+
+ fd = mkostemp_safe(name);
+ assert_se(fd >= 0);
+ fd = safe_close(fd);
+
+ r = search_and_fopen(basename(name), "re", NULL, (const char**) dirs, &f, &p);
+ assert_se(r >= 0);
+ assert_se(e = path_startswith(p, "/tmp/"));
+ assert_se(streq(basename(name), e));
+ f = safe_fclose(f);
+ p = mfree(p);
+
+ r = search_and_fopen(basename(name), NULL, NULL, (const char**) dirs, NULL, &p);
+ assert_se(r >= 0);
+ assert_se(e = path_startswith(p, "/tmp/"));
+ assert_se(streq(basename(name), e));
+ p = mfree(p);
+
+ r = search_and_fopen(name, "re", NULL, (const char**) dirs, &f, &p);
+ assert_se(r >= 0);
+ assert_se(path_equal(name, p));
+ f = safe_fclose(f);
+ p = mfree(p);
+
+ r = search_and_fopen(name, NULL, NULL, (const char**) dirs, NULL, &p);
+ assert_se(r >= 0);
+ assert_se(path_equal(name, p));
+ p = mfree(p);
+
+ r = search_and_fopen(basename(name), "re", "/", (const char**) dirs, &f, &p);
+ assert_se(r >= 0);
+ assert_se(e = path_startswith(p, "/tmp/"));
+ assert_se(streq(basename(name), e));
+ f = safe_fclose(f);
+ p = mfree(p);
+
+ r = search_and_fopen(basename(name), NULL, "/", (const char**) dirs, NULL, &p);
+ assert_se(r >= 0);
+ assert_se(e = path_startswith(p, "/tmp/"));
+ assert_se(streq(basename(name), e));
+ p = mfree(p);
+
+ r = search_and_fopen("/a/file/which/does/not/exist/i/guess", "re", NULL, (const char**) dirs, &f, &p);
+ assert_se(r == -ENOENT);
+ r = search_and_fopen("/a/file/which/does/not/exist/i/guess", NULL, NULL, (const char**) dirs, NULL, &p);
+ assert_se(r == -ENOENT);
+ r = search_and_fopen("afilewhichdoesnotexistiguess", "re", NULL, (const char**) dirs, &f, &p);
+ assert_se(r == -ENOENT);
+ r = search_and_fopen("afilewhichdoesnotexistiguess", NULL, NULL, (const char**) dirs, NULL, &p);
+ assert_se(r == -ENOENT);
+
+ r = unlink(name);
+ assert_se(r == 0);
+
+ r = search_and_fopen(basename(name), "re", NULL, (const char**) dirs, &f, &p);
+ assert_se(r == -ENOENT);
+ r = search_and_fopen(basename(name), NULL, NULL, (const char**) dirs, NULL, &p);
+ assert_se(r == -ENOENT);
+}
+
+TEST(search_and_fopen_nulstr) {
+ static const char dirs[] =
+ "/tmp/foo/bar\0"
+ "/tmp\0";
+
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-search_and_fopen.XXXXXX";
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *p = NULL;
+ _cleanup_close_ int fd = -EBADF;
+ const char *e;
+ int r;
+
+ fd = mkostemp_safe(name);
+ assert_se(fd >= 0);
+ fd = safe_close(fd);
+
+ r = search_and_fopen_nulstr(basename(name), "re", NULL, dirs, &f, &p);
+ assert_se(r >= 0);
+ assert_se(e = path_startswith(p, "/tmp/"));
+ assert_se(streq(basename(name), e));
+ f = safe_fclose(f);
+ p = mfree(p);
+
+ r = search_and_fopen_nulstr(name, "re", NULL, dirs, &f, &p);
+ assert_se(r >= 0);
+ assert_se(path_equal(name, p));
+ f = safe_fclose(f);
+ p = mfree(p);
+
+ r = search_and_fopen_nulstr("/a/file/which/does/not/exist/i/guess", "re", NULL, dirs, &f, &p);
+ assert_se(r == -ENOENT);
+ r = search_and_fopen_nulstr("afilewhichdoesnotexistiguess", "re", NULL, dirs, &f, &p);
+ assert_se(r == -ENOENT);
+
+ r = unlink(name);
+ assert_se(r == 0);
+
+ r = search_and_fopen_nulstr(basename(name), "re", NULL, dirs, &f, &p);
+ assert_se(r == -ENOENT);
+}
+
+TEST(writing_tmpfile) {
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-systemd_writing_tmpfile.XXXXXX";
+ _cleanup_free_ char *contents = NULL;
+ size_t size;
+ _cleanup_close_ int fd = -EBADF;
+ int r;
+
+ struct iovec iov[] = {
+ IOVEC_MAKE_STRING("abc\n"),
+ IOVEC_MAKE_STRING(ALPHANUMERICAL "\n"),
+ IOVEC_MAKE_STRING(""),
+ };
+
+ fd = mkostemp_safe(name);
+ printf("tmpfile: %s", name);
+
+ r = writev(fd, iov, 3);
+ assert_se(r >= 0);
+
+ r = read_full_file(name, &contents, &size);
+ assert_se(r == 0);
+ printf("contents: %s", contents);
+ assert_se(streq(contents, "abc\n" ALPHANUMERICAL "\n"));
+}
+
+TEST(tempfn) {
+ char *ret = NULL, *p;
+
+ assert_se(tempfn_xxxxxx("/foo/bar/waldo", NULL, &ret) >= 0);
+ assert_se(streq_ptr(ret, "/foo/bar/.#waldoXXXXXX"));
+ free(ret);
+
+ assert_se(tempfn_xxxxxx("/foo/bar/waldo", "[miau]", &ret) >= 0);
+ assert_se(streq_ptr(ret, "/foo/bar/.#[miau]waldoXXXXXX"));
+ free(ret);
+
+ assert_se(tempfn_random("/foo/bar/waldo", NULL, &ret) >= 0);
+ assert_se(p = startswith(ret, "/foo/bar/.#waldo"));
+ assert_se(strlen(p) == 16);
+ assert_se(in_charset(p, "0123456789abcdef"));
+ free(ret);
+
+ assert_se(tempfn_random("/foo/bar/waldo", "[wuff]", &ret) >= 0);
+ assert_se(p = startswith(ret, "/foo/bar/.#[wuff]waldo"));
+ assert_se(strlen(p) == 16);
+ assert_se(in_charset(p, "0123456789abcdef"));
+ free(ret);
+
+ assert_se(tempfn_random_child("/foo/bar/waldo", NULL, &ret) >= 0);
+ assert_se(p = startswith(ret, "/foo/bar/waldo/.#"));
+ assert_se(strlen(p) == 16);
+ assert_se(in_charset(p, "0123456789abcdef"));
+ free(ret);
+
+ assert_se(tempfn_random_child("/foo/bar/waldo", "[kikiriki]", &ret) >= 0);
+ assert_se(p = startswith(ret, "/foo/bar/waldo/.#[kikiriki]"));
+ assert_se(strlen(p) == 16);
+ assert_se(in_charset(p, "0123456789abcdef"));
+ free(ret);
+}
+
+static const char chars[] =
+ "Aąę„”\n루\377";
+
+DISABLE_WARNING_TYPE_LIMITS;
+
+TEST(fgetc) {
+ _cleanup_fclose_ FILE *f = NULL;
+ char c;
+
+ assert_se(f = fmemopen_unlocked((void*) chars, sizeof(chars), "r"));
+
+ for (size_t i = 0; i < sizeof(chars); i++) {
+ assert_se(safe_fgetc(f, &c) == 1);
+ assert_se(c == chars[i]);
+
+ if (ungetc(c, f) == EOF) {
+ /* EOF is -1, and hence we can't push value 255 in this way – if char is signed */
+ assert_se(c == (char) EOF);
+ assert_se(CHAR_MIN == -128); /* verify that char is signed on this platform */
+ } else {
+ assert_se(safe_fgetc(f, &c) == 1);
+ assert_se(c == chars[i]);
+ }
+
+ /* But it works when we push it properly cast */
+ assert_se(ungetc((unsigned char) c, f) != EOF);
+ assert_se(safe_fgetc(f, &c) == 1);
+ assert_se(c == chars[i]);
+ }
+
+ assert_se(safe_fgetc(f, &c) == 0);
+}
+
+REENABLE_WARNING;
+
+static const char buffer[] =
+ "Some test data\n"
+ "루Non-ascii chars: ąę„”\n"
+ "terminators\r\n"
+ "and even more\n\r"
+ "now the same with a NUL\n\0"
+ "and more\r\0"
+ "and even more\r\n\0"
+ "and yet even more\n\r\0"
+ "With newlines, and a NUL byte\0"
+ "\n"
+ "an empty line\n"
+ "an ignored line\n"
+ "and a very long line that is supposed to be truncated, because it is so long\n";
+
+static void test_read_line_one_file(FILE *f) {
+ _cleanup_free_ char *line = NULL;
+
+ assert_se(read_line(f, SIZE_MAX, &line) == 15 && streq(line, "Some test data"));
+ line = mfree(line);
+
+ assert_se(read_line(f, SIZE_MAX, &line) > 0 && streq(line, "루Non-ascii chars: ąę„”"));
+ line = mfree(line);
+
+ assert_se(read_line(f, SIZE_MAX, &line) == 13 && streq(line, "terminators"));
+ line = mfree(line);
+
+ assert_se(read_line(f, SIZE_MAX, &line) == 15 && streq(line, "and even more"));
+ line = mfree(line);
+
+ assert_se(read_line(f, SIZE_MAX, &line) == 25 && streq(line, "now the same with a NUL"));
+ line = mfree(line);
+
+ assert_se(read_line(f, SIZE_MAX, &line) == 10 && streq(line, "and more"));
+ line = mfree(line);
+
+ assert_se(read_line(f, SIZE_MAX, &line) == 16 && streq(line, "and even more"));
+ line = mfree(line);
+
+ assert_se(read_line(f, SIZE_MAX, &line) == 20 && streq(line, "and yet even more"));
+ line = mfree(line);
+
+ assert_se(read_line(f, 1024, &line) == 30 && streq(line, "With newlines, and a NUL byte"));
+ line = mfree(line);
+
+ assert_se(read_line(f, 1024, &line) == 1 && streq(line, ""));
+ line = mfree(line);
+
+ assert_se(read_line(f, 1024, &line) == 14 && streq(line, "an empty line"));
+ line = mfree(line);
+
+ assert_se(read_line(f, SIZE_MAX, NULL) == 16);
+
+ assert_se(read_line(f, 16, &line) == -ENOBUFS);
+ line = mfree(line);
+
+ /* read_line() stopped when it hit the limit, that means when we continue reading we'll read at the first
+ * character after the previous limit. Let's make use of that to continue our test. */
+ assert_se(read_line(f, 1024, &line) == 62 && streq(line, "line that is supposed to be truncated, because it is so long"));
+ line = mfree(line);
+
+ assert_se(read_line(f, 1024, &line) == 0 && streq(line, ""));
+}
+
+TEST(read_line1) {
+ _cleanup_fclose_ FILE *f = NULL;
+
+ assert_se(f = fmemopen_unlocked((void*) buffer, sizeof(buffer), "r"));
+ test_read_line_one_file(f);
+}
+
+TEST(read_line2) {
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-fileio.XXXXXX";
+ int fd;
+ _cleanup_fclose_ FILE *f = NULL;
+
+ fd = mkostemp_safe(name);
+ assert_se(fd >= 0);
+ assert_se((size_t) write(fd, buffer, sizeof(buffer)) == sizeof(buffer));
+
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(f = fdopen(fd, "r"));
+
+ test_read_line_one_file(f);
+}
+
+TEST(read_line3) {
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *line = NULL;
+ int r;
+
+ f = fopen("/proc/uptime", "re");
+ if (!f && IN_SET(errno, ENOENT, EPERM))
+ return;
+ assert_se(f);
+
+ r = read_line(f, LINE_MAX, &line);
+ assert_se(r >= 0);
+ if (r == 0)
+ assert_se(line && isempty(line));
+ else
+ assert_se((size_t) r == strlen(line) + 1);
+ assert_se(read_line(f, LINE_MAX, NULL) == 0);
+}
+
+TEST(read_line4) {
+ static const struct {
+ size_t length;
+ const char *string;
+ } eof_endings[] = {
+ /* Each of these will be followed by EOF and should generate the one same single string */
+ { 3, "foo" },
+ { 4, "foo\n" },
+ { 4, "foo\r" },
+ { 4, "foo\0" },
+ { 5, "foo\n\0" },
+ { 5, "foo\r\0" },
+ { 5, "foo\r\n" },
+ { 5, "foo\n\r" },
+ { 6, "foo\r\n\0" },
+ { 6, "foo\n\r\0" },
+ };
+
+ int r;
+
+ for (size_t i = 0; i < ELEMENTSOF(eof_endings); i++) {
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *s = NULL;
+
+ assert_se(f = fmemopen_unlocked((void*) eof_endings[i].string, eof_endings[i].length, "r"));
+
+ r = read_line(f, SIZE_MAX, &s);
+ assert_se((size_t) r == eof_endings[i].length);
+ assert_se(streq_ptr(s, "foo"));
+
+ assert_se(read_line(f, SIZE_MAX, NULL) == 0); /* Ensure we hit EOF */
+ }
+}
+
+TEST(read_nul_string) {
+ static const char test[] = "string nr. 1\0"
+ "string nr. 2\n\0"
+ "\377empty string follows\0"
+ "\0"
+ "final string\n is empty\0"
+ "\0";
+
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *s = NULL;
+
+ assert_se(f = fmemopen_unlocked((void*) test, sizeof(test)-1, "r"));
+
+ assert_se(read_nul_string(f, LONG_LINE_MAX, &s) == 13 && streq_ptr(s, "string nr. 1"));
+ s = mfree(s);
+
+ assert_se(read_nul_string(f, LONG_LINE_MAX, &s) == 14 && streq_ptr(s, "string nr. 2\n"));
+ s = mfree(s);
+
+ assert_se(read_nul_string(f, LONG_LINE_MAX, &s) == 22 && streq_ptr(s, "\377empty string follows"));
+ s = mfree(s);
+
+ assert_se(read_nul_string(f, LONG_LINE_MAX, &s) == 1 && streq_ptr(s, ""));
+ s = mfree(s);
+
+ assert_se(read_nul_string(f, LONG_LINE_MAX, &s) == 23 && streq_ptr(s, "final string\n is empty"));
+ s = mfree(s);
+
+ assert_se(read_nul_string(f, LONG_LINE_MAX, &s) == 1 && streq_ptr(s, ""));
+ s = mfree(s);
+
+ assert_se(read_nul_string(f, LONG_LINE_MAX, &s) == 0 && streq_ptr(s, ""));
+}
+
+TEST(read_full_file_socket) {
+ _cleanup_(rm_rf_physical_and_freep) char *z = NULL;
+ _cleanup_close_ int listener = -EBADF;
+ _cleanup_free_ char *data = NULL, *clientname = NULL;
+ union sockaddr_union sa;
+ const char *j, *jj;
+ size_t size;
+ pid_t pid;
+ int r;
+
+ listener = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0);
+ assert_se(listener >= 0);
+
+ assert_se(mkdtemp_malloc(NULL, &z) >= 0);
+ j = strjoina(z, "/socket");
+
+ assert_se(sockaddr_un_set_path(&sa.un, j) >= 0);
+
+ assert_se(bind(listener, &sa.sa, SOCKADDR_UN_LEN(sa.un)) >= 0);
+ assert_se(listen(listener, 1) >= 0);
+
+ /* Make sure the socket doesn't fit into a struct sockaddr_un, but we can still access it */
+ jj = strjoina(z, "/a_very_long_patha_very_long_patha_very_long_patha_very_long_patha_very_long_patha_very_long_patha_very_long_patha_very_long_path");
+ assert_se(strlen(jj) > sizeof_field(struct sockaddr_un, sun_path));
+ assert_se(rename(j, jj) >= 0);
+
+ /* Bind the *client* socket to some randomized name, to verify that this works correctly. */
+ assert_se(asprintf(&clientname, "@%" PRIx64 "/test-bindname", random_u64()) >= 0);
+
+ r = safe_fork("(server)", FORK_DEATHSIG_SIGTERM|FORK_LOG, &pid);
+ assert_se(r >= 0);
+ if (r == 0) {
+ union sockaddr_union peer = {};
+ socklen_t peerlen = sizeof(peer);
+ _cleanup_close_ int rfd = -EBADF;
+ /* child */
+
+ rfd = accept4(listener, NULL, 0, SOCK_CLOEXEC);
+ assert_se(rfd >= 0);
+
+ assert_se(getpeername(rfd, &peer.sa, &peerlen) >= 0);
+
+ assert_se(peer.un.sun_family == AF_UNIX);
+ assert_se(peerlen > offsetof(struct sockaddr_un, sun_path));
+ assert_se(peer.un.sun_path[0] == 0);
+ assert_se(streq(peer.un.sun_path + 1, clientname + 1));
+
+#define TEST_STR "This is a test\nreally."
+
+ assert_se(write(rfd, TEST_STR, strlen(TEST_STR)) == strlen(TEST_STR));
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(read_full_file_full(AT_FDCWD, jj, UINT64_MAX, SIZE_MAX, 0, NULL, &data, &size) == -ENXIO);
+ assert_se(read_full_file_full(AT_FDCWD, jj, UINT64_MAX, SIZE_MAX, READ_FULL_FILE_CONNECT_SOCKET, clientname, &data, &size) >= 0);
+ assert_se(size == strlen(TEST_STR));
+ assert_se(streq(data, TEST_STR));
+
+ assert_se(wait_for_terminate_and_check("(server)", pid, WAIT_LOG) >= 0);
+#undef TEST_STR
+}
+
+TEST(read_full_file_offset_size) {
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_(unlink_and_freep) char *fn = NULL;
+ _cleanup_free_ char *rbuf = NULL;
+ size_t rbuf_size;
+ uint8_t buf[4711];
+
+ random_bytes(buf, sizeof(buf));
+
+ assert_se(tempfn_random_child(NULL, NULL, &fn) >= 0);
+ assert_se(f = fopen(fn, "we"));
+ assert_se(fwrite(buf, 1, sizeof(buf), f) == sizeof(buf));
+ assert_se(fflush_and_check(f) >= 0);
+
+ assert_se(read_full_file_full(AT_FDCWD, fn, UINT64_MAX, SIZE_MAX, 0, NULL, &rbuf, &rbuf_size) >= 0);
+ assert_se(rbuf_size == sizeof(buf));
+ assert_se(memcmp(buf, rbuf, rbuf_size) == 0);
+ rbuf = mfree(rbuf);
+
+ assert_se(read_full_file_full(AT_FDCWD, fn, UINT64_MAX, 128, 0, NULL, &rbuf, &rbuf_size) >= 0);
+ assert_se(rbuf_size == 128);
+ assert_se(memcmp(buf, rbuf, rbuf_size) == 0);
+ rbuf = mfree(rbuf);
+
+ assert_se(read_full_file_full(AT_FDCWD, fn, UINT64_MAX, 128, READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &rbuf, &rbuf_size) == -E2BIG);
+ assert_se(read_full_file_full(AT_FDCWD, fn, UINT64_MAX, sizeof(buf)-1, READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &rbuf, &rbuf_size) == -E2BIG);
+ assert_se(read_full_file_full(AT_FDCWD, fn, UINT64_MAX, sizeof(buf), READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &rbuf, &rbuf_size) >= 0);
+ assert_se(rbuf_size == sizeof(buf));
+ assert_se(memcmp(buf, rbuf, rbuf_size) == 0);
+ rbuf = mfree(rbuf);
+
+ assert_se(read_full_file_full(AT_FDCWD, fn, 47, 128, READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &rbuf, &rbuf_size) == -E2BIG);
+ assert_se(read_full_file_full(AT_FDCWD, fn, 47, sizeof(buf)-47-1, READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &rbuf, &rbuf_size) == -E2BIG);
+ assert_se(read_full_file_full(AT_FDCWD, fn, 47, sizeof(buf)-47, READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &rbuf, &rbuf_size) >= 0);
+ assert_se(rbuf_size == sizeof(buf)-47);
+ assert_se(memcmp(buf+47, rbuf, rbuf_size) == 0);
+ rbuf = mfree(rbuf);
+
+ assert_se(read_full_file_full(AT_FDCWD, fn, UINT64_MAX, sizeof(buf)+1, READ_FULL_FILE_FAIL_WHEN_LARGER, NULL, &rbuf, &rbuf_size) >= 0);
+ assert_se(rbuf_size == sizeof(buf));
+ assert_se(memcmp(buf, rbuf, rbuf_size) == 0);
+ rbuf = mfree(rbuf);
+
+ assert_se(read_full_file_full(AT_FDCWD, fn, 1234, SIZE_MAX, 0, NULL, &rbuf, &rbuf_size) >= 0);
+ assert_se(rbuf_size == sizeof(buf) - 1234);
+ assert_se(memcmp(buf + 1234, rbuf, rbuf_size) == 0);
+ rbuf = mfree(rbuf);
+
+ assert_se(read_full_file_full(AT_FDCWD, fn, 2345, 777, 0, NULL, &rbuf, &rbuf_size) >= 0);
+ assert_se(rbuf_size == 777);
+ assert_se(memcmp(buf + 2345, rbuf, rbuf_size) == 0);
+ rbuf = mfree(rbuf);
+
+ assert_se(read_full_file_full(AT_FDCWD, fn, 4700, 20, 0, NULL, &rbuf, &rbuf_size) >= 0);
+ assert_se(rbuf_size == 11);
+ assert_se(memcmp(buf + 4700, rbuf, rbuf_size) == 0);
+ rbuf = mfree(rbuf);
+
+ assert_se(read_full_file_full(AT_FDCWD, fn, 10000, 99, 0, NULL, &rbuf, &rbuf_size) >= 0);
+ assert_se(rbuf_size == 0);
+ rbuf = mfree(rbuf);
+}
+
+static void test_read_virtual_file_one(size_t max_size) {
+ int r;
+
+ log_info("/* %s (max_size=%zu) */", __func__, max_size);
+
+ FOREACH_STRING(filename,
+ "/proc/1/cmdline",
+ "/etc/nsswitch.conf",
+ "/sys/kernel/uevent_seqnum",
+ "/proc/kcore",
+ "/proc/kallsyms",
+ "/proc/self/exe",
+ "/proc/self/pagemap") {
+
+ _cleanup_free_ char *buf = NULL;
+ size_t size = 0;
+
+ r = read_virtual_file(filename, max_size, &buf, &size);
+ if (r < 0) {
+ log_info_errno(r, "read_virtual_file(\"%s\", %zu): %m", filename, max_size);
+ assert_se(ERRNO_IS_PRIVILEGE(r) || /* /proc/kcore is not accessible to unpriv */
+ IN_SET(r,
+ -ENOENT, /* Some of the files might be absent */
+ -EINVAL, /* too small reads from /proc/self/pagemap trigger EINVAL */
+ -EFBIG, /* /proc/kcore and /proc/self/pagemap should be too large */
+ -EBADF)); /* /proc/kcore is masked when we are running in docker. */
+ } else
+ log_info("read_virtual_file(\"%s\", %zu): %s (%zu bytes)", filename, max_size, r ? "non-truncated" : "truncated", size);
+ }
+}
+
+TEST(read_virtual_file) {
+ test_read_virtual_file_one(0);
+ test_read_virtual_file_one(1);
+ test_read_virtual_file_one(2);
+ test_read_virtual_file_one(20);
+ test_read_virtual_file_one(4096);
+ test_read_virtual_file_one(4097);
+ test_read_virtual_file_one(SIZE_MAX);
+}
+
+TEST(fdopen_independent) {
+#define TEST_TEXT "this is some random test text we are going to write to a memfd"
+ _cleanup_close_ int fd = -EBADF;
+ _cleanup_fclose_ FILE *f = NULL;
+ char buf[STRLEN(TEST_TEXT) + 1];
+
+ fd = memfd_new("fdopen_independent");
+ if (fd < 0) {
+ assert_se(ERRNO_IS_NOT_SUPPORTED(fd));
+ return;
+ }
+
+ assert_se(write(fd, TEST_TEXT, strlen(TEST_TEXT)) == strlen(TEST_TEXT));
+ /* we'll leave the read offset at the end of the memfd, the fdopen_independent() descriptors should
+ * start at the beginning anyway */
+
+ assert_se(fdopen_independent(fd, "re", &f) >= 0);
+ zero(buf);
+ assert_se(fread(buf, 1, sizeof(buf), f) == strlen(TEST_TEXT));
+ assert_se(streq(buf, TEST_TEXT));
+ assert_se((fcntl(fileno(f), F_GETFL) & O_ACCMODE) == O_RDONLY);
+ assert_se(FLAGS_SET(fcntl(fileno(f), F_GETFD), FD_CLOEXEC));
+ f = safe_fclose(f);
+
+ assert_se(fdopen_independent(fd, "r", &f) >= 0);
+ zero(buf);
+ assert_se(fread(buf, 1, sizeof(buf), f) == strlen(TEST_TEXT));
+ assert_se(streq(buf, TEST_TEXT));
+ assert_se((fcntl(fileno(f), F_GETFL) & O_ACCMODE) == O_RDONLY);
+ assert_se(!FLAGS_SET(fcntl(fileno(f), F_GETFD), FD_CLOEXEC));
+ f = safe_fclose(f);
+
+ assert_se(fdopen_independent(fd, "r+e", &f) >= 0);
+ zero(buf);
+ assert_se(fread(buf, 1, sizeof(buf), f) == strlen(TEST_TEXT));
+ assert_se(streq(buf, TEST_TEXT));
+ assert_se((fcntl(fileno(f), F_GETFL) & O_ACCMODE) == O_RDWR);
+ assert_se(FLAGS_SET(fcntl(fileno(f), F_GETFD), FD_CLOEXEC));
+ f = safe_fclose(f);
+}
+
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-firewall-util.c b/src/test/test-firewall-util.c
new file mode 100644
index 0000000..3f47a30
--- /dev/null
+++ b/src/test/test-firewall-util.c
@@ -0,0 +1,123 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <unistd.h>
+
+#include "firewall-util.h"
+#include "firewall-util-private.h"
+#include "log.h"
+#include "random-util.h"
+#include "socket-util.h"
+#include "tests.h"
+
+static void test_v6(FirewallContext *ctx) {
+ union in_addr_union u1, u2, u3;
+ uint8_t prefixlen;
+ int r;
+
+ assert_se(ctx);
+
+ log_info("/* %s(backend=%s) */", __func__, firewall_backend_to_string(ctx->backend));
+
+ if (!socket_ipv6_is_supported())
+ return log_info("IPv6 is not supported by kernel, skipping tests.");
+
+ assert_se(in_addr_from_string(AF_INET6, "dead::beef", &u1) >= 0);
+ assert_se(in_addr_from_string(AF_INET6, "1c3::c01d", &u2) >= 0);
+
+ prefixlen = random_u64_range(128 + 1 - 8) + 8;
+ random_bytes(&u3, sizeof(u3));
+
+ assert_se(fw_add_masquerade(&ctx, true, AF_INET6, &u1, 128) >= 0);
+ assert_se(fw_add_masquerade(&ctx, false, AF_INET6, &u1, 128) >= 0);
+ assert_se(fw_add_masquerade(&ctx, true, AF_INET6, &u1, 64) >= 0);
+ assert_se(fw_add_masquerade(&ctx, false, AF_INET6, &u1, 64) >= 0);
+ assert_se(fw_add_masquerade(&ctx, true, AF_INET6, &u3, prefixlen) >= 0);
+ assert_se(fw_add_masquerade(&ctx, false, AF_INET6, &u3, prefixlen) >= 0);
+
+ r = fw_add_local_dnat(&ctx, true, AF_INET6, IPPROTO_TCP, 4711, &u1, 815, NULL);
+ if (r == -EOPNOTSUPP) {
+ log_info("IPv6 DNAT seems not supported, skipping the following tests.");
+ return;
+ }
+ assert_se(r >= 0);
+
+ assert_se(fw_add_local_dnat(&ctx, true, AF_INET6, IPPROTO_TCP, 4711, &u2, 815, &u1) >= 0);
+ assert_se(fw_add_local_dnat(&ctx, false, AF_INET6, IPPROTO_TCP, 4711, &u2, 815, NULL) >= 0);
+
+}
+
+static union in_addr_union *parse_addr(const char *str, union in_addr_union *u) {
+ assert_se(str);
+ assert_se(u);
+ assert_se(in_addr_from_string(AF_INET, str, u) >= 0);
+ return u;
+}
+
+static bool test_v4(FirewallContext *ctx) {
+ union in_addr_union u, v;
+ int r;
+
+ assert_se(ctx);
+
+ log_info("/* %s(backend=%s) */", __func__, firewall_backend_to_string(ctx->backend));
+
+#if HAVE_LIBIPTC
+ if (ctx->backend == FW_BACKEND_IPTABLES && fw_iptables_init_nat(NULL) < 0) {
+ log_debug("iptables backend is used, but nat table is not enabled, skipping tests");
+ return false;
+ }
+#endif
+
+ assert_se(fw_add_masquerade(&ctx, true, AF_INET, NULL, 0) == -EINVAL);
+ assert_se(fw_add_masquerade(&ctx, true, AF_INET, parse_addr("10.1.2.0", &u), 0) == -EINVAL);
+
+ r = fw_add_masquerade(&ctx, true, AF_INET, parse_addr("10.1.2.3", &u), 32);
+ if (r < 0) {
+ bool ignore = IN_SET(r, -EPERM, -EOPNOTSUPP, -ENOPROTOOPT);
+
+ log_full_errno(ignore ? LOG_DEBUG : LOG_ERR, r,
+ "Failed to add IPv4 masquerade%s: %m",
+ ignore ? ", skipping following tests" : "");
+
+ if (ignore)
+ return false;
+ }
+ assert_se(r >= 0);
+
+ assert_se(fw_add_masquerade(&ctx, true, AF_INET, parse_addr("10.0.2.0", &u), 28) >= 0);
+ assert_se(fw_add_masquerade(&ctx, false, AF_INET, parse_addr("10.0.2.0", &u), 28) >= 0);
+ assert_se(fw_add_masquerade(&ctx, false, AF_INET, parse_addr("10.1.2.3", &u), 32) >= 0);
+ assert_se(fw_add_local_dnat(&ctx, true, AF_INET, IPPROTO_TCP, 4711, parse_addr("1.2.3.4", &u), 815, NULL) >= 0);
+ assert_se(fw_add_local_dnat(&ctx, true, AF_INET, IPPROTO_TCP, 4711, parse_addr("1.2.3.4", &u), 815, NULL) >= 0);
+ assert_se(fw_add_local_dnat(&ctx, true, AF_INET, IPPROTO_TCP, 4711, parse_addr("1.2.3.5", &u), 815, parse_addr("1.2.3.4", &v)) >= 0);
+ assert_se(fw_add_local_dnat(&ctx, false, AF_INET, IPPROTO_TCP, 4711, parse_addr("1.2.3.5", &u), 815, NULL) >= 0);
+
+ return true;
+}
+
+int main(int argc, char *argv[]) {
+ _cleanup_(fw_ctx_freep) FirewallContext *ctx = NULL;
+
+ test_setup_logging(LOG_DEBUG);
+
+ if (getuid() != 0)
+ return log_tests_skipped("not root");
+
+ assert_se(fw_ctx_new(&ctx) >= 0);
+ assert_se(ctx);
+
+ if (ctx->backend == FW_BACKEND_NONE)
+ return log_tests_skipped("no firewall backend supported");
+
+ if (test_v4(ctx) && ctx->backend == FW_BACKEND_NFTABLES)
+ test_v6(ctx);
+
+#if HAVE_LIBIPTC
+ if (ctx->backend != FW_BACKEND_IPTABLES) {
+ ctx->backend = FW_BACKEND_IPTABLES;
+ test_v4(ctx);
+ }
+#endif
+
+ return 0;
+}
diff --git a/src/test/test-format-table.c b/src/test/test-format-table.c
new file mode 100644
index 0000000..7d544b1
--- /dev/null
+++ b/src/test/test-format-table.c
@@ -0,0 +1,635 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "format-table.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "time-util.h"
+
+TEST(issue_9549) {
+ _cleanup_(table_unrefp) Table *table = NULL;
+ _cleanup_free_ char *formatted = NULL;
+
+ assert_se(table = table_new("name", "type", "ro", "usage", "created", "modified"));
+ assert_se(table_set_align_percent(table, TABLE_HEADER_CELL(3), 100) >= 0);
+ assert_se(table_add_many(table,
+ TABLE_STRING, "foooo",
+ TABLE_STRING, "raw",
+ TABLE_BOOLEAN, false,
+ TABLE_SIZE, (uint64_t) (673.7*1024*1024),
+ TABLE_STRING, "Wed 2018-07-11 00:10:33 JST",
+ TABLE_STRING, "Wed 2018-07-11 00:16:00 JST") >= 0);
+
+ table_set_width(table, 75);
+ assert_se(table_format(table, &formatted) >= 0);
+
+ printf("%s\n", formatted);
+ assert_se(streq(formatted,
+ "NAME TYPE RO USAGE CREATED MODIFIED\n"
+ "foooo raw no 673.6M Wed 2018-07-11 00:10:33 J… Wed 2018-07-11 00:16:00 JST\n"
+ ));
+}
+
+TEST(multiline) {
+ _cleanup_(table_unrefp) Table *table = NULL;
+ _cleanup_free_ char *formatted = NULL;
+
+ assert_se(table = table_new("foo", "bar"));
+
+ assert_se(table_set_align_percent(table, TABLE_HEADER_CELL(1), 100) >= 0);
+
+ assert_se(table_add_many(table,
+ TABLE_STRING, "three\ndifferent\nlines",
+ TABLE_STRING, "two\nlines\n") >= 0);
+
+ table_set_cell_height_max(table, 1);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three… two…\n"));
+ formatted = mfree(formatted);
+
+ table_set_cell_height_max(table, 2);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three two\n"
+ "different… lines\n"));
+ formatted = mfree(formatted);
+
+ table_set_cell_height_max(table, 3);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three two\n"
+ "different lines\n"
+ "lines \n"));
+ formatted = mfree(formatted);
+
+ table_set_cell_height_max(table, SIZE_MAX);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three two\n"
+ "different lines\n"
+ "lines \n"));
+ formatted = mfree(formatted);
+
+ assert_se(table_add_many(table,
+ TABLE_STRING, "short",
+ TABLE_STRING, "a\npair") >= 0);
+
+ assert_se(table_add_many(table,
+ TABLE_STRING, "short2\n",
+ TABLE_STRING, "a\nfour\nline\ncell") >= 0);
+
+ table_set_cell_height_max(table, 1);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three… two…\n"
+ "short a…\n"
+ "short2 a…\n"));
+ formatted = mfree(formatted);
+
+ table_set_cell_height_max(table, 2);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three two\n"
+ "different… lines\n"
+ "short a\n"
+ " pair\n"
+ "short2 a\n"
+ " four…\n"));
+ formatted = mfree(formatted);
+
+ table_set_cell_height_max(table, 3);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three two\n"
+ "different lines\n"
+ "lines \n"
+ "short a\n"
+ " pair\n"
+ "short2 a\n"
+ " four\n"
+ " line…\n"));
+ formatted = mfree(formatted);
+
+ table_set_cell_height_max(table, SIZE_MAX);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three two\n"
+ "different lines\n"
+ "lines \n"
+ "short a\n"
+ " pair\n"
+ "short2 a\n"
+ " four\n"
+ " line\n"
+ " cell\n"));
+ formatted = mfree(formatted);
+}
+
+TEST(strv) {
+ _cleanup_(table_unrefp) Table *table = NULL;
+ _cleanup_free_ char *formatted = NULL;
+
+ assert_se(table = table_new("foo", "bar"));
+
+ assert_se(table_set_align_percent(table, TABLE_HEADER_CELL(1), 100) >= 0);
+
+ assert_se(table_add_many(table,
+ TABLE_STRV, STRV_MAKE("three", "different", "lines"),
+ TABLE_STRV, STRV_MAKE("two", "lines")) >= 0);
+
+ table_set_cell_height_max(table, 1);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three… two…\n"));
+ formatted = mfree(formatted);
+
+ table_set_cell_height_max(table, 2);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three two\n"
+ "different… lines\n"));
+ formatted = mfree(formatted);
+
+ table_set_cell_height_max(table, 3);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three two\n"
+ "different lines\n"
+ "lines \n"));
+ formatted = mfree(formatted);
+
+ table_set_cell_height_max(table, SIZE_MAX);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three two\n"
+ "different lines\n"
+ "lines \n"));
+ formatted = mfree(formatted);
+
+ assert_se(table_add_many(table,
+ TABLE_STRING, "short",
+ TABLE_STRV, STRV_MAKE("a", "pair")) >= 0);
+
+ assert_se(table_add_many(table,
+ TABLE_STRV, STRV_MAKE("short2"),
+ TABLE_STRV, STRV_MAKE("a", "four", "line", "cell")) >= 0);
+
+ table_set_cell_height_max(table, 1);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three… two…\n"
+ "short a…\n"
+ "short2 a…\n"));
+ formatted = mfree(formatted);
+
+ table_set_cell_height_max(table, 2);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three two\n"
+ "different… lines\n"
+ "short a\n"
+ " pair\n"
+ "short2 a\n"
+ " four…\n"));
+ formatted = mfree(formatted);
+
+ table_set_cell_height_max(table, 3);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three two\n"
+ "different lines\n"
+ "lines \n"
+ "short a\n"
+ " pair\n"
+ "short2 a\n"
+ " four\n"
+ " line…\n"));
+ formatted = mfree(formatted);
+
+ table_set_cell_height_max(table, SIZE_MAX);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three two\n"
+ "different lines\n"
+ "lines \n"
+ "short a\n"
+ " pair\n"
+ "short2 a\n"
+ " four\n"
+ " line\n"
+ " cell\n"));
+ formatted = mfree(formatted);
+}
+
+TEST(strv_wrapped) {
+ _cleanup_(table_unrefp) Table *table = NULL;
+ _cleanup_free_ char *formatted = NULL;
+
+ assert_se(table = table_new("foo", "bar"));
+
+ assert_se(table_set_align_percent(table, TABLE_HEADER_CELL(1), 100) >= 0);
+
+ assert_se(table_add_many(table,
+ TABLE_STRV_WRAPPED, STRV_MAKE("three", "different", "lines"),
+ TABLE_STRV_WRAPPED, STRV_MAKE("two", "lines")) >= 0);
+
+ table_set_cell_height_max(table, 1);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three different lines two lines\n"));
+ formatted = mfree(formatted);
+
+ table_set_cell_height_max(table, 2);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three different lines two lines\n"));
+ formatted = mfree(formatted);
+
+ table_set_cell_height_max(table, 3);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three different lines two lines\n"));
+ formatted = mfree(formatted);
+
+ table_set_cell_height_max(table, SIZE_MAX);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three different lines two lines\n"));
+ formatted = mfree(formatted);
+
+ assert_se(table_add_many(table,
+ TABLE_STRING, "short",
+ TABLE_STRV_WRAPPED, STRV_MAKE("a", "pair")) >= 0);
+
+ assert_se(table_add_many(table,
+ TABLE_STRV_WRAPPED, STRV_MAKE("short2"),
+ TABLE_STRV_WRAPPED, STRV_MAKE("a", "eight", "line", "ćęłł",
+ "___5___", "___6___", "___7___", "___8___")) >= 0);
+
+ table_set_cell_height_max(table, 1);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three different… two lines\n"
+ "short a pair\n"
+ "short2 a eight line ćęłł…\n"));
+ formatted = mfree(formatted);
+
+ table_set_cell_height_max(table, 2);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three different two lines\n"
+ "lines \n"
+ "short a pair\n"
+ "short2 a eight line ćęłł\n"
+ " ___5___ ___6___…\n"));
+ formatted = mfree(formatted);
+
+ table_set_cell_height_max(table, 3);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three different two lines\n"
+ "lines \n"
+ "short a pair\n"
+ "short2 a eight line ćęłł\n"
+ " ___5___ ___6___\n"
+ " ___7___ ___8___\n"));
+ formatted = mfree(formatted);
+
+ table_set_cell_height_max(table, SIZE_MAX);
+ assert_se(table_format(table, &formatted) >= 0);
+ fputs(formatted, stdout);
+ assert_se(streq(formatted,
+ "FOO BAR\n"
+ "three different two lines\n"
+ "lines \n"
+ "short a pair\n"
+ "short2 a eight line ćęłł\n"
+ " ___5___ ___6___\n"
+ " ___7___ ___8___\n"));
+ formatted = mfree(formatted);
+}
+
+TEST(json) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL;
+ _cleanup_(table_unrefp) Table *t = NULL;
+
+ assert_se(t = table_new("foo bar", "quux", "piep miau"));
+ assert_se(table_set_json_field_name(t, 2, "zzz") >= 0);
+
+ assert_se(table_add_many(t,
+ TABLE_STRING, "v1",
+ TABLE_UINT64, UINT64_C(4711),
+ TABLE_BOOLEAN, true) >= 0);
+
+ assert_se(table_add_many(t,
+ TABLE_STRV, STRV_MAKE("a", "b", "c"),
+ TABLE_EMPTY,
+ TABLE_MODE, 0755) >= 0);
+
+ assert_se(table_to_json(t, &v) >= 0);
+
+ assert_se(json_build(&w,
+ JSON_BUILD_ARRAY(
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("foo_bar", JSON_BUILD_CONST_STRING("v1")),
+ JSON_BUILD_PAIR("quux", JSON_BUILD_UNSIGNED(4711)),
+ JSON_BUILD_PAIR("zzz", JSON_BUILD_BOOLEAN(true))),
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("foo_bar", JSON_BUILD_STRV(STRV_MAKE("a", "b", "c"))),
+ JSON_BUILD_PAIR("quux", JSON_BUILD_NULL),
+ JSON_BUILD_PAIR("zzz", JSON_BUILD_UNSIGNED(0755))))) >= 0);
+
+ assert_se(json_variant_equal(v, w));
+}
+
+TEST(table) {
+ _cleanup_(table_unrefp) Table *t = NULL;
+ _cleanup_free_ char *formatted = NULL;
+
+ assert_se(t = table_new("one", "two", "three", "four"));
+
+ assert_se(table_set_align_percent(t, TABLE_HEADER_CELL(3), 100) >= 0);
+
+ assert_se(table_add_many(t,
+ TABLE_STRING, "xxx",
+ TABLE_STRING, "yyy",
+ TABLE_BOOLEAN, true,
+ TABLE_INT, -1) >= 0);
+
+ assert_se(table_add_many(t,
+ TABLE_STRING, "a long field",
+ TABLE_STRING, "yyy",
+ TABLE_SET_UPPERCASE, 1,
+ TABLE_BOOLEAN, false,
+ TABLE_INT, -999999) >= 0);
+
+ assert_se(table_format(t, &formatted) >= 0);
+ printf("%s\n", formatted);
+
+ assert_se(streq(formatted,
+ "ONE TWO THREE FOUR\n"
+ "xxx yyy yes -1\n"
+ "a long field YYY no -999999\n"));
+
+ formatted = mfree(formatted);
+
+ table_set_width(t, 40);
+
+ assert_se(table_format(t, &formatted) >= 0);
+ printf("%s\n", formatted);
+
+ assert_se(streq(formatted,
+ "ONE TWO THREE FOUR\n"
+ "xxx yyy yes -1\n"
+ "a long field YYY no -999999\n"));
+
+ formatted = mfree(formatted);
+
+ table_set_width(t, 15);
+ assert_se(table_format(t, &formatted) >= 0);
+ printf("%s\n", formatted);
+
+ assert_se(streq(formatted,
+ "ONE TWO TH… FO…\n"
+ "xxx yyy yes -1\n"
+ "a … YYY no -9…\n"));
+
+ formatted = mfree(formatted);
+
+ table_set_width(t, 5);
+ assert_se(table_format(t, &formatted) >= 0);
+ printf("%s\n", formatted);
+
+ assert_se(streq(formatted,
+ "… … … …\n"
+ "… … … …\n"
+ "… … … …\n"));
+
+ formatted = mfree(formatted);
+
+ table_set_width(t, 3);
+ assert_se(table_format(t, &formatted) >= 0);
+ printf("%s\n", formatted);
+
+ assert_se(streq(formatted,
+ "… … … …\n"
+ "… … … …\n"
+ "… … … …\n"));
+
+ formatted = mfree(formatted);
+
+ table_set_width(t, SIZE_MAX);
+ assert_se(table_set_sort(t, (size_t) 0, (size_t) 2, SIZE_MAX) >= 0);
+
+ assert_se(table_format(t, &formatted) >= 0);
+ printf("%s\n", formatted);
+
+ assert_se(streq(formatted,
+ "ONE TWO THREE FOUR\n"
+ "a long field YYY no -999999\n"
+ "xxx yyy yes -1\n"));
+
+ formatted = mfree(formatted);
+
+ table_set_header(t, false);
+
+ assert_se(table_add_many(t,
+ TABLE_STRING, "fäää",
+ TABLE_STRING, "uuu",
+ TABLE_BOOLEAN, true,
+ TABLE_INT, 42) >= 0);
+
+ assert_se(table_add_many(t,
+ TABLE_STRING, "fäää",
+ TABLE_STRING, "zzz",
+ TABLE_BOOLEAN, false,
+ TABLE_INT, 0) >= 0);
+
+ assert_se(table_add_many(t,
+ TABLE_EMPTY,
+ TABLE_SIZE, (uint64_t) 4711,
+ TABLE_TIMESPAN, (usec_t) 5*USEC_PER_MINUTE,
+ TABLE_INT64, (uint64_t) -123456789) >= 0);
+
+ assert_se(table_format(t, &formatted) >= 0);
+ printf("%s\n", formatted);
+
+ assert_se(streq(formatted,
+ "a long field YYY no -999999\n"
+ "fäää zzz no 0\n"
+ "fäää uuu yes 42\n"
+ "xxx yyy yes -1\n"
+ " 4.6K 5min -123456789\n"));
+
+ formatted = mfree(formatted);
+
+ assert_se(table_set_display(t, (size_t) 2, (size_t) 0, (size_t) 2, (size_t) 0, (size_t) 0, SIZE_MAX) >= 0);
+
+ assert_se(table_format(t, &formatted) >= 0);
+ printf("%s\n", formatted);
+
+ if (isatty(STDOUT_FILENO))
+ assert_se(streq(formatted,
+ "no a long f… no a long f… a long fi…\n"
+ "no fäää no fäää fäää\n"
+ "yes fäää yes fäää fäää\n"
+ "yes xxx yes xxx xxx\n"
+ "5min 5min \n"));
+ else
+ assert_se(streq(formatted,
+ "no a long field no a long field a long field\n"
+ "no fäää no fäää fäää\n"
+ "yes fäää yes fäää fäää\n"
+ "yes xxx yes xxx xxx\n"
+ "5min 5min \n"));
+}
+
+TEST(vertical) {
+ _cleanup_(table_unrefp) Table *t = NULL;
+ _cleanup_free_ char *formatted = NULL;
+
+ assert_se(t = table_new_vertical());
+
+ assert_se(table_add_many(t,
+ TABLE_FIELD, "pfft aa", TABLE_STRING, "foo",
+ TABLE_FIELD, "uuu o", TABLE_SIZE, UINT64_C(1024),
+ TABLE_FIELD, "lllllllllllo", TABLE_STRING, "jjjjjjjjjjjjjjjjj") >= 0);
+
+ assert_se(table_set_json_field_name(t, 1, "dimpfelmoser") >= 0);
+
+ assert_se(table_format(t, &formatted) >= 0);
+
+ assert_se(streq(formatted,
+ " pfft aa: foo\n"
+ " uuu o: 1.0K\n"
+ "lllllllllllo: jjjjjjjjjjjjjjjjj\n"));
+
+ _cleanup_(json_variant_unrefp) JsonVariant *a = NULL, *b = NULL;
+ assert_se(table_to_json(t, &a) >= 0);
+
+ assert_se(json_build(&b, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("pfft_aa", JSON_BUILD_STRING("foo")),
+ JSON_BUILD_PAIR("dimpfelmoser", JSON_BUILD_UNSIGNED(1024)),
+ JSON_BUILD_PAIR("lllllllllllo", JSON_BUILD_STRING("jjjjjjjjjjjjjjjjj")))) >= 0);
+
+ assert_se(json_variant_equal(a, b));
+}
+
+TEST(path_basename) {
+ _cleanup_(table_unrefp) Table *t = NULL;
+ _cleanup_free_ char *formatted = NULL;
+
+ assert_se(t = table_new("x"));
+
+ table_set_header(t, false);
+
+ assert_se(table_add_many(t,
+ TABLE_PATH_BASENAME, "/foo/bar",
+ TABLE_PATH_BASENAME, "/quux/bar",
+ TABLE_PATH_BASENAME, "/foo/baz") >= 0);
+
+ assert_se(table_format(t, &formatted) >= 0);
+
+ assert_se(streq(formatted, "bar\nbar\nbaz\n"));
+}
+
+TEST(dup_cell) {
+ _cleanup_(table_unrefp) Table *t = NULL;
+ _cleanup_free_ char *formatted = NULL;
+
+ assert_se(t = table_new("foo", "bar", "x", "baz", ".", "%", "!", "~", "+"));
+ table_set_width(t, 75);
+
+ assert_se(table_add_many(t,
+ TABLE_STRING, "hello",
+ TABLE_UINT8, UINT8_C(42),
+ TABLE_UINT16, UINT16_C(666),
+ TABLE_UINT32, UINT32_C(253),
+ TABLE_PERCENT, 0,
+ TABLE_PATH_BASENAME, "/foo/bar",
+ TABLE_STRING, "aaa",
+ TABLE_STRING, "bbb",
+ TABLE_STRING, "ccc") >= 0);
+
+ /* Add the second row by duping cells */
+ for (size_t i = 0; i < table_get_columns(t); i++)
+ assert_se(table_dup_cell(t, table_get_cell(t, 1, i)) >= 0);
+
+ /* Another row, but dupe the last three strings from the same cell */
+ assert_se(table_add_many(t,
+ TABLE_STRING, "aaa",
+ TABLE_UINT8, UINT8_C(0),
+ TABLE_UINT16, UINT16_C(65535),
+ TABLE_UINT32, UINT32_C(4294967295),
+ TABLE_PERCENT, 100,
+ TABLE_PATH_BASENAME, "../") >= 0);
+
+ for (size_t i = 6; i < table_get_columns(t); i++)
+ assert_se(table_dup_cell(t, table_get_cell(t, 2, 0)) >= 0);
+
+ assert_se(table_format(t, &formatted) >= 0);
+ printf("%s\n", formatted);
+ assert_se(streq(formatted,
+ "FOO BAR X BAZ . % ! ~ +\n"
+ "hello 42 666 253 0% bar aaa bbb ccc\n"
+ "hello 42 666 253 0% bar aaa bbb ccc\n"
+ "aaa 0 65535 4294967295 100% ../ hello hello hello\n"));
+}
+
+static int intro(void) {
+ assert_se(setenv("SYSTEMD_COLORS", "0", 1) >= 0);
+ assert_se(setenv("COLUMNS", "40", 1) >= 0);
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_INFO, intro);
diff --git a/src/test/test-format-util.c b/src/test/test-format-util.c
new file mode 100644
index 0000000..3063509
--- /dev/null
+++ b/src/test/test-format-util.c
@@ -0,0 +1,61 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "format-util.h"
+#include "macro.h"
+#include "string-util.h"
+#include "tests.h"
+#include "uchar.h"
+
+/* Do some basic checks on STRLEN() and DECIMAL_STR_MAX() */
+assert_cc(STRLEN("") == 0);
+assert_cc(STRLEN("a") == 1);
+assert_cc(STRLEN("123") == 3);
+assert_cc(STRLEN(u8"") == 0);
+assert_cc(STRLEN(u8"a") == 1);
+assert_cc(STRLEN(u8"123") == 3);
+assert_cc(STRLEN(u"") == 0);
+assert_cc(STRLEN(u"a") == sizeof(char16_t));
+assert_cc(STRLEN(u"123") == 3 * sizeof(char16_t));
+assert_cc(STRLEN(U"") == 0);
+assert_cc(STRLEN(U"a") == sizeof(char32_t));
+assert_cc(STRLEN(U"123") == 3 * sizeof(char32_t));
+assert_cc(STRLEN(L"") == 0);
+assert_cc(STRLEN(L"a") == sizeof(wchar_t));
+assert_cc(STRLEN(L"123") == 3 * sizeof(wchar_t));
+assert_cc(DECIMAL_STR_MAX(uint8_t) == STRLEN("255")+1);
+assert_cc(DECIMAL_STR_MAX(int8_t) == STRLEN("-127")+1);
+assert_cc(DECIMAL_STR_MAX(uint64_t) == STRLEN("18446744073709551615")+1);
+assert_cc(DECIMAL_STR_MAX(int64_t) == CONST_MAX(STRLEN("-9223372036854775808"), STRLEN("9223372036854775807"))+1);
+assert_cc(DECIMAL_STR_MAX(signed char) == STRLEN("-127")+1);
+assert_cc(DECIMAL_STR_MAX(unsigned char) == STRLEN("255")+1);
+assert_cc(CONST_MAX(DECIMAL_STR_MAX(int8_t), STRLEN("xxx")) == 5);
+
+static void test_format_bytes_one(uint64_t val, bool trailing_B, const char *iec_with_p, const char *iec_without_p,
+ const char *si_with_p, const char *si_without_p) {
+ char buf[FORMAT_BYTES_MAX];
+
+ assert_se(streq_ptr(format_bytes_full(buf, sizeof buf, val, FORMAT_BYTES_USE_IEC | FORMAT_BYTES_BELOW_POINT | (trailing_B ? FORMAT_BYTES_TRAILING_B : 0)), iec_with_p));
+ assert_se(streq_ptr(format_bytes_full(buf, sizeof buf, val, FORMAT_BYTES_USE_IEC | (trailing_B ? FORMAT_BYTES_TRAILING_B : 0)), iec_without_p));
+ assert_se(streq_ptr(format_bytes_full(buf, sizeof buf, val, FORMAT_BYTES_BELOW_POINT | (trailing_B ? FORMAT_BYTES_TRAILING_B : 0)), si_with_p));
+ assert_se(streq_ptr(format_bytes_full(buf, sizeof buf, val, trailing_B ? FORMAT_BYTES_TRAILING_B : 0), si_without_p));
+}
+
+TEST(format_bytes) {
+ test_format_bytes_one(900, true, "900B", "900B", "900B", "900B");
+ test_format_bytes_one(900, false, "900", "900", "900", "900");
+ test_format_bytes_one(1023, true, "1023B", "1023B", "1.0K", "1K");
+ test_format_bytes_one(1023, false, "1023", "1023", "1.0K", "1K");
+ test_format_bytes_one(1024, true, "1.0K", "1K", "1.0K", "1K");
+ test_format_bytes_one(1024, false, "1.0K", "1K", "1.0K", "1K");
+ test_format_bytes_one(1100, true, "1.0K", "1K", "1.1K", "1K");
+ test_format_bytes_one(1500, true, "1.4K", "1K", "1.5K", "1K");
+ test_format_bytes_one(UINT64_C(3)*1024*1024, true, "3.0M", "3M", "3.1M", "3M");
+ test_format_bytes_one(UINT64_C(3)*1024*1024*1024, true, "3.0G", "3G", "3.2G", "3G");
+ test_format_bytes_one(UINT64_C(3)*1024*1024*1024*1024, true, "3.0T", "3T", "3.2T", "3T");
+ test_format_bytes_one(UINT64_C(3)*1024*1024*1024*1024*1024, true, "3.0P", "3P", "3.3P", "3P");
+ test_format_bytes_one(UINT64_C(3)*1024*1024*1024*1024*1024*1024, true, "3.0E", "3E", "3.4E", "3E");
+ test_format_bytes_one(UINT64_MAX, true, NULL, NULL, NULL, NULL);
+ test_format_bytes_one(UINT64_MAX, false, NULL, NULL, NULL, NULL);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-fs-util.c b/src/test/test-fs-util.c
new file mode 100644
index 0000000..ef335b4
--- /dev/null
+++ b/src/test/test-fs-util.c
@@ -0,0 +1,796 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/file.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "copy.h"
+#include "dirent-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "macro.h"
+#include "mkdir.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "random-util.h"
+#include "rm-rf.h"
+#include "stat-util.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "sync-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+#include "umask-util.h"
+#include "user-util.h"
+#include "virt.h"
+
+static const char *arg_test_dir = NULL;
+
+TEST(readlink_and_make_absolute) {
+ const char *tempdir, *name, *name2, *name_alias;
+ _cleanup_free_ char *r1 = NULL, *r2 = NULL, *pwd = NULL;
+
+ tempdir = strjoina(arg_test_dir ?: "/tmp", "/test-readlink_and_make_absolute");
+ name = strjoina(tempdir, "/original");
+ name2 = "test-readlink_and_make_absolute/original";
+ name_alias = strjoina(arg_test_dir ?: "/tmp", "/test-readlink_and_make_absolute-alias");
+
+ assert_se(mkdir_safe(tempdir, 0755, getuid(), getgid(), MKDIR_WARN_MODE) >= 0);
+ assert_se(touch(name) >= 0);
+
+ if (symlink(name, name_alias) < 0) {
+ assert_se(IN_SET(errno, EINVAL, ENOSYS, ENOTTY, EPERM));
+ log_tests_skipped_errno(errno, "symlink() not possible");
+ } else {
+ assert_se(readlink_and_make_absolute(name_alias, &r1) >= 0);
+ assert_se(streq(r1, name));
+ assert_se(unlink(name_alias) >= 0);
+
+ assert_se(safe_getcwd(&pwd) >= 0);
+
+ assert_se(chdir(tempdir) >= 0);
+ assert_se(symlink(name2, name_alias) >= 0);
+ assert_se(readlink_and_make_absolute(name_alias, &r2) >= 0);
+ assert_se(streq(r2, name));
+ assert_se(unlink(name_alias) >= 0);
+
+ assert_se(chdir(pwd) >= 0);
+ }
+
+ assert_se(rm_rf(tempdir, REMOVE_ROOT|REMOVE_PHYSICAL) >= 0);
+}
+
+TEST(get_files_in_directory) {
+ _cleanup_strv_free_ char **l = NULL, **t = NULL;
+
+ assert_se(get_files_in_directory(arg_test_dir ?: "/tmp", &l) >= 0);
+ assert_se(get_files_in_directory(".", &t) >= 0);
+ assert_se(get_files_in_directory(".", NULL) >= 0);
+}
+
+TEST(var_tmp) {
+ _cleanup_free_ char *tmpdir_backup = NULL, *temp_backup = NULL, *tmp_backup = NULL;
+ const char *tmp_dir = NULL, *t;
+
+ t = getenv("TMPDIR");
+ if (t) {
+ tmpdir_backup = strdup(t);
+ assert_se(tmpdir_backup);
+ }
+
+ t = getenv("TEMP");
+ if (t) {
+ temp_backup = strdup(t);
+ assert_se(temp_backup);
+ }
+
+ t = getenv("TMP");
+ if (t) {
+ tmp_backup = strdup(t);
+ assert_se(tmp_backup);
+ }
+
+ assert_se(unsetenv("TMPDIR") >= 0);
+ assert_se(unsetenv("TEMP") >= 0);
+ assert_se(unsetenv("TMP") >= 0);
+
+ assert_se(var_tmp_dir(&tmp_dir) >= 0);
+ assert_se(streq(tmp_dir, "/var/tmp"));
+
+ assert_se(setenv("TMPDIR", "/tmp", true) >= 0);
+ assert_se(streq(getenv("TMPDIR"), "/tmp"));
+
+ assert_se(var_tmp_dir(&tmp_dir) >= 0);
+ assert_se(streq(tmp_dir, "/tmp"));
+
+ assert_se(setenv("TMPDIR", "/88_does_not_exist_88", true) >= 0);
+ assert_se(streq(getenv("TMPDIR"), "/88_does_not_exist_88"));
+
+ assert_se(var_tmp_dir(&tmp_dir) >= 0);
+ assert_se(streq(tmp_dir, "/var/tmp"));
+
+ if (tmpdir_backup) {
+ assert_se(setenv("TMPDIR", tmpdir_backup, true) >= 0);
+ assert_se(streq(getenv("TMPDIR"), tmpdir_backup));
+ }
+
+ if (temp_backup) {
+ assert_se(setenv("TEMP", temp_backup, true) >= 0);
+ assert_se(streq(getenv("TEMP"), temp_backup));
+ }
+
+ if (tmp_backup) {
+ assert_se(setenv("TMP", tmp_backup, true) >= 0);
+ assert_se(streq(getenv("TMP"), tmp_backup));
+ }
+}
+
+TEST(dot_or_dot_dot) {
+ assert_se(!dot_or_dot_dot(NULL));
+ assert_se(!dot_or_dot_dot(""));
+ assert_se(!dot_or_dot_dot("xxx"));
+ assert_se(dot_or_dot_dot("."));
+ assert_se(dot_or_dot_dot(".."));
+ assert_se(!dot_or_dot_dot(".foo"));
+ assert_se(!dot_or_dot_dot("..foo"));
+}
+
+TEST(access_fd) {
+ _cleanup_(rmdir_and_freep) char *p = NULL;
+ _cleanup_close_ int fd = -EBADF;
+ const char *a;
+
+ a = strjoina(arg_test_dir ?: "/tmp", "/access-fd.XXXXXX");
+ assert_se(mkdtemp_malloc(a, &p) >= 0);
+
+ fd = open(p, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
+ assert_se(fd >= 0);
+
+ assert_se(access_fd(fd, R_OK) >= 0);
+ assert_se(access_fd(fd, F_OK) >= 0);
+ assert_se(access_fd(fd, W_OK) >= 0);
+
+ assert_se(fchmod(fd, 0000) >= 0);
+
+ assert_se(access_fd(fd, F_OK) >= 0);
+
+ if (geteuid() == 0) {
+ assert_se(access_fd(fd, R_OK) >= 0);
+ assert_se(access_fd(fd, W_OK) >= 0);
+ } else {
+ assert_se(access_fd(fd, R_OK) == -EACCES);
+ assert_se(access_fd(fd, W_OK) == -EACCES);
+ }
+}
+
+TEST(touch_file) {
+ uid_t test_uid, test_gid;
+ _cleanup_(rm_rf_physical_and_freep) char *p = NULL;
+ struct stat st;
+ const char *a;
+ usec_t test_mtime;
+ int r;
+
+ test_uid = geteuid() == 0 ? 65534 : getuid();
+ test_gid = geteuid() == 0 ? 65534 : getgid();
+
+ test_mtime = usec_sub_unsigned(now(CLOCK_REALTIME), USEC_PER_WEEK);
+
+ a = strjoina(arg_test_dir ?: "/dev/shm", "/touch-file-XXXXXX");
+ assert_se(mkdtemp_malloc(a, &p) >= 0);
+
+ a = strjoina(p, "/regular");
+ r = touch_file(a, false, test_mtime, test_uid, test_gid, 0640);
+ if (r < 0) {
+ assert_se(IN_SET(r, -EINVAL, -ENOSYS, -ENOTTY, -EPERM));
+ log_tests_skipped_errno(errno, "touch_file() not possible");
+ return;
+ }
+
+ assert_se(lstat(a, &st) >= 0);
+ assert_se(st.st_uid == test_uid);
+ assert_se(st.st_gid == test_gid);
+ assert_se(S_ISREG(st.st_mode));
+ assert_se((st.st_mode & 0777) == 0640);
+ assert_se(timespec_load(&st.st_mtim) == test_mtime);
+
+ a = strjoina(p, "/dir");
+ assert_se(mkdir(a, 0775) >= 0);
+ assert_se(touch_file(a, false, test_mtime, test_uid, test_gid, 0640) >= 0);
+ assert_se(lstat(a, &st) >= 0);
+ assert_se(st.st_uid == test_uid);
+ assert_se(st.st_gid == test_gid);
+ assert_se(S_ISDIR(st.st_mode));
+ assert_se((st.st_mode & 0777) == 0640);
+ assert_se(timespec_load(&st.st_mtim) == test_mtime);
+
+ a = strjoina(p, "/fifo");
+ assert_se(mkfifo(a, 0775) >= 0);
+ assert_se(touch_file(a, false, test_mtime, test_uid, test_gid, 0640) >= 0);
+ assert_se(lstat(a, &st) >= 0);
+ assert_se(st.st_uid == test_uid);
+ assert_se(st.st_gid == test_gid);
+ assert_se(S_ISFIFO(st.st_mode));
+ assert_se((st.st_mode & 0777) == 0640);
+ assert_se(timespec_load(&st.st_mtim) == test_mtime);
+
+ a = strjoina(p, "/sock");
+ assert_se(mknod(a, 0775 | S_IFSOCK, 0) >= 0);
+ assert_se(touch_file(a, false, test_mtime, test_uid, test_gid, 0640) >= 0);
+ assert_se(lstat(a, &st) >= 0);
+ assert_se(st.st_uid == test_uid);
+ assert_se(st.st_gid == test_gid);
+ assert_se(S_ISSOCK(st.st_mode));
+ assert_se((st.st_mode & 0777) == 0640);
+ assert_se(timespec_load(&st.st_mtim) == test_mtime);
+
+ if (geteuid() == 0) {
+ a = strjoina(p, "/bdev");
+ r = mknod(a, 0775 | S_IFBLK, makedev(0, 0));
+ if (r < 0 && errno == EPERM && detect_container() > 0) {
+ log_notice("Running in unprivileged container? Skipping remaining tests in %s", __func__);
+ return;
+ }
+ assert_se(r >= 0);
+ assert_se(touch_file(a, false, test_mtime, test_uid, test_gid, 0640) >= 0);
+ assert_se(lstat(a, &st) >= 0);
+ assert_se(st.st_uid == test_uid);
+ assert_se(st.st_gid == test_gid);
+ assert_se(S_ISBLK(st.st_mode));
+ assert_se((st.st_mode & 0777) == 0640);
+ assert_se(timespec_load(&st.st_mtim) == test_mtime);
+
+ a = strjoina(p, "/cdev");
+ assert_se(mknod(a, 0775 | S_IFCHR, makedev(0, 0)) >= 0);
+ assert_se(touch_file(a, false, test_mtime, test_uid, test_gid, 0640) >= 0);
+ assert_se(lstat(a, &st) >= 0);
+ assert_se(st.st_uid == test_uid);
+ assert_se(st.st_gid == test_gid);
+ assert_se(S_ISCHR(st.st_mode));
+ assert_se((st.st_mode & 0777) == 0640);
+ assert_se(timespec_load(&st.st_mtim) == test_mtime);
+ }
+
+ a = strjoina(p, "/lnk");
+ assert_se(symlink("target", a) >= 0);
+ assert_se(touch_file(a, false, test_mtime, test_uid, test_gid, 0640) >= 0);
+ assert_se(lstat(a, &st) >= 0);
+ assert_se(st.st_uid == test_uid);
+ assert_se(st.st_gid == test_gid);
+ assert_se(S_ISLNK(st.st_mode));
+ assert_se(timespec_load(&st.st_mtim) == test_mtime);
+}
+
+TEST(unlinkat_deallocate) {
+ _cleanup_free_ char *p = NULL;
+ _cleanup_close_ int fd = -EBADF;
+ struct stat st;
+
+ assert_se(tempfn_random_child(arg_test_dir, "unlink-deallocation", &p) >= 0);
+
+ fd = open(p, O_WRONLY|O_CLOEXEC|O_CREAT|O_EXCL, 0600);
+ assert_se(fd >= 0);
+
+ assert_se(write(fd, "hallo\n", 6) == 6);
+
+ assert_se(fstat(fd, &st) >= 0);
+ assert_se(st.st_size == 6);
+ assert_se(st.st_blocks > 0);
+ assert_se(st.st_nlink == 1);
+
+ assert_se(unlinkat_deallocate(AT_FDCWD, p, UNLINK_ERASE) >= 0);
+
+ assert_se(fstat(fd, &st) >= 0);
+ assert_se(IN_SET(st.st_size, 0, 6)); /* depending on whether hole punching worked the size will be 6
+ (it worked) or 0 (we had to resort to truncation) */
+ assert_se(st.st_blocks == 0);
+ assert_se(st.st_nlink == 0);
+}
+
+TEST(fsync_directory_of_file) {
+ _cleanup_close_ int fd = -EBADF;
+
+ fd = open_tmpfile_unlinkable(arg_test_dir, O_RDWR);
+ assert_se(fd >= 0);
+
+ assert_se(fsync_directory_of_file(fd) >= 0);
+}
+
+TEST(rename_noreplace) {
+ static const char* const table[] = {
+ "/reg",
+ "/dir",
+ "/fifo",
+ "/socket",
+ "/symlink",
+ NULL
+ };
+
+ _cleanup_(rm_rf_physical_and_freep) char *z = NULL;
+ const char *j = NULL;
+
+ if (arg_test_dir)
+ j = strjoina(arg_test_dir, "/testXXXXXX");
+ assert_se(mkdtemp_malloc(j, &z) >= 0);
+
+ j = strjoina(z, table[0]);
+ assert_se(touch(j) >= 0);
+
+ j = strjoina(z, table[1]);
+ assert_se(mkdir(j, 0777) >= 0);
+
+ j = strjoina(z, table[2]);
+ (void) mkfifo(j, 0777);
+
+ j = strjoina(z, table[3]);
+ (void) mknod(j, S_IFSOCK | 0777, 0);
+
+ j = strjoina(z, table[4]);
+ (void) symlink("foobar", j);
+
+ STRV_FOREACH(a, table) {
+ _cleanup_free_ char *x = NULL, *y = NULL;
+
+ x = strjoin(z, *a);
+ assert_se(x);
+
+ if (access(x, F_OK) < 0) {
+ assert_se(errno == ENOENT);
+ continue;
+ }
+
+ STRV_FOREACH(b, table) {
+ _cleanup_free_ char *w = NULL;
+
+ w = strjoin(z, *b);
+ assert_se(w);
+
+ if (access(w, F_OK) < 0) {
+ assert_se(errno == ENOENT);
+ continue;
+ }
+
+ assert_se(rename_noreplace(AT_FDCWD, x, AT_FDCWD, w) == -EEXIST);
+ }
+
+ y = strjoin(z, "/somethingelse");
+ assert_se(y);
+
+ assert_se(rename_noreplace(AT_FDCWD, x, AT_FDCWD, y) >= 0);
+ assert_se(rename_noreplace(AT_FDCWD, y, AT_FDCWD, x) >= 0);
+ }
+}
+
+TEST(chmod_and_chown) {
+ _cleanup_(rm_rf_physical_and_freep) char *d = NULL;
+ struct stat st;
+ const char *p;
+
+ if (geteuid() != 0)
+ return;
+
+ BLOCK_WITH_UMASK(0000);
+
+ assert_se(mkdtemp_malloc(NULL, &d) >= 0);
+
+ p = strjoina(d, "/reg");
+ assert_se(mknod(p, S_IFREG | 0123, 0) >= 0);
+
+ assert_se(chmod_and_chown(p, S_IFREG | 0321, 1, 2) >= 0);
+ assert_se(chmod_and_chown(p, S_IFDIR | 0555, 3, 4) == -EINVAL);
+
+ assert_se(lstat(p, &st) >= 0);
+ assert_se(S_ISREG(st.st_mode));
+ assert_se((st.st_mode & 07777) == 0321);
+
+ p = strjoina(d, "/dir");
+ assert_se(mkdir(p, 0123) >= 0);
+
+ assert_se(chmod_and_chown(p, S_IFDIR | 0321, 1, 2) >= 0);
+ assert_se(chmod_and_chown(p, S_IFREG | 0555, 3, 4) == -EINVAL);
+
+ assert_se(lstat(p, &st) >= 0);
+ assert_se(S_ISDIR(st.st_mode));
+ assert_se((st.st_mode & 07777) == 0321);
+
+ p = strjoina(d, "/lnk");
+ assert_se(symlink("idontexist", p) >= 0);
+
+ assert_se(chmod_and_chown(p, S_IFLNK | 0321, 1, 2) >= 0);
+ assert_se(chmod_and_chown(p, S_IFREG | 0555, 3, 4) == -EINVAL);
+ assert_se(chmod_and_chown(p, S_IFDIR | 0555, 3, 4) == -EINVAL);
+
+ assert_se(lstat(p, &st) >= 0);
+ assert_se(S_ISLNK(st.st_mode));
+}
+
+static void create_binary_file(const char *p, const void *data, size_t l) {
+ _cleanup_close_ int fd = -EBADF;
+
+ fd = open(p, O_CREAT|O_WRONLY|O_EXCL|O_CLOEXEC, 0600);
+ assert_se(fd >= 0);
+ assert_se(write(fd, data, l) == (ssize_t) l);
+}
+
+TEST(conservative_rename) {
+ _cleanup_(unlink_and_freep) char *p = NULL;
+ _cleanup_free_ char *q = NULL;
+ size_t l = 16*1024 + random_u64() % (32 * 1024); /* some randomly sized buffer 16k…48k */
+ uint8_t buffer[l+1];
+
+ random_bytes(buffer, l);
+
+ assert_se(tempfn_random_child(NULL, NULL, &p) >= 0);
+ create_binary_file(p, buffer, l);
+
+ assert_se(tempfn_random_child(NULL, NULL, &q) >= 0);
+
+ /* Check that the hardlinked "copy" is detected */
+ assert_se(link(p, q) >= 0);
+ assert_se(conservative_renameat(AT_FDCWD, q, AT_FDCWD, p) == 0);
+ assert_se(access(q, F_OK) < 0 && errno == ENOENT);
+
+ /* Check that a manual copy is detected */
+ assert_se(copy_file(p, q, 0, MODE_INVALID, COPY_REFLINK) >= 0);
+ assert_se(conservative_renameat(AT_FDCWD, q, AT_FDCWD, p) == 0);
+ assert_se(access(q, F_OK) < 0 && errno == ENOENT);
+
+ /* Check that a manual new writeout is also detected */
+ create_binary_file(q, buffer, l);
+ assert_se(conservative_renameat(AT_FDCWD, q, AT_FDCWD, p) == 0);
+ assert_se(access(q, F_OK) < 0 && errno == ENOENT);
+
+ /* Check that a minimally changed version is detected */
+ buffer[47] = ~buffer[47];
+ create_binary_file(q, buffer, l);
+ assert_se(conservative_renameat(AT_FDCWD, q, AT_FDCWD, p) > 0);
+ assert_se(access(q, F_OK) < 0 && errno == ENOENT);
+
+ /* Check that this really is new updated version */
+ create_binary_file(q, buffer, l);
+ assert_se(conservative_renameat(AT_FDCWD, q, AT_FDCWD, p) == 0);
+ assert_se(access(q, F_OK) < 0 && errno == ENOENT);
+
+ /* Make sure we detect extended files */
+ buffer[l++] = 47;
+ create_binary_file(q, buffer, l);
+ assert_se(conservative_renameat(AT_FDCWD, q, AT_FDCWD, p) > 0);
+ assert_se(access(q, F_OK) < 0 && errno == ENOENT);
+
+ /* Make sure we detect truncated files */
+ l--;
+ create_binary_file(q, buffer, l);
+ assert_se(conservative_renameat(AT_FDCWD, q, AT_FDCWD, p) > 0);
+ assert_se(access(q, F_OK) < 0 && errno == ENOENT);
+}
+
+static void test_rmdir_parents_one(
+ const char *prefix,
+ const char *path,
+ const char *stop,
+ int expected,
+ const char *test_exist,
+ const char *test_nonexist_subdir) {
+
+ const char *p, *s;
+
+ log_debug("/* %s(%s, %s) */", __func__, path, stop);
+
+ p = strjoina(prefix, path);
+ s = strjoina(prefix, stop);
+
+ if (expected >= 0)
+ assert_se(mkdir_parents(p, 0700) >= 0);
+
+ assert_se(rmdir_parents(p, s) == expected);
+
+ if (expected >= 0) {
+ const char *e, *f;
+
+ e = strjoina(prefix, test_exist);
+ f = strjoina(e, test_nonexist_subdir);
+
+ assert_se(access(e, F_OK) >= 0);
+ assert_se(access(f, F_OK) < 0);
+ }
+}
+
+TEST(rmdir_parents) {
+ char *temp;
+
+ temp = strjoina(arg_test_dir ?: "/tmp", "/test-rmdir.XXXXXX");
+ assert_se(mkdtemp(temp));
+
+ test_rmdir_parents_one(temp, "/aaa/../hoge/foo", "/hoge/foo", -EINVAL, NULL, NULL);
+ test_rmdir_parents_one(temp, "/aaa/bbb/ccc", "/hoge/../aaa", -EINVAL, NULL, NULL);
+
+ test_rmdir_parents_one(temp, "/aaa/bbb/ccc/ddd/eee", "/aaa/bbb/ccc/ddd", 0, "/aaa/bbb/ccc/ddd", "/eee");
+ test_rmdir_parents_one(temp, "/aaa/bbb/ccc/ddd/eee", "/aaa/bbb/ccc", 0, "/aaa/bbb/ccc", "/ddd");
+ test_rmdir_parents_one(temp, "/aaa/bbb/ccc/ddd/eee", "/aaa/bbb", 0, "/aaa/bbb", "/ccc");
+ test_rmdir_parents_one(temp, "/aaa/bbb/ccc/ddd/eee", "/aaa", 0, "/aaa", "/bbb");
+ test_rmdir_parents_one(temp, "/aaa/bbb/ccc/ddd/eee", "/", 0, "/", "/aaa");
+
+ test_rmdir_parents_one(temp, "/aaa/bbb/ccc/ddd/eee", "/aaa/hoge/foo", 0, "/aaa", "/bbb");
+ test_rmdir_parents_one(temp, "/aaa////bbb/.//ccc//ddd/eee///./.", "///././aaa/.", 0, "/aaa", "/bbb");
+
+ assert_se(rm_rf(temp, REMOVE_ROOT|REMOVE_PHYSICAL) >= 0);
+}
+
+static void test_parse_cifs_service_one(const char *f, const char *h, const char *s, const char *d, int ret) {
+ _cleanup_free_ char *a = NULL, *b = NULL, *c = NULL;
+
+ assert_se(parse_cifs_service(f, &a, &b, &c) == ret);
+ assert_se(streq_ptr(a, h));
+ assert_se(streq_ptr(b, s));
+ assert_se(streq_ptr(c, d));
+}
+
+TEST(parse_cifs_service) {
+ test_parse_cifs_service_one("//foo/bar/baz", "foo", "bar", "baz", 0);
+ test_parse_cifs_service_one("\\\\foo\\bar\\baz", "foo", "bar", "baz", 0);
+ test_parse_cifs_service_one("//foo/bar", "foo", "bar", NULL, 0);
+ test_parse_cifs_service_one("\\\\foo\\bar", "foo", "bar", NULL, 0);
+ test_parse_cifs_service_one("//foo/bar/baz/uuu", "foo", "bar", "baz/uuu", 0);
+ test_parse_cifs_service_one("\\\\foo\\bar\\baz\\uuu", "foo", "bar", "baz/uuu", 0);
+
+ test_parse_cifs_service_one(NULL, NULL, NULL, NULL, -EINVAL);
+ test_parse_cifs_service_one("", NULL, NULL, NULL, -EINVAL);
+ test_parse_cifs_service_one("abc", NULL, NULL, NULL, -EINVAL);
+ test_parse_cifs_service_one("abc/cde/efg", NULL, NULL, NULL, -EINVAL);
+ test_parse_cifs_service_one("//foo/bar/baz/..", NULL, NULL, NULL, -EINVAL);
+ test_parse_cifs_service_one("//foo///", NULL, NULL, NULL, -EINVAL);
+ test_parse_cifs_service_one("//foo/.", NULL, NULL, NULL, -EINVAL);
+ test_parse_cifs_service_one("//foo/a/.", NULL, NULL, NULL, -EINVAL);
+ test_parse_cifs_service_one("//./a", NULL, NULL, NULL, -EINVAL);
+}
+
+TEST(open_mkdir_at) {
+ _cleanup_close_ int fd = -EBADF, subdir_fd = -EBADF, subsubdir_fd = -EBADF;
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ struct stat sta, stb;
+
+ assert_se(open_mkdir_at(AT_FDCWD, "/", O_EXCL|O_CLOEXEC, 0) == -EEXIST);
+ assert_se(open_mkdir_at(AT_FDCWD, ".", O_EXCL|O_CLOEXEC, 0) == -EEXIST);
+
+ fd = open_mkdir_at(AT_FDCWD, "/", O_CLOEXEC, 0);
+ assert_se(fd >= 0);
+ assert_se(stat("/", &sta) >= 0);
+ assert_se(fstat(fd, &stb) >= 0);
+ assert_se(stat_inode_same(&sta, &stb));
+ fd = safe_close(fd);
+
+ fd = open_mkdir_at(AT_FDCWD, ".", O_CLOEXEC, 0);
+ assert_se(stat(".", &sta) >= 0);
+ assert_se(fstat(fd, &stb) >= 0);
+ assert_se(stat_inode_same(&sta, &stb));
+ fd = safe_close(fd);
+
+ assert_se(open_mkdir_at(AT_FDCWD, "/proc", O_EXCL|O_CLOEXEC, 0) == -EEXIST);
+
+ fd = open_mkdir_at(AT_FDCWD, "/proc", O_CLOEXEC, 0);
+ assert_se(fd >= 0);
+ fd = safe_close(fd);
+
+ assert_se(open_mkdir_at(AT_FDCWD, "/bin/sh", O_EXCL|O_CLOEXEC, 0) == -EEXIST);
+ assert_se(open_mkdir_at(AT_FDCWD, "/bin/sh", O_CLOEXEC, 0) == -EEXIST);
+
+ assert_se(mkdtemp_malloc(NULL, &t) >= 0);
+
+ assert_se(open_mkdir_at(AT_FDCWD, t, O_EXCL|O_CLOEXEC, 0) == -EEXIST);
+ assert_se(open_mkdir_at(AT_FDCWD, t, O_PATH|O_EXCL|O_CLOEXEC, 0) == -EEXIST);
+
+ fd = open_mkdir_at(AT_FDCWD, t, O_CLOEXEC, 0000);
+ assert_se(fd >= 0);
+ fd = safe_close(fd);
+
+ fd = open_mkdir_at(AT_FDCWD, t, O_PATH|O_CLOEXEC, 0000);
+ assert_se(fd >= 0);
+
+ subdir_fd = open_mkdir_at(fd, "xxx", O_PATH|O_EXCL|O_CLOEXEC, 0700);
+ assert_se(subdir_fd >= 0);
+
+ assert_se(open_mkdir_at(fd, "xxx", O_PATH|O_EXCL|O_CLOEXEC, 0) == -EEXIST);
+
+ subsubdir_fd = open_mkdir_at(subdir_fd, "yyy", O_EXCL|O_CLOEXEC, 0700);
+ assert_se(subsubdir_fd >= 0);
+ subsubdir_fd = safe_close(subsubdir_fd);
+
+ assert_se(open_mkdir_at(subdir_fd, "yyy", O_EXCL|O_CLOEXEC, 0) == -EEXIST);
+
+ assert_se(open_mkdir_at(fd, "xxx/yyy", O_EXCL|O_CLOEXEC, 0) == -EEXIST);
+
+ subsubdir_fd = open_mkdir_at(fd, "xxx/yyy", O_CLOEXEC, 0700);
+ assert_se(subsubdir_fd >= 0);
+}
+
+TEST(openat_report_new) {
+ _cleanup_free_ char *j = NULL;
+ _cleanup_(rm_rf_physical_and_freep) char *d = NULL;
+ _cleanup_close_ int fd = -EBADF;
+ bool b;
+
+ assert_se(mkdtemp_malloc(NULL, &d) >= 0);
+
+ j = path_join(d, "test");
+ assert_se(j);
+
+ fd = openat_report_new(AT_FDCWD, j, O_RDWR|O_CREAT, 0666, &b);
+ assert_se(fd >= 0);
+ fd = safe_close(fd);
+ assert_se(b);
+
+ fd = openat_report_new(AT_FDCWD, j, O_RDWR|O_CREAT, 0666, &b);
+ assert_se(fd >= 0);
+ fd = safe_close(fd);
+ assert_se(!b);
+
+ fd = openat_report_new(AT_FDCWD, j, O_RDWR|O_CREAT, 0666, &b);
+ assert_se(fd >= 0);
+ fd = safe_close(fd);
+ assert_se(!b);
+
+ assert_se(unlink(j) >= 0);
+
+ fd = openat_report_new(AT_FDCWD, j, O_RDWR|O_CREAT, 0666, &b);
+ assert_se(fd >= 0);
+ fd = safe_close(fd);
+ assert_se(b);
+
+ fd = openat_report_new(AT_FDCWD, j, O_RDWR|O_CREAT, 0666, &b);
+ assert_se(fd >= 0);
+ fd = safe_close(fd);
+ assert_se(!b);
+
+ assert_se(unlink(j) >= 0);
+
+ fd = openat_report_new(AT_FDCWD, j, O_RDWR|O_CREAT, 0666, NULL);
+ assert_se(fd >= 0);
+ fd = safe_close(fd);
+
+ fd = openat_report_new(AT_FDCWD, j, O_RDWR|O_CREAT, 0666, &b);
+ assert_se(fd >= 0);
+ fd = safe_close(fd);
+ assert_se(!b);
+
+ fd = openat_report_new(AT_FDCWD, j, O_RDWR, 0666, &b);
+ assert_se(fd >= 0);
+ fd = safe_close(fd);
+ assert_se(!b);
+
+ fd = openat_report_new(AT_FDCWD, j, O_RDWR|O_CREAT|O_EXCL, 0666, &b);
+ assert_se(fd == -EEXIST);
+
+ assert_se(unlink(j) >= 0);
+
+ fd = openat_report_new(AT_FDCWD, j, O_RDWR, 0666, &b);
+ assert_se(fd == -ENOENT);
+
+ fd = openat_report_new(AT_FDCWD, j, O_RDWR|O_CREAT|O_EXCL, 0666, &b);
+ assert_se(fd >= 0);
+ fd = safe_close(fd);
+ assert_se(b);
+}
+
+TEST(xopenat) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ _cleanup_close_ int tfd = -EBADF, fd = -EBADF, fd2 = -EBADF;
+
+ assert_se((tfd = mkdtemp_open(NULL, 0, &t)) >= 0);
+
+ /* Test that xopenat() creates directories if O_DIRECTORY is specified. */
+
+ assert_se((fd = xopenat(tfd, "abc", O_DIRECTORY|O_CREAT|O_EXCL|O_CLOEXEC, 0, 0755)) >= 0);
+ assert_se((fd_verify_directory(fd) >= 0));
+ fd = safe_close(fd);
+
+ assert_se(xopenat(tfd, "abc", O_DIRECTORY|O_CREAT|O_EXCL|O_CLOEXEC, 0, 0755) == -EEXIST);
+
+ assert_se((fd = xopenat(tfd, "abc", O_DIRECTORY|O_CREAT|O_CLOEXEC, 0, 0755)) >= 0);
+ assert_se((fd_verify_directory(fd) >= 0));
+ fd = safe_close(fd);
+
+ /* Test that xopenat() creates regular files if O_DIRECTORY is not specified. */
+
+ assert_se((fd = xopenat(tfd, "def", O_CREAT|O_EXCL|O_CLOEXEC, 0, 0644)) >= 0);
+ assert_se(fd_verify_regular(fd) >= 0);
+ fd = safe_close(fd);
+
+ /* Test that we can reopen an existing fd with xopenat() by specifying an empty path. */
+
+ assert_se((fd = xopenat(tfd, "def", O_PATH|O_CLOEXEC, 0, 0)) >= 0);
+ assert_se((fd2 = xopenat(fd, "", O_RDWR|O_CLOEXEC, 0, 0644)) >= 0);
+}
+
+TEST(xopenat_lock) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ _cleanup_close_ int tfd = -EBADF, fd = -EBADF;
+ siginfo_t si;
+
+ assert_se((tfd = mkdtemp_open(NULL, 0, &t)) >= 0);
+
+ /* Test that we can acquire an exclusive lock on a directory in one process, remove the directory,
+ * and close the file descriptor and still properly create the directory and acquire the lock in
+ * another process. */
+
+ fd = xopenat_lock(tfd, "abc", O_CREAT|O_DIRECTORY|O_CLOEXEC, 0, 0755, LOCK_BSD, LOCK_EX);
+ assert_se(fd >= 0);
+ assert_se(faccessat(tfd, "abc", F_OK, 0) >= 0);
+ assert_se(fd_verify_directory(fd) >= 0);
+ assert_se(xopenat_lock(tfd, "abc", O_DIRECTORY|O_CLOEXEC, 0, 0755, LOCK_BSD, LOCK_EX|LOCK_NB) == -EAGAIN);
+
+ pid_t pid = fork();
+ assert_se(pid >= 0);
+
+ if (pid == 0) {
+ safe_close(fd);
+
+ fd = xopenat_lock(tfd, "abc", O_CREAT|O_DIRECTORY|O_CLOEXEC, 0, 0755, LOCK_BSD, LOCK_EX);
+ assert_se(fd >= 0);
+ assert_se(faccessat(tfd, "abc", F_OK, 0) >= 0);
+ assert_se(fd_verify_directory(fd) >= 0);
+ assert_se(xopenat_lock(tfd, "abc", O_DIRECTORY|O_CLOEXEC, 0, 0755, LOCK_BSD, LOCK_EX|LOCK_NB) == -EAGAIN);
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ /* We need to give the child process some time to get past the xopenat() call in xopenat_lock() and
+ * block in the call to lock_generic() waiting for the lock to become free. We can't modify
+ * xopenat_lock() to signal an eventfd to let us know when that has happened, so we just sleep for a
+ * little and assume that's enough time for the child process to get along far enough. It doesn't
+ * matter if it doesn't get far enough, in that case we just won't trigger the fallback logic in
+ * xopenat_lock(), but the test will still succeed. */
+ assert_se(usleep_safe(20 * USEC_PER_MSEC) >= 0);
+
+ assert_se(unlinkat(tfd, "abc", AT_REMOVEDIR) >= 0);
+ fd = safe_close(fd);
+
+ assert_se(wait_for_terminate(pid, &si) >= 0);
+ assert_se(si.si_code == CLD_EXITED);
+
+ assert_se(xopenat_lock(tfd, "abc", 0, 0, 0755, LOCK_POSIX, LOCK_EX) == -EBADF);
+ assert_se(xopenat_lock(tfd, "def", O_DIRECTORY, 0, 0755, LOCK_POSIX, LOCK_EX) == -EBADF);
+}
+
+static int intro(void) {
+ arg_test_dir = saved_argv[1];
+ return EXIT_SUCCESS;
+}
+
+TEST(readlinkat_malloc) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ _cleanup_close_ int tfd = -EBADF, fd = -EBADF;
+ _cleanup_free_ char *p = NULL, *q = NULL;
+ const char *expect = "hgoehogefoobar";
+
+ tfd = mkdtemp_open(NULL, O_PATH, &t);
+ assert_se(tfd >= 0);
+
+ assert_se(symlinkat(expect, tfd, "linkname") >= 0);
+
+ assert_se(readlinkat_malloc(tfd, "linkname", &p) >= 0);
+ assert_se(streq(p, expect));
+ p = mfree(p);
+
+ fd = openat(tfd, "linkname", O_PATH | O_NOFOLLOW | O_CLOEXEC);
+ assert_se(fd >= 0);
+ assert_se(readlinkat_malloc(fd, NULL, &p) >= 0);
+ assert_se(streq(p, expect));
+ p = mfree(p);
+ assert_se(readlinkat_malloc(fd, "", &p) >= 0);
+ assert_se(streq(p, expect));
+ p = mfree(p);
+ fd = safe_close(fd);
+
+ assert_se(q = path_join(t, "linkname"));
+ assert_se(readlinkat_malloc(AT_FDCWD, q, &p) >= 0);
+ assert_se(streq(p, expect));
+ p = mfree(p);
+ assert_se(readlinkat_malloc(INT_MAX, q, &p) >= 0);
+ assert_se(streq(p, expect));
+ p = mfree(p);
+ q = mfree(q);
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_INFO, intro);
diff --git a/src/test/test-fstab-util.c b/src/test/test-fstab-util.c
new file mode 100644
index 0000000..89365b0
--- /dev/null
+++ b/src/test/test-fstab-util.c
@@ -0,0 +1,192 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+
+#include "alloc-util.h"
+#include "fstab-util.h"
+#include "log.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+
+/*
+int fstab_filter_options(
+ const char *opts,
+ const char *names,
+ const char **ret_namefound,
+ const char **ret_value,
+ const char **ret_values,
+ char **ret_filtered);
+*/
+
+static void do_fstab_filter_options(const char *opts,
+ const char *remove,
+ int r_expected,
+ int r_values_expected,
+ const char *name_expected,
+ const char *value_expected,
+ const char *values_expected,
+ const char *filtered_expected) {
+ int r;
+ const char *name;
+ _cleanup_free_ char *value = NULL, *filtered = NULL, *joined = NULL;
+ _cleanup_strv_free_ char **values = NULL;
+
+ /* test mode which returns the last value */
+
+ r = fstab_filter_options(opts, remove, &name, &value, NULL, &filtered);
+ log_info("1: \"%s\" → %d, \"%s\", \"%s\", \"%s\", expected %d, \"%s\", \"%s\", \"%s\"",
+ opts, r, strnull(name), value, filtered,
+ r_expected, strnull(name_expected), strnull(value_expected), filtered_expected ?: opts);
+ assert_se(r == r_expected);
+ assert_se(streq_ptr(name, name_expected));
+ assert_se(streq_ptr(value, value_expected));
+ assert_se(streq_ptr(filtered, filtered_expected ?: opts));
+
+ /* test mode which returns all the values */
+
+ r = fstab_filter_options(opts, remove, &name, NULL, &values, NULL);
+ assert_se(joined = strv_join(values, ":"));
+ log_info("2: \"%s\" → %d, \"%s\", \"%s\", expected %d, \"%s\", \"%s\"",
+ opts, r, strnull(name), joined,
+ r_values_expected, strnull(name_expected), strnull(values_expected));
+ assert_se(r == r_values_expected);
+ assert_se(streq_ptr(name, r_values_expected > 0 ? name_expected : NULL));
+ assert_se(streq_ptr(joined, values_expected));
+
+ /* also test the malloc-less mode */
+ r = fstab_filter_options(opts, remove, &name, NULL, NULL, NULL);
+ log_info("3: \"%s\" → %d, \"%s\", expected %d, \"%s\"\n-",
+ opts, r, strnull(name),
+ r_expected, strnull(name_expected));
+ assert_se(r == r_expected);
+ assert_se(streq_ptr(name, name_expected));
+}
+
+TEST(fstab_filter_options) {
+ do_fstab_filter_options("opt=0", "opt\0x-opt\0", 1, 1, "opt", "0", "0", "");
+ do_fstab_filter_options("opt=0", "x-opt\0opt\0", 1, 1, "opt", "0", "0", "");
+ do_fstab_filter_options("opt", "opt\0x-opt\0", 1, 0, "opt", NULL, "", "");
+ do_fstab_filter_options("opt", "x-opt\0opt\0", 1, 0, "opt", NULL, "", "");
+ do_fstab_filter_options("x-opt", "x-opt\0opt\0", 1, 0, "x-opt", NULL, "", "");
+
+ do_fstab_filter_options("opt=0,other", "opt\0x-opt\0", 1, 1, "opt", "0", "0", "other");
+ do_fstab_filter_options("opt=0,other", "x-opt\0opt\0", 1, 1, "opt", "0", "0", "other");
+ do_fstab_filter_options("opt,other", "opt\0x-opt\0", 1, 0, "opt", NULL, "", "other");
+ do_fstab_filter_options("opt,other", "x-opt\0opt\0", 1, 0, "opt", NULL, "", "other");
+ do_fstab_filter_options("x-opt,other", "opt\0x-opt\0", 1, 0, "x-opt", NULL, "", "other");
+
+ do_fstab_filter_options("opt=0\\,1,other", "opt\0x-opt\0", 1, 1, "opt", "0,1", "0,1", "other");
+ do_fstab_filter_options("opt=0,other,x-opt\\,foobar", "x-opt\0opt\0", 1, 1, "opt", "0", "0", "other,x-opt\\,foobar");
+ do_fstab_filter_options("opt,other,x-opt\\,part", "opt\0x-opt\0", 1, 0, "opt", NULL, "", "other,x-opt\\,part");
+ do_fstab_filter_options("opt,other,part\\,x-opt", "x-opt\0opt\0", 1, 0, "opt", NULL, "", "other,part\\,x-opt");
+ do_fstab_filter_options("opt,other\\,\\,\\,opt,x-part", "opt\0x-opt\0", 1, 0, "opt", NULL, "", "other\\,\\,\\,opt,x-part");
+
+ do_fstab_filter_options("opto=0,other", "opt\0x-opt\0", 0, 0, NULL, NULL, "", NULL);
+ do_fstab_filter_options("opto,other", "opt\0x-opt\0", 0, 0, NULL, NULL, "", NULL);
+ do_fstab_filter_options("x-opto,other", "opt\0x-opt\0", 0, 0, NULL, NULL, "", NULL);
+
+ do_fstab_filter_options("first,opt=0", "opt\0x-opt\0", 1, 1, "opt", "0", "0", "first");
+ do_fstab_filter_options("first=1,opt=0", "opt\0x-opt\0", 1, 1, "opt", "0", "0", "first=1");
+ do_fstab_filter_options("first,opt=", "opt\0x-opt\0", 1, 1, "opt", "", "", "first");
+ do_fstab_filter_options("first=1,opt", "opt\0x-opt\0", 1, 0, "opt", NULL, "", "first=1");
+ do_fstab_filter_options("first=1,x-opt", "opt\0x-opt\0", 1, 0, "x-opt", NULL, "", "first=1");
+
+ do_fstab_filter_options("first,opt=0,last=1", "opt\0x-opt\0", 1, 1, "opt", "0", "0", "first,last=1");
+ do_fstab_filter_options("first=1,opt=0,last=2", "x-opt\0opt\0", 1, 1, "opt", "0", "0", "first=1,last=2");
+ do_fstab_filter_options("first,opt,last", "opt\0", 1, 0, "opt", NULL, "", "first,last");
+ do_fstab_filter_options("first=1,opt,last", "x-opt\0opt\0", 1, 0, "opt", NULL, "", "first=1,last");
+ do_fstab_filter_options("first=,opt,last", "opt\0noopt\0", 1, 0, "opt", NULL, "", "first=,last");
+
+ /* check repeated options */
+ do_fstab_filter_options("first,opt=0,noopt=1,last=1", "opt\0noopt\0", 1, 1, "noopt", "1", "0:1", "first,last=1");
+ do_fstab_filter_options("first=1,opt=0,last=2,opt=1", "opt\0", 1, 1, "opt", "1", "0:1", "first=1,last=2");
+ do_fstab_filter_options("x-opt=0,x-opt=1", "opt\0x-opt\0", 1, 1, "x-opt", "1", "0:1", "");
+ do_fstab_filter_options("opt=0,x-opt=1", "opt\0x-opt\0", 1, 1, "x-opt", "1", "0:1", "");
+ do_fstab_filter_options("opt=0,opt=1,opt=,opt=,opt=2", "opt\0noopt\0", 1, 1, "opt", "2", "0:1:::2", "");
+
+ /* check that semicolons are not misinterpreted */
+ do_fstab_filter_options("opt=0;", "opt\0", 1, 1, "opt", "0;", "0;", "");
+ do_fstab_filter_options("opt;=0", "x-opt\0opt\0noopt\0x-noopt\0", 0, 0, NULL, NULL, "", NULL);
+ do_fstab_filter_options("opt;", "opt\0x-opt\0", 0, 0, NULL, NULL, "", NULL);
+
+ /* check that spaces are not misinterpreted */
+ do_fstab_filter_options("opt=0 ", "opt\0", 1, 1, "opt", "0 ", "0 ", "");
+ do_fstab_filter_options("opt =0", "x-opt\0opt\0noopt\0x-noopt\0", 0, 0, NULL, NULL, "", NULL);
+ do_fstab_filter_options(" opt ", "opt\0x-opt\0", 0, 0, NULL, NULL, "", NULL);
+
+ /* check function with NULL args */
+ do_fstab_filter_options(NULL, "opt\0", 0, 0, NULL, NULL, "", "");
+ do_fstab_filter_options("", "opt\0", 0, 0, NULL, NULL, "", "");
+
+ /* unnecessary comma separators */
+ do_fstab_filter_options("opt=x,,,,", "opt\0", 1, 1, "opt", "x", "x", "");
+ do_fstab_filter_options(",,,opt=x,,,,", "opt\0", 1, 1, "opt", "x", "x", "");
+
+ /* escaped characters */
+ do_fstab_filter_options("opt1=\\\\,opt2=\\xff", "opt1\0", 1, 1, "opt1", "\\", "\\", "opt2=\\xff");
+ do_fstab_filter_options("opt1=\\\\,opt2=\\xff", "opt2\0", 1, 1, "opt2", "\\xff", "\\xff", "opt1=\\");
+}
+
+TEST(fstab_find_pri) {
+ int pri = -1;
+
+ assert_se(fstab_find_pri("pri", &pri) == 0);
+ assert_se(pri == -1);
+
+ assert_se(fstab_find_pri("pri=11", &pri) == 1);
+ assert_se(pri == 11);
+
+ assert_se(fstab_find_pri("pri=-2", &pri) == 1);
+ assert_se(pri == -2);
+
+ assert_se(fstab_find_pri("opt,pri=12,opt", &pri) == 1);
+ assert_se(pri == 12);
+
+ assert_se(fstab_find_pri("opt,opt,pri=12,pri=13", &pri) == 1);
+ assert_se(pri == 13);
+}
+
+TEST(fstab_yes_no_option) {
+ assert_se(fstab_test_yes_no_option("nofail,fail,nofail", "nofail\0fail\0") == true);
+ assert_se(fstab_test_yes_no_option("nofail,nofail,fail", "nofail\0fail\0") == false);
+ assert_se(fstab_test_yes_no_option("abc,cde,afail", "nofail\0fail\0") == false);
+ assert_se(fstab_test_yes_no_option("nofail,fail=0,nofail=0", "nofail\0fail\0") == true);
+ assert_se(fstab_test_yes_no_option("nofail,nofail=0,fail=0", "nofail\0fail\0") == false);
+}
+
+TEST(fstab_node_to_udev_node) {
+ char *n;
+
+ n = fstab_node_to_udev_node("LABEL=applé/jack");
+ puts(n);
+ assert_se(streq(n, "/dev/disk/by-label/applé\\x2fjack"));
+ free(n);
+
+ n = fstab_node_to_udev_node("PARTLABEL=pinkié pie");
+ puts(n);
+ assert_se(streq(n, "/dev/disk/by-partlabel/pinkié\\x20pie"));
+ free(n);
+
+ n = fstab_node_to_udev_node("UUID=037b9d94-148e-4ee4-8d38-67bfe15bb535");
+ puts(n);
+ assert_se(streq(n, "/dev/disk/by-uuid/037b9d94-148e-4ee4-8d38-67bfe15bb535"));
+ free(n);
+
+ n = fstab_node_to_udev_node("PARTUUID=037b9d94-148e-4ee4-8d38-67bfe15bb535");
+ puts(n);
+ assert_se(streq(n, "/dev/disk/by-partuuid/037b9d94-148e-4ee4-8d38-67bfe15bb535"));
+ free(n);
+
+ n = fstab_node_to_udev_node("PONIES=awesome");
+ puts(n);
+ assert_se(streq(n, "PONIES=awesome"));
+ free(n);
+
+ n = fstab_node_to_udev_node("/dev/xda1");
+ puts(n);
+ assert_se(streq(n, "/dev/xda1"));
+ free(n);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-glob-util.c b/src/test/test-glob-util.c
new file mode 100644
index 0000000..9b3e73c
--- /dev/null
+++ b/src/test/test-glob-util.c
@@ -0,0 +1,137 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "dirent-util.h"
+#include "fs-util.h"
+#include "glob-util.h"
+#include "macro.h"
+#include "rm-rf.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+TEST(glob_first) {
+ char *first, name[] = "/tmp/test-glob_first.XXXXXX";
+ int fd = -EBADF;
+ int r;
+
+ fd = mkostemp_safe(name);
+ assert_se(fd >= 0);
+ close(fd);
+
+ r = glob_first("/tmp/test-glob_first*", &first);
+ assert_se(r == 1);
+ assert_se(streq(name, first));
+ first = mfree(first);
+
+ r = unlink(name);
+ assert_se(r == 0);
+ r = glob_first("/tmp/test-glob_first*", &first);
+ assert_se(r == 0);
+ assert_se(first == NULL);
+}
+
+TEST(glob_exists) {
+ char name[] = "/tmp/test-glob_exists.XXXXXX";
+ int fd = -EBADF;
+ int r;
+
+ fd = mkostemp_safe(name);
+ assert_se(fd >= 0);
+ close(fd);
+
+ r = glob_exists("/tmp/test-glob_exists*");
+ assert_se(r == 1);
+
+ r = unlink(name);
+ assert_se(r == 0);
+ r = glob_exists("/tmp/test-glob_exists*");
+ assert_se(r == 0);
+}
+
+static void closedir_wrapper(void* v) {
+ (void) closedir(v);
+}
+
+TEST(glob_no_dot) {
+ char template[] = "/tmp/test-glob-util.XXXXXXX";
+ const char *fn;
+
+ _cleanup_globfree_ glob_t g = {
+ .gl_closedir = closedir_wrapper,
+ .gl_readdir = (struct dirent *(*)(void *)) readdir_no_dot,
+ .gl_opendir = (void *(*)(const char *)) opendir,
+ .gl_lstat = lstat,
+ .gl_stat = stat,
+ };
+
+ int r;
+
+ assert_se(mkdtemp(template));
+
+ fn = strjoina(template, "/*");
+ r = glob(fn, GLOB_NOSORT|GLOB_BRACE|GLOB_ALTDIRFUNC, NULL, &g);
+ assert_se(r == GLOB_NOMATCH);
+
+ fn = strjoina(template, "/.*");
+ r = glob(fn, GLOB_NOSORT|GLOB_BRACE|GLOB_ALTDIRFUNC, NULL, &g);
+ assert_se(r == GLOB_NOMATCH);
+
+ (void) rm_rf(template, REMOVE_ROOT|REMOVE_PHYSICAL);
+}
+
+TEST(safe_glob) {
+ char template[] = "/tmp/test-glob-util.XXXXXXX";
+ const char *fn, *fn2, *fname;
+
+ _cleanup_globfree_ glob_t g = {};
+ int r;
+
+ assert_se(mkdtemp(template));
+
+ fn = strjoina(template, "/*");
+ r = safe_glob(fn, 0, &g);
+ assert_se(r == -ENOENT);
+
+ fn2 = strjoina(template, "/.*");
+ r = safe_glob(fn2, GLOB_NOSORT|GLOB_BRACE, &g);
+ assert_se(r == -ENOENT);
+
+ fname = strjoina(template, "/.foobar");
+ assert_se(touch(fname) == 0);
+
+ r = safe_glob(fn, 0, &g);
+ assert_se(r == -ENOENT);
+
+ r = safe_glob(fn2, GLOB_NOSORT|GLOB_BRACE, &g);
+ assert_se(r == 0);
+ assert_se(g.gl_pathc == 1);
+ assert_se(streq(g.gl_pathv[0], fname));
+ assert_se(g.gl_pathv[1] == NULL);
+
+ (void) rm_rf(template, REMOVE_ROOT|REMOVE_PHYSICAL);
+}
+
+static void test_glob_non_glob_prefix_one(const char *path, const char *expected) {
+ _cleanup_free_ char *t;
+
+ assert_se(glob_non_glob_prefix(path, &t) == 0);
+ assert_se(streq(t, expected));
+}
+
+TEST(glob_non_glob) {
+ test_glob_non_glob_prefix_one("/tmp/.X11-*", "/tmp/");
+ test_glob_non_glob_prefix_one("/tmp/*", "/tmp/");
+ test_glob_non_glob_prefix_one("/tmp*", "/");
+ test_glob_non_glob_prefix_one("/tmp/*/whatever", "/tmp/");
+ test_glob_non_glob_prefix_one("/tmp/*/whatever?", "/tmp/");
+ test_glob_non_glob_prefix_one("/?", "/");
+
+ char *x;
+ assert_se(glob_non_glob_prefix("?", &x) == -ENOENT);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-gpt.c b/src/test/test-gpt.c
new file mode 100644
index 0000000..fa5923e
--- /dev/null
+++ b/src/test/test-gpt.c
@@ -0,0 +1,111 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "architecture.h"
+#include "glyph-util.h"
+#include "gpt.h"
+#include "log.h"
+#include "pretty-print.h"
+#include "strv.h"
+#include "terminal-util.h"
+#include "tests.h"
+
+TEST(gpt_types_against_architectures) {
+ int r;
+
+ /* Dumps a table indicating for which architectures we know we have matching GPT partition
+ * types. Also validates whether we can properly categorize the entries. */
+
+ FOREACH_STRING(prefix, "root-", "usr-")
+ for (Architecture a = 0; a < _ARCHITECTURE_MAX; a++)
+ FOREACH_STRING(suffix, "", "-verity", "-verity-sig") {
+ _cleanup_free_ char *joined = NULL;
+ GptPartitionType type;
+
+ joined = strjoin(prefix, architecture_to_string(a), suffix);
+ if (!joined)
+ return (void) log_oom();
+
+ r = gpt_partition_type_from_string(joined, &type);
+ if (r < 0) {
+ printf("%s %s\n", RED_CROSS_MARK(), joined);
+ continue;
+ }
+
+ printf("%s %s\n", GREEN_CHECK_MARK(), joined);
+
+ if (streq(prefix, "root-") && streq(suffix, ""))
+ assert_se(type.designator == PARTITION_ROOT);
+ if (streq(prefix, "root-") && streq(suffix, "-verity"))
+ assert_se(type.designator == PARTITION_ROOT_VERITY);
+ if (streq(prefix, "usr-") && streq(suffix, ""))
+ assert_se(type.designator == PARTITION_USR);
+ if (streq(prefix, "usr-") && streq(suffix, "-verity"))
+ assert_se(type.designator == PARTITION_USR_VERITY);
+
+ assert_se(type.arch == a);
+ }
+}
+
+TEST(verity_mappings) {
+ for (PartitionDesignator p = 0; p < _PARTITION_DESIGNATOR_MAX; p++) {
+ PartitionDesignator q;
+
+ q = partition_verity_of(p);
+ assert_se(q < 0 || partition_verity_to_data(q) == p);
+
+ q = partition_verity_sig_of(p);
+ assert_se(q < 0 || partition_verity_sig_to_data(q) == p);
+
+ q = partition_verity_to_data(p);
+ assert_se(q < 0 || partition_verity_of(q) == p);
+
+ q = partition_verity_sig_to_data(p);
+ assert_se(q < 0 || partition_verity_sig_of(q) == p);
+ }
+}
+
+TEST(type_alias_same) {
+ /* Check that the partition type table is consistent, i.e. all aliases of the same partition type
+ * carry the same metadata */
+
+ for (const GptPartitionType *t = gpt_partition_type_table; t->name; t++) {
+ GptPartitionType x, y;
+
+ x = gpt_partition_type_from_uuid(t->uuid); /* search first by uuid */
+ assert_se(gpt_partition_type_from_string(t->name, &y) >= 0); /* search first by name */
+
+ assert_se(t->arch == x.arch);
+ assert_se(t->arch == y.arch);
+ assert_se(t->designator == x.designator);
+ assert_se(t->designator == y.designator);
+ }
+}
+
+TEST(override_architecture) {
+ GptPartitionType x, y;
+
+ assert_se(gpt_partition_type_from_string("root-x86-64", &x) >= 0);
+ assert_se(x.arch == ARCHITECTURE_X86_64);
+
+ assert_se(gpt_partition_type_from_string("root-arm64", &y) >= 0);
+ assert(y.arch == ARCHITECTURE_ARM64);
+
+ x = gpt_partition_type_override_architecture(x, ARCHITECTURE_ARM64);
+ assert_se(x.arch == y.arch);
+ assert_se(x.designator == y.designator);
+ assert_se(sd_id128_equal(x.uuid, y.uuid));
+ assert_se(streq(x.name, y.name));
+
+ /* If the partition type does not have an architecture, nothing should change. */
+
+ assert_se(gpt_partition_type_from_string("esp", &x) >= 0);
+ y = x;
+
+ x = gpt_partition_type_override_architecture(x, ARCHITECTURE_ARM64);
+ assert_se(x.arch == y.arch);
+ assert_se(x.designator == y.designator);
+ assert_se(sd_id128_equal(x.uuid, y.uuid));
+ assert_se(streq(x.name, y.name));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-gunicode.c b/src/test/test-gunicode.c
new file mode 100644
index 0000000..1836cdc
--- /dev/null
+++ b/src/test/test-gunicode.c
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "gunicode.h"
+#include "tests.h"
+#include "utf8.h"
+
+TEST(unichar_iswide) {
+ char32_t c;
+ int r;
+
+ /* FIXME: the cats are wide, but we get this wrong */
+ for (const char *narrow = "abX_…ąęµ!" "😼😿🙀😸😻"; *narrow; narrow += r) {
+ r = utf8_encoded_to_unichar(narrow, &c);
+ bool w = unichar_iswide(c);
+ assert_se(r > 0);
+ assert_se(!w);
+ }
+
+ for (const char *wide = "🐱/¥"; *wide; wide += r) {
+ r = utf8_encoded_to_unichar(wide, &c);
+ bool w = unichar_iswide(c);
+ assert_se(r > 0);
+ assert_se(w);
+ }
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-hash-funcs.c b/src/test/test-hash-funcs.c
new file mode 100644
index 0000000..f5166c1
--- /dev/null
+++ b/src/test/test-hash-funcs.c
@@ -0,0 +1,77 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "tests.h"
+#include "hash-funcs.h"
+#include "set.h"
+
+TEST(path_hash_set) {
+ /* The goal is to make sure that non-simplified path are hashed as expected,
+ * and that we don't need to simplify them beforehand. */
+
+ /* No freeing of keys, we operate on static strings here… */
+ _cleanup_set_free_ Set *set = NULL;
+
+ assert_se(set_isempty(set));
+ assert_se(set_ensure_put(&set, &path_hash_ops, "foo") == 1);
+ assert_se(set_ensure_put(&set, &path_hash_ops, "foo") == 0);
+ assert_se(set_ensure_put(&set, &path_hash_ops, "bar") == 1);
+ assert_se(set_ensure_put(&set, &path_hash_ops, "bar") == 0);
+ assert_se(set_ensure_put(&set, &path_hash_ops, "/foo") == 1);
+ assert_se(set_ensure_put(&set, &path_hash_ops, "/bar") == 1);
+ assert_se(set_ensure_put(&set, &path_hash_ops, "/foo/.") == 0);
+ assert_se(set_ensure_put(&set, &path_hash_ops, "/./bar/./.") == 0);
+
+ assert_se(set_contains(set, "foo"));
+ assert_se(set_contains(set, "bar"));
+ assert_se(set_contains(set, "./foo"));
+ assert_se(set_contains(set, "./foo/."));
+ assert_se(set_contains(set, "./bar"));
+ assert_se(set_contains(set, "./bar/."));
+ assert_se(set_contains(set, "/foo"));
+ assert_se(set_contains(set, "/bar"));
+ assert_se(set_contains(set, "//./foo"));
+ assert_se(set_contains(set, "///./foo/."));
+ assert_se(set_contains(set, "////./bar"));
+ assert_se(set_contains(set, "/////./bar/."));
+
+ assert_se(set_contains(set, "foo/"));
+ assert_se(set_contains(set, "bar/"));
+ assert_se(set_contains(set, "./foo/"));
+ assert_se(set_contains(set, "./foo/./"));
+ assert_se(set_contains(set, "./bar/"));
+ assert_se(set_contains(set, "./bar/./"));
+ assert_se(set_contains(set, "/foo/"));
+ assert_se(set_contains(set, "/bar/"));
+ assert_se(set_contains(set, "//./foo/"));
+ assert_se(set_contains(set, "///./foo/./"));
+ assert_se(set_contains(set, "////./bar/"));
+ assert_se(set_contains(set, "/////./bar/./"));
+
+ assert_se(!set_contains(set, "foo."));
+ assert_se(!set_contains(set, ".bar"));
+ assert_se(!set_contains(set, "./foo."));
+ assert_se(!set_contains(set, "./.foo/."));
+ assert_se(!set_contains(set, "../bar"));
+ assert_se(!set_contains(set, "./bar/.."));
+ assert_se(!set_contains(set, "./foo.."));
+ assert_se(!set_contains(set, "/..bar"));
+ assert_se(!set_contains(set, "//../foo"));
+ assert_se(!set_contains(set, "///../foo/."));
+ assert_se(!set_contains(set, "////../bar"));
+ assert_se(!set_contains(set, "/////../bar/."));
+
+ assert_se(!set_contains(set, "foo./"));
+ assert_se(!set_contains(set, ".bar/"));
+ assert_se(!set_contains(set, "./foo./"));
+ assert_se(!set_contains(set, "./.foo/./"));
+ assert_se(!set_contains(set, "../bar/"));
+ assert_se(!set_contains(set, "./bar/../"));
+ assert_se(!set_contains(set, "./foo../"));
+ assert_se(!set_contains(set, "/..bar/"));
+ assert_se(!set_contains(set, "//../foo/"));
+ assert_se(!set_contains(set, "///../foo/./"));
+ assert_se(!set_contains(set, "////../bar/"));
+ assert_se(!set_contains(set, "/////../bar/./"));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-hashmap-ordered.awk b/src/test/test-hashmap-ordered.awk
new file mode 100644
index 0000000..88ffc25
--- /dev/null
+++ b/src/test/test-hashmap-ordered.awk
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+BEGIN {
+ print "/* GENERATED FILE */";
+ print "#define ORDERED"
+}
+{
+ if (!match($0, "^#include"))
+ gsub(/hashmap/, "ordered_hashmap");
+ gsub(/HASHMAP/, "ORDERED_HASHMAP");
+ gsub(/Hashmap/, "OrderedHashmap");
+ print
+}
diff --git a/src/test/test-hashmap-plain.c b/src/test/test-hashmap-plain.c
new file mode 100644
index 0000000..152b1c0
--- /dev/null
+++ b/src/test/test-hashmap-plain.c
@@ -0,0 +1,1008 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "hashmap.h"
+#include "log.h"
+#include "nulstr-util.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "time-util.h"
+
+TEST(hashmap_replace) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+ _cleanup_free_ char *val1 = NULL, *val2 = NULL, *val3 = NULL, *val4 = NULL, *val5 = NULL;
+ char *r;
+
+ m = hashmap_new(&string_hash_ops);
+
+ val1 = strdup("val1");
+ assert_se(val1);
+ val2 = strdup("val2");
+ assert_se(val2);
+ val3 = strdup("val3");
+ assert_se(val3);
+ val4 = strdup("val4");
+ assert_se(val4);
+ val5 = strdup("val5");
+ assert_se(val5);
+
+ hashmap_put(m, "key 1", val1);
+ hashmap_put(m, "key 2", val2);
+ hashmap_put(m, "key 3", val3);
+ hashmap_put(m, "key 4", val4);
+
+ hashmap_replace(m, "key 3", val1);
+ r = hashmap_get(m, "key 3");
+ assert_se(streq(r, "val1"));
+
+ hashmap_replace(m, "key 5", val5);
+ r = hashmap_get(m, "key 5");
+ assert_se(streq(r, "val5"));
+}
+
+TEST(hashmap_copy) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+ _cleanup_hashmap_free_free_ Hashmap *copy = NULL;
+ char *val1, *val2, *val3, *val4, *r;
+
+ val1 = strdup("val1");
+ assert_se(val1);
+ val2 = strdup("val2");
+ assert_se(val2);
+ val3 = strdup("val3");
+ assert_se(val3);
+ val4 = strdup("val4");
+ assert_se(val4);
+
+ m = hashmap_new(&string_hash_ops);
+
+ hashmap_put(m, "key 1", val1);
+ hashmap_put(m, "key 2", val2);
+ hashmap_put(m, "key 3", val3);
+ hashmap_put(m, "key 4", val4);
+
+ copy = hashmap_copy(m);
+
+ r = hashmap_get(copy, "key 1");
+ assert_se(streq(r, "val1"));
+ r = hashmap_get(copy, "key 2");
+ assert_se(streq(r, "val2"));
+ r = hashmap_get(copy, "key 3");
+ assert_se(streq(r, "val3"));
+ r = hashmap_get(copy, "key 4");
+ assert_se(streq(r, "val4"));
+}
+
+TEST(hashmap_get_strv) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+ _cleanup_strv_free_ char **strv = NULL;
+ char *val1, *val2, *val3, *val4;
+
+ val1 = strdup("val1");
+ assert_se(val1);
+ val2 = strdup("val2");
+ assert_se(val2);
+ val3 = strdup("val3");
+ assert_se(val3);
+ val4 = strdup("val4");
+ assert_se(val4);
+
+ m = hashmap_new(&string_hash_ops);
+
+ hashmap_put(m, "key 1", val1);
+ hashmap_put(m, "key 2", val2);
+ hashmap_put(m, "key 3", val3);
+ hashmap_put(m, "key 4", val4);
+
+ strv = hashmap_get_strv(m);
+
+#ifndef ORDERED
+ strv = strv_sort(strv);
+#endif
+
+ assert_se(streq(strv[0], "val1"));
+ assert_se(streq(strv[1], "val2"));
+ assert_se(streq(strv[2], "val3"));
+ assert_se(streq(strv[3], "val4"));
+}
+
+TEST(hashmap_move_one) {
+ _cleanup_hashmap_free_free_ Hashmap *m = NULL, *n = NULL;
+ char *val1, *val2, *val3, *val4, *r;
+
+ val1 = strdup("val1");
+ assert_se(val1);
+ val2 = strdup("val2");
+ assert_se(val2);
+ val3 = strdup("val3");
+ assert_se(val3);
+ val4 = strdup("val4");
+ assert_se(val4);
+
+ m = hashmap_new(&string_hash_ops);
+ n = hashmap_new(&string_hash_ops);
+
+ hashmap_put(m, "key 1", val1);
+ hashmap_put(m, "key 2", val2);
+ hashmap_put(m, "key 3", val3);
+ hashmap_put(m, "key 4", val4);
+
+ assert_se(hashmap_move_one(n, NULL, "key 3") == -ENOENT);
+ assert_se(hashmap_move_one(n, m, "key 5") == -ENOENT);
+ assert_se(hashmap_move_one(n, m, "key 3") == 0);
+ assert_se(hashmap_move_one(n, m, "key 4") == 0);
+
+ r = hashmap_get(n, "key 3");
+ assert_se(r && streq(r, "val3"));
+ r = hashmap_get(n, "key 4");
+ assert_se(r && streq(r, "val4"));
+ r = hashmap_get(m, "key 3");
+ assert_se(!r);
+
+ assert_se(hashmap_move_one(n, m, "key 3") == -EEXIST);
+}
+
+TEST(hashmap_move) {
+ _cleanup_hashmap_free_free_ Hashmap *m = NULL, *n = NULL;
+ char *val1, *val2, *val3, *val4, *r;
+
+ val1 = strdup("val1");
+ assert_se(val1);
+ val2 = strdup("val2");
+ assert_se(val2);
+ val3 = strdup("val3");
+ assert_se(val3);
+ val4 = strdup("val4");
+ assert_se(val4);
+
+ m = hashmap_new(&string_hash_ops);
+ n = hashmap_new(&string_hash_ops);
+
+ hashmap_put(n, "key 1", strdup(val1));
+ hashmap_put(m, "key 1", val1);
+ hashmap_put(m, "key 2", val2);
+ hashmap_put(m, "key 3", val3);
+ hashmap_put(m, "key 4", val4);
+
+ assert_se(hashmap_move(n, NULL) == 0);
+ assert_se(hashmap_move(n, m) == 0);
+
+ assert_se(hashmap_size(m) == 1);
+ r = hashmap_get(m, "key 1");
+ assert_se(r && streq(r, "val1"));
+
+ r = hashmap_get(n, "key 1");
+ assert_se(r && streq(r, "val1"));
+ r = hashmap_get(n, "key 2");
+ assert_se(r && streq(r, "val2"));
+ r = hashmap_get(n, "key 3");
+ assert_se(r && streq(r, "val3"));
+ r = hashmap_get(n, "key 4");
+ assert_se(r && streq(r, "val4"));
+}
+
+TEST(hashmap_update) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+ _cleanup_free_ char *val1 = NULL, *val2 = NULL;
+ char *r;
+
+ m = hashmap_new(&string_hash_ops);
+ val1 = strdup("old_value");
+ assert_se(val1);
+ val2 = strdup("new_value");
+ assert_se(val2);
+
+ hashmap_put(m, "key 1", val1);
+ r = hashmap_get(m, "key 1");
+ assert_se(streq(r, "old_value"));
+
+ assert_se(hashmap_update(m, "key 2", val2) == -ENOENT);
+ r = hashmap_get(m, "key 1");
+ assert_se(streq(r, "old_value"));
+
+ assert_se(hashmap_update(m, "key 1", val2) == 0);
+ r = hashmap_get(m, "key 1");
+ assert_se(streq(r, "new_value"));
+}
+
+TEST(hashmap_put) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+ int valid_hashmap_put;
+ void *val1 = (void*) "val 1";
+ void *val2 = (void*) "val 2";
+ _cleanup_free_ char* key1 = NULL;
+
+ assert_se(hashmap_ensure_allocated(&m, &string_hash_ops) == 1);
+ assert_se(m);
+
+ valid_hashmap_put = hashmap_put(m, "key 1", val1);
+ assert_se(valid_hashmap_put == 1);
+ assert_se(hashmap_put(m, "key 1", val1) == 0);
+ assert_se(hashmap_put(m, "key 1", val2) == -EEXIST);
+ key1 = strdup("key 1");
+ assert_se(hashmap_put(m, key1, val1) == 0);
+ assert_se(hashmap_put(m, key1, val2) == -EEXIST);
+}
+
+TEST(hashmap_remove1) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+ char *r;
+
+ r = hashmap_remove(NULL, "key 1");
+ assert_se(r == NULL);
+
+ m = hashmap_new(&string_hash_ops);
+ assert_se(m);
+
+ r = hashmap_remove(m, "no such key");
+ assert_se(r == NULL);
+
+ hashmap_put(m, "key 1", (void*) "val 1");
+ hashmap_put(m, "key 2", (void*) "val 2");
+
+ r = hashmap_remove(m, "key 1");
+ assert_se(streq(r, "val 1"));
+
+ r = hashmap_get(m, "key 2");
+ assert_se(streq(r, "val 2"));
+ assert_se(!hashmap_get(m, "key 1"));
+}
+
+TEST(hashmap_remove2) {
+ _cleanup_hashmap_free_free_free_ Hashmap *m = NULL;
+ char key1[] = "key 1";
+ char key2[] = "key 2";
+ char val1[] = "val 1";
+ char val2[] = "val 2";
+ void *r, *r2;
+
+ r = hashmap_remove2(NULL, "key 1", &r2);
+ assert_se(r == NULL);
+
+ m = hashmap_new(&string_hash_ops);
+ assert_se(m);
+
+ r = hashmap_remove2(m, "no such key", &r2);
+ assert_se(r == NULL);
+
+ hashmap_put(m, strdup(key1), strdup(val1));
+ hashmap_put(m, strdup(key2), strdup(val2));
+
+ r = hashmap_remove2(m, key1, &r2);
+ assert_se(streq(r, val1));
+ assert_se(streq(r2, key1));
+ free(r);
+ free(r2);
+
+ r = hashmap_get(m, key2);
+ assert_se(streq(r, val2));
+ assert_se(!hashmap_get(m, key1));
+}
+
+TEST(hashmap_remove_value) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+ char *r;
+
+ char val1[] = "val 1";
+ char val2[] = "val 2";
+
+ r = hashmap_remove_value(NULL, "key 1", val1);
+ assert_se(r == NULL);
+
+ m = hashmap_new(&string_hash_ops);
+ assert_se(m);
+
+ r = hashmap_remove_value(m, "key 1", val1);
+ assert_se(r == NULL);
+
+ hashmap_put(m, "key 1", val1);
+ hashmap_put(m, "key 2", val2);
+
+ r = hashmap_remove_value(m, "key 1", val1);
+ assert_se(streq(r, "val 1"));
+
+ r = hashmap_get(m, "key 2");
+ assert_se(streq(r, "val 2"));
+ assert_se(!hashmap_get(m, "key 1"));
+
+ r = hashmap_remove_value(m, "key 2", val1);
+ assert_se(r == NULL);
+
+ r = hashmap_get(m, "key 2");
+ assert_se(streq(r, "val 2"));
+ assert_se(!hashmap_get(m, "key 1"));
+}
+
+TEST(hashmap_remove_and_put) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+ int valid;
+ char *r;
+
+ m = hashmap_new(&string_hash_ops);
+ assert_se(m);
+
+ valid = hashmap_remove_and_put(m, "invalid key", "new key", NULL);
+ assert_se(valid == -ENOENT);
+
+ valid = hashmap_put(m, "key 1", (void*) (const char *) "val 1");
+ assert_se(valid == 1);
+
+ valid = hashmap_remove_and_put(NULL, "key 1", "key 2", (void*) (const char *) "val 2");
+ assert_se(valid == -ENOENT);
+
+ valid = hashmap_remove_and_put(m, "key 1", "key 2", (void*) (const char *) "val 2");
+ assert_se(valid == 0);
+
+ r = hashmap_get(m, "key 2");
+ assert_se(streq(r, "val 2"));
+ assert_se(!hashmap_get(m, "key 1"));
+
+ valid = hashmap_put(m, "key 3", (void*) (const char *) "val 3");
+ assert_se(valid == 1);
+ valid = hashmap_remove_and_put(m, "key 3", "key 2", (void*) (const char *) "val 2");
+ assert_se(valid == -EEXIST);
+}
+
+TEST(hashmap_remove_and_replace) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+ int valid;
+ void *key1 = UINT_TO_PTR(1);
+ void *key2 = UINT_TO_PTR(2);
+ void *key3 = UINT_TO_PTR(3);
+ void *r;
+ int i, j;
+
+ m = hashmap_new(&trivial_hash_ops);
+ assert_se(m);
+
+ valid = hashmap_remove_and_replace(m, key1, key2, NULL);
+ assert_se(valid == -ENOENT);
+
+ valid = hashmap_put(m, key1, key1);
+ assert_se(valid == 1);
+
+ valid = hashmap_remove_and_replace(NULL, key1, key2, key2);
+ assert_se(valid == -ENOENT);
+
+ valid = hashmap_remove_and_replace(m, key1, key2, key2);
+ assert_se(valid == 0);
+
+ r = hashmap_get(m, key2);
+ assert_se(r == key2);
+ assert_se(!hashmap_get(m, key1));
+
+ valid = hashmap_put(m, key3, key3);
+ assert_se(valid == 1);
+ valid = hashmap_remove_and_replace(m, key3, key2, key2);
+ assert_se(valid == 0);
+ r = hashmap_get(m, key2);
+ assert_se(r == key2);
+ assert_se(!hashmap_get(m, key3));
+
+ /* Repeat this test several times to increase the chance of hitting
+ * the less likely case in hashmap_remove_and_replace where it
+ * compensates for the backward shift. */
+ for (i = 0; i < 20; i++) {
+ hashmap_clear(m);
+
+ for (j = 1; j < 7; j++)
+ hashmap_put(m, UINT_TO_PTR(10*i + j), UINT_TO_PTR(10*i + j));
+ valid = hashmap_remove_and_replace(m, UINT_TO_PTR(10*i + 1),
+ UINT_TO_PTR(10*i + 2),
+ UINT_TO_PTR(10*i + 2));
+ assert_se(valid == 0);
+ assert_se(!hashmap_get(m, UINT_TO_PTR(10*i + 1)));
+ for (j = 2; j < 7; j++) {
+ r = hashmap_get(m, UINT_TO_PTR(10*i + j));
+ assert_se(r == UINT_TO_PTR(10*i + j));
+ }
+ }
+}
+
+TEST(hashmap_ensure_allocated) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+ int r;
+
+ r = hashmap_ensure_allocated(&m, &string_hash_ops);
+ assert_se(r == 1);
+
+ r = hashmap_ensure_allocated(&m, &string_hash_ops);
+ assert_se(r == 0);
+
+ /* different hash ops shouldn't matter at this point */
+ r = hashmap_ensure_allocated(&m, &trivial_hash_ops);
+ assert_se(r == 0);
+}
+
+TEST(hashmap_foreach_key) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+ bool key_found[] = { false, false, false, false };
+ const char *s;
+ const char *key;
+ static const char key_table[] =
+ "key 1\0"
+ "key 2\0"
+ "key 3\0"
+ "key 4\0";
+
+ m = hashmap_new(&string_hash_ops);
+
+ NULSTR_FOREACH(k, key_table)
+ hashmap_put(m, k, (void*) (const char*) "my dummy val");
+
+ HASHMAP_FOREACH_KEY(s, key, m) {
+ assert_se(s);
+ if (!key_found[0] && streq(key, "key 1"))
+ key_found[0] = true;
+ else if (!key_found[1] && streq(key, "key 2"))
+ key_found[1] = true;
+ else if (!key_found[2] && streq(key, "key 3"))
+ key_found[2] = true;
+ else if (!key_found[3] && streq(key, "fail"))
+ key_found[3] = true;
+ }
+
+ assert_se(m);
+ assert_se(key_found[0] && key_found[1] && key_found[2] && !key_found[3]);
+}
+
+TEST(hashmap_foreach) {
+ _cleanup_hashmap_free_free_ Hashmap *m = NULL;
+ bool value_found[] = { false, false, false, false };
+ char *val1, *val2, *val3, *val4, *s;
+ unsigned count;
+
+ val1 = strdup("my val1");
+ assert_se(val1);
+ val2 = strdup("my val2");
+ assert_se(val2);
+ val3 = strdup("my val3");
+ assert_se(val3);
+ val4 = strdup("my val4");
+ assert_se(val4);
+
+ count = 0;
+ HASHMAP_FOREACH(s, m)
+ count++;
+ assert_se(count == 0);
+
+ m = hashmap_new(&string_hash_ops);
+
+ count = 0;
+ HASHMAP_FOREACH(s, m)
+ count++;
+ assert_se(count == 0);
+
+ hashmap_put(m, "Key 1", val1);
+ hashmap_put(m, "Key 2", val2);
+ hashmap_put(m, "Key 3", val3);
+ hashmap_put(m, "Key 4", val4);
+
+ HASHMAP_FOREACH(s, m) {
+ if (!value_found[0] && streq(s, val1))
+ value_found[0] = true;
+ else if (!value_found[1] && streq(s, val2))
+ value_found[1] = true;
+ else if (!value_found[2] && streq(s, val3))
+ value_found[2] = true;
+ else if (!value_found[3] && streq(s, val4))
+ value_found[3] = true;
+ }
+
+ assert_se(m);
+ assert_se(value_found[0] && value_found[1] && value_found[2] && value_found[3]);
+}
+
+TEST(hashmap_merge) {
+ _cleanup_hashmap_free_free_ Hashmap *m = NULL;
+ _cleanup_hashmap_free_ Hashmap *n = NULL;
+ char *val1, *val2, *val3, *val4, *r;
+
+ val1 = strdup("my val1");
+ assert_se(val1);
+ val2 = strdup("my val2");
+ assert_se(val2);
+ val3 = strdup("my val3");
+ assert_se(val3);
+ val4 = strdup("my val4");
+ assert_se(val4);
+
+ m = hashmap_new(&string_hash_ops);
+ n = hashmap_new(&string_hash_ops);
+
+ hashmap_put(m, "Key 1", val1);
+ hashmap_put(m, "Key 2", val2);
+ hashmap_put(n, "Key 3", val3);
+ hashmap_put(n, "Key 4", val4);
+
+ assert_se(hashmap_merge(m, n) == 0);
+ r = hashmap_get(m, "Key 3");
+ assert_se(r && streq(r, "my val3"));
+ r = hashmap_get(m, "Key 4");
+ assert_se(r && streq(r, "my val4"));
+
+ assert_se(m);
+ assert_se(n);
+}
+
+TEST(hashmap_contains) {
+ _cleanup_hashmap_free_free_ Hashmap *m = NULL;
+ char *val1;
+
+ val1 = strdup("my val");
+ assert_se(val1);
+
+ m = hashmap_new(&string_hash_ops);
+
+ assert_se(!hashmap_contains(m, "Key 1"));
+ hashmap_put(m, "Key 1", val1);
+ assert_se(hashmap_contains(m, "Key 1"));
+ assert_se(!hashmap_contains(m, "Key 2"));
+
+ assert_se(!hashmap_contains(NULL, "Key 1"));
+
+ assert_se(m);
+}
+
+TEST(hashmap_isempty) {
+ _cleanup_hashmap_free_free_ Hashmap *m = NULL;
+ char *val1;
+
+ val1 = strdup("my val");
+ assert_se(val1);
+
+ m = hashmap_new(&string_hash_ops);
+
+ assert_se(hashmap_isempty(m));
+ hashmap_put(m, "Key 1", val1);
+ assert_se(!hashmap_isempty(m));
+
+ assert_se(m);
+}
+
+TEST(hashmap_size) {
+ _cleanup_hashmap_free_free_ Hashmap *m = NULL;
+ char *val1, *val2, *val3, *val4;
+
+ val1 = strdup("my val");
+ assert_se(val1);
+ val2 = strdup("my val");
+ assert_se(val2);
+ val3 = strdup("my val");
+ assert_se(val3);
+ val4 = strdup("my val");
+ assert_se(val4);
+
+ assert_se(hashmap_size(NULL) == 0);
+ assert_se(hashmap_buckets(NULL) == 0);
+
+ m = hashmap_new(&string_hash_ops);
+
+ hashmap_put(m, "Key 1", val1);
+ hashmap_put(m, "Key 2", val2);
+ hashmap_put(m, "Key 3", val3);
+ hashmap_put(m, "Key 4", val4);
+
+ assert_se(m);
+ assert_se(hashmap_size(m) == 4);
+ assert_se(hashmap_buckets(m) >= 4);
+}
+
+TEST(hashmap_get) {
+ _cleanup_hashmap_free_free_ Hashmap *m = NULL;
+ char *r;
+ char *val;
+
+ val = strdup("my val");
+ assert_se(val);
+
+ r = hashmap_get(NULL, "Key 1");
+ assert_se(r == NULL);
+
+ m = hashmap_new(&string_hash_ops);
+
+ hashmap_put(m, "Key 1", val);
+
+ r = hashmap_get(m, "Key 1");
+ assert_se(streq(r, val));
+
+ r = hashmap_get(m, "no such key");
+ assert_se(r == NULL);
+
+ assert_se(m);
+}
+
+TEST(hashmap_get2) {
+ _cleanup_(hashmap_free_free_freep) Hashmap *m = NULL;
+ char *r;
+ char *val;
+ char key_orig[] = "Key 1";
+ void *key_copy;
+
+ val = strdup("my val");
+ assert_se(val);
+
+ key_copy = strdup(key_orig);
+ assert_se(key_copy);
+
+ r = hashmap_get2(NULL, key_orig, &key_copy);
+ assert_se(r == NULL);
+
+ m = hashmap_new(&string_hash_ops);
+
+ hashmap_put(m, key_copy, val);
+ key_copy = NULL;
+
+ r = hashmap_get2(m, key_orig, &key_copy);
+ assert_se(streq(r, val));
+ assert_se(key_orig != key_copy);
+ assert_se(streq(key_orig, key_copy));
+
+ r = hashmap_get2(m, "no such key", NULL);
+ assert_se(r == NULL);
+
+ assert_se(m);
+}
+
+static void crippled_hashmap_func(const void *p, struct siphash *state) {
+ return trivial_hash_func(INT_TO_PTR(PTR_TO_INT(p) & 0xff), state);
+}
+
+static const struct hash_ops crippled_hashmap_ops = {
+ .hash = crippled_hashmap_func,
+ .compare = trivial_compare_func,
+};
+
+TEST(hashmap_many) {
+ Hashmap *h;
+ unsigned i, j;
+ void *v, *k;
+ bool slow = slow_tests_enabled();
+ const struct {
+ const char *title;
+ const struct hash_ops *ops;
+ unsigned n_entries;
+ } tests[] = {
+ { "trivial_hashmap_ops", NULL, slow ? 1 << 20 : 240 },
+ { "crippled_hashmap_ops", &crippled_hashmap_ops, slow ? 1 << 14 : 140 },
+ };
+
+ log_info("/* %s (%s) */", __func__, slow ? "slow" : "fast");
+
+ for (j = 0; j < ELEMENTSOF(tests); j++) {
+ usec_t ts = now(CLOCK_MONOTONIC), n;
+
+ assert_se(h = hashmap_new(tests[j].ops));
+
+ for (i = 1; i < tests[j].n_entries*3; i+=3) {
+ assert_se(hashmap_put(h, UINT_TO_PTR(i), UINT_TO_PTR(i)) >= 0);
+ assert_se(PTR_TO_UINT(hashmap_get(h, UINT_TO_PTR(i))) == i);
+ }
+
+ for (i = 1; i < tests[j].n_entries*3; i++)
+ assert_se(hashmap_contains(h, UINT_TO_PTR(i)) == (i % 3 == 1));
+
+ log_info("%s %u <= %u * 0.8 = %g",
+ tests[j].title, hashmap_size(h), hashmap_buckets(h), hashmap_buckets(h) * 0.8);
+
+ assert_se(hashmap_size(h) <= hashmap_buckets(h) * 0.8);
+ assert_se(hashmap_size(h) == tests[j].n_entries);
+
+ while (!hashmap_isempty(h)) {
+ k = hashmap_first_key(h);
+ v = hashmap_remove(h, k);
+ assert_se(v == k);
+ }
+
+ hashmap_free(h);
+
+ n = now(CLOCK_MONOTONIC);
+ log_info("test took %s", FORMAT_TIMESPAN(n - ts, 0));
+ }
+}
+
+extern unsigned custom_counter;
+extern const struct hash_ops boring_hash_ops, custom_hash_ops;
+
+TEST(hashmap_free) {
+ Hashmap *h;
+ bool slow = slow_tests_enabled();
+ usec_t ts, n;
+ unsigned n_entries = slow ? 1 << 20 : 240;
+
+ const struct {
+ const char *title;
+ const struct hash_ops *ops;
+ unsigned expect_counter;
+ } tests[] = {
+ { "string_hash_ops", &boring_hash_ops, 2 * n_entries},
+ { "custom_free_hash_ops", &custom_hash_ops, 0 },
+ };
+
+ log_info("/* %s (%s, %u entries) */", __func__, slow ? "slow" : "fast", n_entries);
+
+ for (unsigned j = 0; j < ELEMENTSOF(tests); j++) {
+ ts = now(CLOCK_MONOTONIC);
+ assert_se(h = hashmap_new(tests[j].ops));
+
+ custom_counter = 0;
+ for (unsigned i = 0; i < n_entries; i++) {
+ char s[DECIMAL_STR_MAX(unsigned)];
+ char *k, *v;
+
+ xsprintf(s, "%u", i);
+ assert_se(k = strdup(s));
+ assert_se(v = strdup(s));
+ custom_counter += 2;
+
+ assert_se(hashmap_put(h, k, v) >= 0);
+ }
+
+ hashmap_free(h);
+
+ n = now(CLOCK_MONOTONIC);
+ log_info("%s test took %s", tests[j].title, FORMAT_TIMESPAN(n - ts, 0));
+
+ assert_se(custom_counter == tests[j].expect_counter);
+ }
+}
+
+typedef struct Item {
+ int seen;
+} Item;
+static void item_seen(Item *item) {
+ item->seen++;
+}
+
+TEST(hashmap_free_with_destructor) {
+ Hashmap *m;
+ struct Item items[4] = {};
+ unsigned i;
+
+ assert_se(m = hashmap_new(NULL));
+ for (i = 0; i < ELEMENTSOF(items) - 1; i++)
+ assert_se(hashmap_put(m, INT_TO_PTR(i), items + i) == 1);
+
+ m = hashmap_free_with_destructor(m, item_seen);
+ assert_se(items[0].seen == 1);
+ assert_se(items[1].seen == 1);
+ assert_se(items[2].seen == 1);
+ assert_se(items[3].seen == 0);
+}
+
+TEST(hashmap_first) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+
+ m = hashmap_new(&string_hash_ops);
+ assert_se(m);
+
+ assert_se(!hashmap_first(m));
+ assert_se(hashmap_put(m, "key 1", (void*) "val 1") == 1);
+ assert_se(streq(hashmap_first(m), "val 1"));
+ assert_se(hashmap_put(m, "key 2", (void*) "val 2") == 1);
+#ifdef ORDERED
+ assert_se(streq(hashmap_first(m), "val 1"));
+ assert_se(hashmap_remove(m, "key 1"));
+ assert_se(streq(hashmap_first(m), "val 2"));
+#endif
+}
+
+TEST(hashmap_first_key) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+
+ m = hashmap_new(&string_hash_ops);
+ assert_se(m);
+
+ assert_se(!hashmap_first_key(m));
+ assert_se(hashmap_put(m, "key 1", NULL) == 1);
+ assert_se(streq(hashmap_first_key(m), "key 1"));
+ assert_se(hashmap_put(m, "key 2", NULL) == 1);
+#ifdef ORDERED
+ assert_se(streq(hashmap_first_key(m), "key 1"));
+ assert_se(hashmap_remove(m, "key 1") == NULL);
+ assert_se(streq(hashmap_first_key(m), "key 2"));
+#endif
+}
+
+TEST(hashmap_steal_first_key) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+
+ m = hashmap_new(&string_hash_ops);
+ assert_se(m);
+
+ assert_se(!hashmap_steal_first_key(m));
+ assert_se(hashmap_put(m, "key 1", NULL) == 1);
+ assert_se(streq(hashmap_steal_first_key(m), "key 1"));
+
+ assert_se(hashmap_isempty(m));
+}
+
+TEST(hashmap_steal_first) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+ int seen[3] = {};
+ char *val;
+
+ m = hashmap_new(&string_hash_ops);
+ assert_se(m);
+
+ assert_se(hashmap_put(m, "key 1", (void*) "1") == 1);
+ assert_se(hashmap_put(m, "key 2", (void*) "22") == 1);
+ assert_se(hashmap_put(m, "key 3", (void*) "333") == 1);
+
+ while ((val = hashmap_steal_first(m)))
+ seen[strlen(val) - 1]++;
+
+ assert_se(seen[0] == 1 && seen[1] == 1 && seen[2] == 1);
+
+ assert_se(hashmap_isempty(m));
+}
+
+TEST(hashmap_clear_free_free) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+
+ m = hashmap_new(&string_hash_ops);
+ assert_se(m);
+
+ assert_se(hashmap_put(m, strdup("key 1"), NULL) == 1);
+ assert_se(hashmap_put(m, strdup("key 2"), NULL) == 1);
+ assert_se(hashmap_put(m, strdup("key 3"), NULL) == 1);
+
+ hashmap_clear_free_free(m);
+ assert_se(hashmap_isempty(m));
+
+ assert_se(hashmap_put(m, strdup("key 1"), strdup("value 1")) == 1);
+ assert_se(hashmap_put(m, strdup("key 2"), strdup("value 2")) == 1);
+ assert_se(hashmap_put(m, strdup("key 3"), strdup("value 3")) == 1);
+
+ hashmap_clear_free_free(m);
+ assert_se(hashmap_isempty(m));
+}
+
+DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(test_hash_ops_key, char, string_hash_func, string_compare_func, free);
+DEFINE_PRIVATE_HASH_OPS_FULL(test_hash_ops_full, char, string_hash_func, string_compare_func, free, char, free);
+
+TEST(hashmap_clear_free_with_destructor) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+
+ m = hashmap_new(&test_hash_ops_key);
+ assert_se(m);
+
+ assert_se(hashmap_put(m, strdup("key 1"), NULL) == 1);
+ assert_se(hashmap_put(m, strdup("key 2"), NULL) == 1);
+ assert_se(hashmap_put(m, strdup("key 3"), NULL) == 1);
+
+ hashmap_clear_free(m);
+ assert_se(hashmap_isempty(m));
+ m = hashmap_free(m);
+
+ m = hashmap_new(&test_hash_ops_full);
+ assert_se(m);
+
+ assert_se(hashmap_put(m, strdup("key 1"), strdup("value 1")) == 1);
+ assert_se(hashmap_put(m, strdup("key 2"), strdup("value 2")) == 1);
+ assert_se(hashmap_put(m, strdup("key 3"), strdup("value 3")) == 1);
+
+ hashmap_clear_free(m);
+ assert_se(hashmap_isempty(m));
+}
+
+TEST(hashmap_reserve) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+
+ m = hashmap_new(&string_hash_ops);
+
+ assert_se(hashmap_reserve(m, 1) == 0);
+ assert_se(hashmap_buckets(m) < 1000);
+ assert_se(hashmap_reserve(m, 1000) == 0);
+ assert_se(hashmap_buckets(m) >= 1000);
+ assert_se(hashmap_isempty(m));
+
+ assert_se(hashmap_put(m, "key 1", (void*) "val 1") == 1);
+
+ assert_se(hashmap_reserve(m, UINT_MAX) == -ENOMEM);
+ assert_se(hashmap_reserve(m, UINT_MAX - 1) == -ENOMEM);
+}
+
+TEST(path_hashmap) {
+ _cleanup_hashmap_free_ Hashmap *h = NULL;
+
+ assert_se(h = hashmap_new(&path_hash_ops));
+
+ assert_se(hashmap_put(h, "foo", INT_TO_PTR(1)) >= 0);
+ assert_se(hashmap_put(h, "/foo", INT_TO_PTR(2)) >= 0);
+ assert_se(hashmap_put(h, "//foo", INT_TO_PTR(3)) == -EEXIST);
+ assert_se(hashmap_put(h, "//foox/", INT_TO_PTR(4)) >= 0);
+ assert_se(hashmap_put(h, "/foox////", INT_TO_PTR(5)) == -EEXIST);
+ assert_se(hashmap_put(h, "//././/foox//.//.", INT_TO_PTR(5)) == -EEXIST);
+ assert_se(hashmap_put(h, "foo//////bar/quux//", INT_TO_PTR(6)) >= 0);
+ assert_se(hashmap_put(h, "foo/bar//quux/", INT_TO_PTR(8)) == -EEXIST);
+ assert_se(hashmap_put(h, "foo./ba.r//.quux/", INT_TO_PTR(9)) >= 0);
+ assert_se(hashmap_put(h, "foo./ba.r//.//.quux///./", INT_TO_PTR(10)) == -EEXIST);
+
+ assert_se(hashmap_get(h, "foo") == INT_TO_PTR(1));
+ assert_se(hashmap_get(h, "foo/") == INT_TO_PTR(1));
+ assert_se(hashmap_get(h, "foo////") == INT_TO_PTR(1));
+ assert_se(hashmap_get(h, "/foo") == INT_TO_PTR(2));
+ assert_se(hashmap_get(h, "//foo") == INT_TO_PTR(2));
+ assert_se(hashmap_get(h, "/////foo////") == INT_TO_PTR(2));
+ assert_se(hashmap_get(h, "/////foox////") == INT_TO_PTR(4));
+ assert_se(hashmap_get(h, "/.///./foox//.//") == INT_TO_PTR(4));
+ assert_se(hashmap_get(h, "/foox/") == INT_TO_PTR(4));
+ assert_se(hashmap_get(h, "/foox") == INT_TO_PTR(4));
+ assert_se(!hashmap_get(h, "foox"));
+ assert_se(hashmap_get(h, "foo/bar/quux") == INT_TO_PTR(6));
+ assert_se(hashmap_get(h, "foo////bar////quux/////") == INT_TO_PTR(6));
+ assert_se(!hashmap_get(h, "/foo////bar////quux/////"));
+ assert_se(hashmap_get(h, "foo././//ba.r////.quux///.//.") == INT_TO_PTR(9));
+}
+
+TEST(string_strv_hashmap) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+ char **s;
+
+ assert_se(string_strv_hashmap_put(&m, "foo", "bar") == 1);
+ assert_se(string_strv_hashmap_put(&m, "foo", "bar") == 0);
+ assert_se(string_strv_hashmap_put(&m, "foo", "BAR") == 1);
+ assert_se(string_strv_hashmap_put(&m, "foo", "BAR") == 0);
+ assert_se(string_strv_hashmap_put(&m, "foo", "bar") == 0);
+ assert_se(hashmap_contains(m, "foo"));
+
+ s = hashmap_get(m, "foo");
+ assert_se(strv_equal(s, STRV_MAKE("bar", "BAR")));
+
+ assert_se(string_strv_hashmap_put(&m, "xxx", "bar") == 1);
+ assert_se(string_strv_hashmap_put(&m, "xxx", "bar") == 0);
+ assert_se(string_strv_hashmap_put(&m, "xxx", "BAR") == 1);
+ assert_se(string_strv_hashmap_put(&m, "xxx", "BAR") == 0);
+ assert_se(string_strv_hashmap_put(&m, "xxx", "bar") == 0);
+ assert_se(hashmap_contains(m, "xxx"));
+
+ s = hashmap_get(m, "xxx");
+ assert_se(strv_equal(s, STRV_MAKE("bar", "BAR")));
+}
+
+TEST(hashmap_dump_sorted) {
+ static void * const expected[] = { UINT_TO_PTR(123U), UINT_TO_PTR(12U), UINT_TO_PTR(345U), };
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+ _cleanup_free_ void **vals = NULL;
+ size_t n;
+
+ assert_se(m = hashmap_new(&string_hash_ops));
+
+ assert_se(hashmap_dump_sorted(m, &vals, &n) >= 0);
+ assert_se(n == 0);
+ assert_se(!vals);
+
+ assert_se(hashmap_put(m, "key 0", expected[0]) == 1);
+ assert_se(hashmap_put(m, "key 1", expected[1]) == 1);
+ assert_se(hashmap_put(m, "key 2", expected[2]) == 1);
+
+ assert_se(hashmap_dump_sorted(m, &vals, &n) >= 0);
+ assert_se(n == ELEMENTSOF(expected));
+ assert_se(memcmp(vals, expected, n * sizeof(void*)) == 0);
+
+ vals = mfree(vals);
+ m = hashmap_free(m);
+
+ assert_se(m = hashmap_new(NULL));
+
+ assert_se(hashmap_dump_sorted(m, &vals, &n) >= 0);
+ assert_se(n == 0);
+ assert_se(!vals);
+
+ assert_se(hashmap_put(m, UINT_TO_PTR(333U), expected[2]) == 1);
+ assert_se(hashmap_put(m, UINT_TO_PTR(222U), expected[1]) == 1);
+ assert_se(hashmap_put(m, UINT_TO_PTR(111U), expected[0]) == 1);
+
+ assert_se(hashmap_dump_sorted(m, &vals, &n) >= 0);
+ assert_se(n == ELEMENTSOF(expected));
+ assert_se(memcmp(vals, expected, n * sizeof(void*)) == 0);
+}
+
+/* Signal to test-hashmap.c that tests from this compilation unit were run. */
+extern int n_extern_tests_run;
+TEST(ensure_extern_hashmap_tests) {
+ n_extern_tests_run++;
+}
diff --git a/src/test/test-hashmap.c b/src/test/test-hashmap.c
new file mode 100644
index 0000000..5daa0e6
--- /dev/null
+++ b/src/test/test-hashmap.c
@@ -0,0 +1,171 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "hashmap.h"
+#include "string-util.h"
+#include "tests.h"
+
+unsigned custom_counter = 0;
+static void custom_destruct(void* p) {
+ custom_counter--;
+ free(p);
+}
+
+DEFINE_HASH_OPS_FULL(boring_hash_ops, char, string_hash_func, string_compare_func, free, char, free);
+DEFINE_HASH_OPS_FULL(custom_hash_ops, char, string_hash_func, string_compare_func, custom_destruct, char, custom_destruct);
+
+TEST(ordered_hashmap_next) {
+ _cleanup_ordered_hashmap_free_ OrderedHashmap *m = NULL;
+ int i;
+
+ assert_se(m = ordered_hashmap_new(NULL));
+ for (i = -2; i <= 2; i++)
+ assert_se(ordered_hashmap_put(m, INT_TO_PTR(i), INT_TO_PTR(i+10)) == 1);
+ for (i = -2; i <= 1; i++)
+ assert_se(ordered_hashmap_next(m, INT_TO_PTR(i)) == INT_TO_PTR(i+11));
+ assert_se(!ordered_hashmap_next(m, INT_TO_PTR(2)));
+ assert_se(!ordered_hashmap_next(NULL, INT_TO_PTR(1)));
+ assert_se(!ordered_hashmap_next(m, INT_TO_PTR(3)));
+}
+
+TEST(uint64_compare_func) {
+ const uint64_t a = 0x100, b = 0x101;
+
+ assert_se(uint64_compare_func(&a, &a) == 0);
+ assert_se(uint64_compare_func(&a, &b) == -1);
+ assert_se(uint64_compare_func(&b, &a) == 1);
+}
+
+TEST(trivial_compare_func) {
+ assert_se(trivial_compare_func(INT_TO_PTR('a'), INT_TO_PTR('a')) == 0);
+ assert_se(trivial_compare_func(INT_TO_PTR('a'), INT_TO_PTR('b')) == -1);
+ assert_se(trivial_compare_func(INT_TO_PTR('b'), INT_TO_PTR('a')) == 1);
+}
+
+TEST(string_compare_func) {
+ assert_se(string_compare_func("fred", "wilma") != 0);
+ assert_se(string_compare_func("fred", "fred") == 0);
+}
+
+static void compare_cache(Hashmap *map, IteratedCache *cache) {
+ const void **keys = NULL, **values = NULL;
+ unsigned num, idx;
+ void *k, *v;
+
+ assert_se(iterated_cache_get(cache, &keys, &values, &num) == 0);
+ assert_se(num == 0 || keys);
+ assert_se(num == 0 || values);
+
+ idx = 0;
+ HASHMAP_FOREACH_KEY(v, k, map) {
+ assert_se(v == values[idx]);
+ assert_se(k == keys[idx]);
+
+ idx++;
+ }
+
+ assert_se(idx == num);
+}
+
+TEST(iterated_cache) {
+ Hashmap *m;
+ IteratedCache *c;
+
+ assert_se(m = hashmap_new(NULL));
+ assert_se(c = hashmap_iterated_cache_new(m));
+ compare_cache(m, c);
+
+ for (int stage = 0; stage < 100; stage++) {
+
+ for (int i = 0; i < 100; i++) {
+ int foo = stage * 1000 + i;
+
+ assert_se(hashmap_put(m, INT_TO_PTR(foo), INT_TO_PTR(foo + 777)) == 1);
+ }
+
+ compare_cache(m, c);
+
+ if (!(stage % 10)) {
+ for (int i = 0; i < 100; i++) {
+ int foo = stage * 1000 + i;
+
+ assert_se(hashmap_remove(m, INT_TO_PTR(foo)) == INT_TO_PTR(foo + 777));
+ }
+
+ compare_cache(m, c);
+ }
+ }
+
+ hashmap_clear(m);
+ compare_cache(m, c);
+
+ assert_se(hashmap_free(m) == NULL);
+ assert_se(iterated_cache_free(c) == NULL);
+}
+
+TEST(hashmap_put_strdup) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+ char *s;
+
+ /* We don't have ordered_hashmap_put_strdup() yet. If it is added,
+ * these tests should be moved to test-hashmap-plain.c. */
+
+ assert_se(hashmap_put_strdup(&m, "foo", "bar") == 1);
+ assert_se(hashmap_put_strdup(&m, "foo", "bar") == 0);
+ assert_se(hashmap_put_strdup(&m, "foo", "BAR") == -EEXIST);
+ assert_se(hashmap_put_strdup(&m, "foo", "bar") == 0);
+ assert_se(hashmap_contains(m, "foo"));
+
+ s = hashmap_get(m, "foo");
+ assert_se(streq(s, "bar"));
+
+ assert_se(hashmap_put_strdup(&m, "xxx", "bar") == 1);
+ assert_se(hashmap_put_strdup(&m, "xxx", "bar") == 0);
+ assert_se(hashmap_put_strdup(&m, "xxx", "BAR") == -EEXIST);
+ assert_se(hashmap_put_strdup(&m, "xxx", "bar") == 0);
+ assert_se(hashmap_contains(m, "xxx"));
+
+ s = hashmap_get(m, "xxx");
+ assert_se(streq(s, "bar"));
+}
+
+TEST(hashmap_put_strdup_null) {
+ _cleanup_hashmap_free_ Hashmap *m = NULL;
+ char *s;
+
+ assert_se(hashmap_put_strdup(&m, "foo", "bar") == 1);
+ assert_se(hashmap_put_strdup(&m, "foo", "bar") == 0);
+ assert_se(hashmap_put_strdup(&m, "foo", NULL) == -EEXIST);
+ assert_se(hashmap_put_strdup(&m, "foo", "bar") == 0);
+ assert_se(hashmap_contains(m, "foo"));
+
+ s = hashmap_get(m, "foo");
+ assert_se(streq(s, "bar"));
+
+ assert_se(hashmap_put_strdup(&m, "xxx", NULL) == 1);
+ assert_se(hashmap_put_strdup(&m, "xxx", "bar") == -EEXIST);
+ assert_se(hashmap_put_strdup(&m, "xxx", NULL) == 0);
+ assert_se(hashmap_contains(m, "xxx"));
+
+ s = hashmap_get(m, "xxx");
+ assert_se(s == NULL);
+}
+
+/* This file tests in test-hashmap-plain.c, and tests in test-hashmap-ordered.c, which is generated
+ * from test-hashmap-plain.c. Hashmap tests should be added to test-hashmap-plain.c, and here only if
+ * they don't apply to ordered hashmaps. */
+
+/* This variable allows us to assert that the tests from different compilation units were actually run. */
+int n_extern_tests_run = 0;
+
+static int intro(void) {
+ assert_se(n_extern_tests_run == 0);
+ return EXIT_SUCCESS;
+}
+
+static int outro(void) {
+ /* Ensure hashmap and ordered_hashmap were tested. */
+ assert_se(n_extern_tests_run == 2);
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_FULL(LOG_INFO, intro, outro);
diff --git a/src/test/test-hexdecoct.c b/src/test/test-hexdecoct.c
new file mode 100644
index 0000000..f884008
--- /dev/null
+++ b/src/test/test-hexdecoct.c
@@ -0,0 +1,548 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+
+#include "alloc-util.h"
+#include "hexdecoct.h"
+#include "macro.h"
+#include "random-util.h"
+#include "string-util.h"
+#include "tests.h"
+
+TEST(hexchar) {
+ assert_se(hexchar(0xa) == 'a');
+ assert_se(hexchar(0x0) == '0');
+}
+
+TEST(unhexchar) {
+ assert_se(unhexchar('a') == 0xA);
+ assert_se(unhexchar('A') == 0xA);
+ assert_se(unhexchar('0') == 0x0);
+}
+
+TEST(base32hexchar) {
+ assert_se(base32hexchar(0) == '0');
+ assert_se(base32hexchar(9) == '9');
+ assert_se(base32hexchar(10) == 'A');
+ assert_se(base32hexchar(31) == 'V');
+}
+
+TEST(unbase32hexchar) {
+ assert_se(unbase32hexchar('0') == 0);
+ assert_se(unbase32hexchar('9') == 9);
+ assert_se(unbase32hexchar('A') == 10);
+ assert_se(unbase32hexchar('V') == 31);
+ assert_se(unbase32hexchar('=') == -EINVAL);
+}
+
+TEST(base64char) {
+ assert_se(base64char(0) == 'A');
+ assert_se(base64char(26) == 'a');
+ assert_se(base64char(63) == '/');
+}
+
+TEST(unbase64char) {
+ assert_se(unbase64char('A') == 0);
+ assert_se(unbase64char('Z') == 25);
+ assert_se(unbase64char('a') == 26);
+ assert_se(unbase64char('z') == 51);
+ assert_se(unbase64char('0') == 52);
+ assert_se(unbase64char('9') == 61);
+ assert_se(unbase64char('+') == 62);
+ assert_se(unbase64char('/') == 63);
+ assert_se(unbase64char('=') == -EINVAL);
+}
+
+TEST(octchar) {
+ assert_se(octchar(00) == '0');
+ assert_se(octchar(07) == '7');
+}
+
+TEST(unoctchar) {
+ assert_se(unoctchar('0') == 00);
+ assert_se(unoctchar('7') == 07);
+}
+
+TEST(decchar) {
+ assert_se(decchar(0) == '0');
+ assert_se(decchar(9) == '9');
+}
+
+TEST(undecchar) {
+ assert_se(undecchar('0') == 0);
+ assert_se(undecchar('9') == 9);
+}
+
+static void test_hexmem_one(const char *in, const char *expected) {
+ _cleanup_free_ char *result = NULL;
+ _cleanup_free_ void *mem = NULL;
+ size_t len;
+
+ assert_se(result = hexmem(in, strlen_ptr(in)));
+ log_debug("hexmem(\"%s\") → \"%s\" (expected: \"%s\")", strnull(in), result, expected);
+ assert_se(streq(result, expected));
+
+ assert_se(unhexmem(result, SIZE_MAX, &mem, &len) >= 0);
+ assert_se(memcmp_safe(mem, in, len) == 0);
+}
+
+TEST(hexmem) {
+ test_hexmem_one(NULL, "");
+ test_hexmem_one("", "");
+ test_hexmem_one("foo", "666f6f");
+}
+
+static void test_unhexmem_one(const char *s, size_t l, int retval) {
+ _cleanup_free_ char *hex = NULL;
+ _cleanup_free_ void *mem = NULL;
+ size_t len;
+
+ assert_se(unhexmem(s, l, &mem, &len) == retval);
+ if (retval == 0) {
+ char *answer;
+
+ if (l == SIZE_MAX)
+ l = strlen(s);
+
+ assert_se(hex = hexmem(mem, len));
+ answer = strndupa_safe(strempty(s), l);
+ assert_se(streq(delete_chars(answer, WHITESPACE), hex));
+ }
+}
+
+TEST(unhexmem) {
+ const char *hex = "efa2149213";
+ const char *hex_space = " e f a\n 2\r 14\n\r\t9\t2 \n1\r3 \r\r\t";
+ const char *hex_invalid = "efa214921o";
+
+ test_unhexmem_one(NULL, 0, 0);
+ test_unhexmem_one("", 0, 0);
+ test_unhexmem_one("", SIZE_MAX, 0);
+ test_unhexmem_one(" \n \t\r \t\t \n\n\n", SIZE_MAX, 0);
+ test_unhexmem_one(hex_invalid, strlen(hex_invalid), -EINVAL);
+ test_unhexmem_one(hex_invalid, (size_t) - 1, -EINVAL);
+ test_unhexmem_one(hex, strlen(hex) - 1, -EPIPE);
+ test_unhexmem_one(hex, strlen(hex), 0);
+ test_unhexmem_one(hex, SIZE_MAX, 0);
+ test_unhexmem_one(hex_space, strlen(hex_space), 0);
+ test_unhexmem_one(hex_space, SIZE_MAX, 0);
+}
+
+/* https://tools.ietf.org/html/rfc4648#section-10 */
+TEST(base32hexmem) {
+ char *b32;
+
+ b32 = base32hexmem("", STRLEN(""), true);
+ assert_se(b32);
+ assert_se(streq(b32, ""));
+ free(b32);
+
+ b32 = base32hexmem("f", STRLEN("f"), true);
+ assert_se(b32);
+ assert_se(streq(b32, "CO======"));
+ free(b32);
+
+ b32 = base32hexmem("fo", STRLEN("fo"), true);
+ assert_se(b32);
+ assert_se(streq(b32, "CPNG===="));
+ free(b32);
+
+ b32 = base32hexmem("foo", STRLEN("foo"), true);
+ assert_se(b32);
+ assert_se(streq(b32, "CPNMU==="));
+ free(b32);
+
+ b32 = base32hexmem("foob", STRLEN("foob"), true);
+ assert_se(b32);
+ assert_se(streq(b32, "CPNMUOG="));
+ free(b32);
+
+ b32 = base32hexmem("fooba", STRLEN("fooba"), true);
+ assert_se(b32);
+ assert_se(streq(b32, "CPNMUOJ1"));
+ free(b32);
+
+ b32 = base32hexmem("foobar", STRLEN("foobar"), true);
+ assert_se(b32);
+ assert_se(streq(b32, "CPNMUOJ1E8======"));
+ free(b32);
+
+ b32 = base32hexmem("", STRLEN(""), false);
+ assert_se(b32);
+ assert_se(streq(b32, ""));
+ free(b32);
+
+ b32 = base32hexmem("f", STRLEN("f"), false);
+ assert_se(b32);
+ assert_se(streq(b32, "CO"));
+ free(b32);
+
+ b32 = base32hexmem("fo", STRLEN("fo"), false);
+ assert_se(b32);
+ assert_se(streq(b32, "CPNG"));
+ free(b32);
+
+ b32 = base32hexmem("foo", STRLEN("foo"), false);
+ assert_se(b32);
+ assert_se(streq(b32, "CPNMU"));
+ free(b32);
+
+ b32 = base32hexmem("foob", STRLEN("foob"), false);
+ assert_se(b32);
+ assert_se(streq(b32, "CPNMUOG"));
+ free(b32);
+
+ b32 = base32hexmem("fooba", STRLEN("fooba"), false);
+ assert_se(b32);
+ assert_se(streq(b32, "CPNMUOJ1"));
+ free(b32);
+
+ b32 = base32hexmem("foobar", STRLEN("foobar"), false);
+ assert_se(b32);
+ assert_se(streq(b32, "CPNMUOJ1E8"));
+ free(b32);
+}
+
+static void test_unbase32hexmem_one(const char *hex, bool padding, int retval, const char *ans) {
+ _cleanup_free_ void *mem = NULL;
+ size_t len;
+
+ assert_se(unbase32hexmem(hex, SIZE_MAX, padding, &mem, &len) == retval);
+ if (retval == 0) {
+ char *str;
+
+ str = strndupa_safe(mem, len);
+ assert_se(streq(str, ans));
+ }
+}
+
+TEST(unbase32hexmem) {
+ test_unbase32hexmem_one("", true, 0, "");
+
+ test_unbase32hexmem_one("CO======", true, 0, "f");
+ test_unbase32hexmem_one("CPNG====", true, 0, "fo");
+ test_unbase32hexmem_one("CPNMU===", true, 0, "foo");
+ test_unbase32hexmem_one("CPNMUOG=", true, 0, "foob");
+ test_unbase32hexmem_one("CPNMUOJ1", true, 0, "fooba");
+ test_unbase32hexmem_one("CPNMUOJ1E8======", true, 0, "foobar");
+
+ test_unbase32hexmem_one("A", true, -EINVAL, NULL);
+ test_unbase32hexmem_one("A=======", true, -EINVAL, NULL);
+ test_unbase32hexmem_one("AAA=====", true, -EINVAL, NULL);
+ test_unbase32hexmem_one("AAAAAA==", true, -EINVAL, NULL);
+ test_unbase32hexmem_one("AB======", true, -EINVAL, NULL);
+ test_unbase32hexmem_one("AAAB====", true, -EINVAL, NULL);
+ test_unbase32hexmem_one("AAAAB===", true, -EINVAL, NULL);
+ test_unbase32hexmem_one("AAAAAAB=", true, -EINVAL, NULL);
+
+ test_unbase32hexmem_one("XPNMUOJ1", true, -EINVAL, NULL);
+ test_unbase32hexmem_one("CXNMUOJ1", true, -EINVAL, NULL);
+ test_unbase32hexmem_one("CPXMUOJ1", true, -EINVAL, NULL);
+ test_unbase32hexmem_one("CPNXUOJ1", true, -EINVAL, NULL);
+ test_unbase32hexmem_one("CPNMXOJ1", true, -EINVAL, NULL);
+ test_unbase32hexmem_one("CPNMUXJ1", true, -EINVAL, NULL);
+ test_unbase32hexmem_one("CPNMUOX1", true, -EINVAL, NULL);
+ test_unbase32hexmem_one("CPNMUOJX", true, -EINVAL, NULL);
+
+ test_unbase32hexmem_one("", false, 0, "");
+ test_unbase32hexmem_one("CO", false, 0, "f");
+ test_unbase32hexmem_one("CPNG", false, 0, "fo");
+ test_unbase32hexmem_one("CPNMU", false, 0, "foo");
+ test_unbase32hexmem_one("CPNMUOG", false, 0, "foob");
+ test_unbase32hexmem_one("CPNMUOJ1", false, 0, "fooba");
+ test_unbase32hexmem_one("CPNMUOJ1E8", false, 0, "foobar");
+ test_unbase32hexmem_one("CPNMUOG=", false, -EINVAL, NULL);
+ test_unbase32hexmem_one("CPNMUOJ1E8======", false, -EINVAL, NULL);
+
+ test_unbase32hexmem_one("A", false, -EINVAL, NULL);
+ test_unbase32hexmem_one("AAA", false, -EINVAL, NULL);
+ test_unbase32hexmem_one("AAAAAA", false, -EINVAL, NULL);
+ test_unbase32hexmem_one("AB", false, -EINVAL, NULL);
+ test_unbase32hexmem_one("AAAB", false, -EINVAL, NULL);
+ test_unbase32hexmem_one("AAAAB", false, -EINVAL, NULL);
+ test_unbase32hexmem_one("AAAAAAB", false, -EINVAL, NULL);
+}
+
+/* https://tools.ietf.org/html/rfc4648#section-10 */
+TEST(base64mem) {
+ char *b64;
+
+ assert_se(base64mem("", STRLEN(""), &b64) == 0);
+ assert_se(streq(b64, ""));
+ free(b64);
+
+ assert_se(base64mem("f", STRLEN("f"), &b64) == 4);
+ assert_se(streq(b64, "Zg=="));
+ free(b64);
+
+ assert_se(base64mem("fo", STRLEN("fo"), &b64) == 4);
+ assert_se(streq(b64, "Zm8="));
+ free(b64);
+
+ assert_se(base64mem("foo", STRLEN("foo"), &b64) == 4);
+ assert_se(streq(b64, "Zm9v"));
+ free(b64);
+
+ assert_se(base64mem("foob", STRLEN("foob"), &b64) == 8);
+ assert_se(streq(b64, "Zm9vYg=="));
+ free(b64);
+
+ assert_se(base64mem("fooba", STRLEN("fooba"), &b64) == 8);
+ assert_se(streq(b64, "Zm9vYmE="));
+ free(b64);
+
+ assert_se(base64mem("foobar", STRLEN("foobar"), &b64) == 8);
+ assert_se(streq(b64, "Zm9vYmFy"));
+ free(b64);
+}
+
+TEST(base64mem_linebreak) {
+ uint8_t data[4096];
+
+ for (size_t i = 0; i < 20; i++) {
+ _cleanup_free_ char *encoded = NULL;
+ _cleanup_free_ void *decoded = NULL;
+ size_t decoded_size;
+ uint64_t n, m;
+ ssize_t l;
+
+ /* Try a bunch of differently sized blobs */
+ n = random_u64_range(sizeof(data));
+ random_bytes(data, n);
+
+ /* Break at various different columns */
+ m = 1 + random_u64_range(n + 5);
+
+ l = base64mem_full(data, n, m, &encoded);
+ assert_se(l >= 0);
+ assert_se(encoded);
+ assert_se((size_t) l == strlen(encoded));
+
+ assert_se(unbase64mem(encoded, SIZE_MAX, &decoded, &decoded_size) >= 0);
+ assert_se(decoded_size == n);
+ assert_se(memcmp(data, decoded, n) == 0);
+
+ /* Also try in secure mode */
+ decoded = mfree(decoded);
+ decoded_size = 0;
+ assert_se(unbase64mem_full(encoded, SIZE_MAX, /* secure= */ true, &decoded, &decoded_size) >= 0);
+ assert_se(decoded_size == n);
+ assert_se(memcmp(data, decoded, n) == 0);
+
+ for (size_t j = 0; j < (size_t) l; j++)
+ assert_se((encoded[j] == '\n') == (j % (m + 1) == m));
+ }
+}
+
+static void test_base64_append_one(char **buf, size_t *len, const char *in, const char *expected) {
+ ssize_t new_len;
+
+ new_len = base64_append(buf, *len, in, strlen_ptr(in), 8, 12);
+ assert_se(new_len >= 0);
+ log_debug("base64_append_one(\"%s\")\nresult:\n%s\nexpected:\n%s", in, strnull(*buf), strnull(expected));
+ assert_se((size_t) new_len == strlen_ptr(*buf));
+ assert_se(streq_ptr(*buf, expected));
+ *len = new_len;
+}
+
+TEST(base64_append) {
+ _cleanup_free_ char *buf = NULL;
+ size_t len = 0;
+
+ test_base64_append_one(&buf, &len, "", NULL);
+ test_base64_append_one(&buf, &len, "f",
+ "Zg==");
+ test_base64_append_one(&buf, &len, "fo",
+ "Zg== Zm8=");
+ test_base64_append_one(&buf, &len, "foo",
+ "Zg== Zm8=\n"
+ " Zm9v");
+ test_base64_append_one(&buf, &len, "foob",
+ "Zg== Zm8=\n"
+ " Zm9v\n"
+ " Zm9v\n"
+ " Yg==");
+ test_base64_append_one(&buf, &len, "fooba",
+ "Zg== Zm8=\n"
+ " Zm9v\n"
+ " Zm9v\n"
+ " Yg==\n"
+ " Zm9v\n"
+ " YmE=");
+ test_base64_append_one(&buf, &len, "foobar",
+ "Zg== Zm8=\n"
+ " Zm9v\n"
+ " Zm9v\n"
+ " Yg==\n"
+ " Zm9v\n"
+ " YmE=\n"
+ " Zm9v\n"
+ " YmFy");
+
+ assert_se(free_and_strdup(&buf, "hogehogehogehoge") >= 0);
+ len = strlen(buf);
+
+ test_base64_append_one(&buf, &len, "",
+ "hogehogehogehoge");
+ test_base64_append_one(&buf, &len, "f",
+ "hogehogehogehoge\n"
+ " Zg==");
+ test_base64_append_one(&buf, &len, "fo",
+ "hogehogehogehoge\n"
+ " Zg==\n"
+ " Zm8=");
+ test_base64_append_one(&buf, &len, "foo",
+ "hogehogehogehoge\n"
+ " Zg==\n"
+ " Zm8=\n"
+ " Zm9v");
+ test_base64_append_one(&buf, &len, "foob",
+ "hogehogehogehoge\n"
+ " Zg==\n"
+ " Zm8=\n"
+ " Zm9v\n"
+ " Zm9v\n"
+ " Yg==");
+ test_base64_append_one(&buf, &len, "fooba",
+ "hogehogehogehoge\n"
+ " Zg==\n"
+ " Zm8=\n"
+ " Zm9v\n"
+ " Zm9v\n"
+ " Yg==\n"
+ " Zm9v\n"
+ " YmE=");
+ test_base64_append_one(&buf, &len, "foobar",
+ "hogehogehogehoge\n"
+ " Zg==\n"
+ " Zm8=\n"
+ " Zm9v\n"
+ " Zm9v\n"
+ " Yg==\n"
+ " Zm9v\n"
+ " YmE=\n"
+ " Zm9v\n"
+ " YmFy");
+
+ assert_se(free_and_strdup(&buf, "hogehogehogehoge") >= 0);
+ len = strlen(buf);
+
+ test_base64_append_one(&buf, &len, "foobarfoobarfoobarfoobar",
+ "hogehogehogehoge\n"
+ " Zm9v\n"
+ " YmFy\n"
+ " Zm9v\n"
+ " YmFy\n"
+ " Zm9v\n"
+ " YmFy\n"
+ " Zm9v\n"
+ " YmFy");
+
+ assert_se(free_and_strdup(&buf, "aaa") >= 0);
+ len = strlen(buf);
+
+ test_base64_append_one(&buf, &len, "foobarfoobarfoobarfoobar",
+ "aaa Zm9vYmFy\n"
+ " Zm9vYmFy\n"
+ " Zm9vYmFy\n"
+ " Zm9vYmFy");
+}
+
+static void test_unbase64mem_one(const char *input, const char *output, int ret) {
+ _cleanup_free_ void *buffer = NULL;
+ size_t size = 0;
+
+ assert_se(unbase64mem(input, SIZE_MAX, &buffer, &size) == ret);
+ if (ret >= 0) {
+ assert_se(size == strlen(output));
+ assert_se(memcmp(buffer, output, size) == 0);
+ assert_se(((char*) buffer)[size] == 0);
+ }
+
+ /* also try in secure mode */
+ buffer = mfree(buffer);
+ size = 0;
+
+ assert_se(unbase64mem_full(input, SIZE_MAX, /* secure=*/ true, &buffer, &size) == ret);
+ if (ret >= 0) {
+ assert_se(size == strlen(output));
+ assert_se(memcmp(buffer, output, size) == 0);
+ assert_se(((char*) buffer)[size] == 0);
+ }
+}
+
+TEST(unbase64mem) {
+
+ test_unbase64mem_one("", "", 0);
+ test_unbase64mem_one("Zg==", "f", 0);
+ test_unbase64mem_one("Zm8=", "fo", 0);
+ test_unbase64mem_one("Zm9v", "foo", 0);
+ test_unbase64mem_one("Zm9vYg==", "foob", 0);
+ test_unbase64mem_one("Zm9vYmE=", "fooba", 0);
+ test_unbase64mem_one("Zm9vYmFy", "foobar", 0);
+
+ test_unbase64mem_one(" ", "", 0);
+ test_unbase64mem_one(" \n\r ", "", 0);
+ test_unbase64mem_one(" Zg\n== ", "f", 0);
+ test_unbase64mem_one(" Zm 8=\r", "fo", 0);
+ test_unbase64mem_one(" Zm9\n\r\r\nv ", "foo", 0);
+ test_unbase64mem_one(" Z m9vYg==\n\r", "foob", 0);
+ test_unbase64mem_one(" Zm 9vYmE= ", "fooba", 0);
+ test_unbase64mem_one(" Z m9v YmFy ", "foobar", 0);
+
+ test_unbase64mem_one("A", NULL, -EPIPE);
+ test_unbase64mem_one("A====", NULL, -EINVAL);
+ test_unbase64mem_one("AAB==", NULL, -EINVAL);
+ test_unbase64mem_one(" A A A B = ", NULL, -EINVAL);
+ test_unbase64mem_one(" Z m 8 = q u u x ", NULL, -ENAMETOOLONG);
+}
+
+TEST(hexdump) {
+ uint8_t data[146];
+ unsigned i;
+
+ hexdump(stdout, NULL, 0);
+ hexdump(stdout, "", 0);
+ hexdump(stdout, "", 1);
+ hexdump(stdout, "x", 1);
+ hexdump(stdout, "x", 2);
+ hexdump(stdout, "foobar", 7);
+ hexdump(stdout, "f\nobar", 7);
+ hexdump(stdout, "xxxxxxxxxxxxxxxxxxxxyz", 23);
+
+ for (i = 0; i < ELEMENTSOF(data); i++)
+ data[i] = i*2;
+
+ hexdump(stdout, data, sizeof(data));
+}
+
+TEST(base64withwithouturl) {
+ static const uint8_t plaintext[] = {
+ 0xcc, 0xa1, 0x72, 0x22, 0xae, 0xda, 0x66, 0x7e, 0x04, 0xa6, 0xe0, 0x82,
+ 0x9a, 0x97, 0x05, 0xf6, 0x33, 0xe0, 0x0f, 0xc2, 0x45, 0x13, 0x58, 0x3f,
+ 0xc5, 0xf4, 0xf4, 0x31, 0xab, 0x3c, 0x5f, 0x83, 0x34, 0x5b, 0x27, 0x32,
+ 0x8a, 0x04, 0x6c, 0x43, 0x82, 0x07, 0xe3, 0x2c, 0xac, 0xb9, 0xfb, 0xac,
+ 0xd0, 0x03, 0x91, 0x42, 0xcb, 0xa4, 0xde, 0x87, 0x86, 0x85, 0x10, 0xbb,
+ 0xb7, 0x5b, 0x4b, 0xc8, 0xa0, 0xf4, 0x22, 0x1d, 0x15, 0x71, 0x87, 0x9d,
+ 0xbf, 0x9f, 0xa9, 0xf1, 0xee, 0xa2, 0xb6, 0xaa, 0xc8, 0xc3, 0x37, 0x9c,
+ 0xbb, 0xdf, 0x3e, 0xac, 0xdc, 0x94, 0x54, 0x38, 0x56, 0x07, 0x34, 0xb4,
+ 0x3c, 0xcc, 0x31, 0x13
+ };
+
+ _cleanup_free_ void *buffer = NULL;
+ size_t size;
+
+ /* This is regular base64 */
+ assert_se(unbase64mem("zKFyIq7aZn4EpuCCmpcF9jPgD8JFE1g/xfT0Mas8X4M0WycyigRsQ4IH4yysufus0AORQsuk3oeGhRC7t1tLyKD0Ih0VcYedv5+p8e6itqrIwzecu98+rNyUVDhWBzS0PMwxEw==", SIZE_MAX, &buffer, &size) >= 0);
+ assert_se(memcmp_nn(plaintext, sizeof(plaintext), buffer, size) == 0);
+ buffer = mfree(buffer);
+
+ /* This is the same but in base64url */
+ assert_se(unbase64mem("zKFyIq7aZn4EpuCCmpcF9jPgD8JFE1g_xfT0Mas8X4M0WycyigRsQ4IH4yysufus0AORQsuk3oeGhRC7t1tLyKD0Ih0VcYedv5-p8e6itqrIwzecu98-rNyUVDhWBzS0PMwxEw==", SIZE_MAX, &buffer, &size) >= 0);
+ assert_se(memcmp_nn(plaintext, sizeof(plaintext), buffer, size) == 0);
+
+ /* Hint: use xxd -i to generate the static C array from some data, and basenc --base64 + basenc
+ * --base64url to generate the correctly encoded base64 strings */
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-hmac.c b/src/test/test-hmac.c
new file mode 100644
index 0000000..1b788b1
--- /dev/null
+++ b/src/test/test-hmac.c
@@ -0,0 +1,68 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "hexdecoct.h"
+#include "hmac.h"
+#include "string-util.h"
+#include "tests.h"
+
+static void hmac_sha256_by_string(const char *key, const char *value, uint8_t res[static SHA256_DIGEST_SIZE]) {
+ hmac_sha256(key, strlen(key), value, strlen(value), res);
+}
+
+TEST(hmac) {
+ uint8_t result[SHA256_DIGEST_SIZE];
+ char *hex_result = NULL;
+
+ /* Results compared with output of 'echo -n "<input>" | openssl dgst -sha256 -hmac "<key>"' */
+
+ hmac_sha256_by_string("waldo",
+ "",
+ result);
+ hex_result = hexmem(result, sizeof(result));
+ assert_se(streq_ptr(hex_result, "cadd5e42114351181f3abff477641d88efb57d2b5641a1e5c6d623363a6d3bad"));
+ hex_result = mfree(hex_result);
+
+ hmac_sha256_by_string("waldo",
+ "baldohaldo",
+ result);
+ hex_result = hexmem(result, sizeof(result));
+ assert_se(streq_ptr(hex_result, "c47ad5031ba21605e52c6ca68090d66a2dd5ccf84efa4bace15361a8cba63cda"));
+ hex_result = mfree(hex_result);
+
+ hmac_sha256_by_string("waldo",
+ "baldo haldo",
+ result);
+ hex_result = hexmem(result, sizeof(result));
+ assert_se(streq_ptr(hex_result, "4e8974ad6c08b98cc2519cd1e27aa7195769fcf86db1dd7ceaab4d44c490ad69"));
+ hex_result = mfree(hex_result);
+
+ hmac_sha256_by_string("waldo",
+ "baldo 4e8974ad6c08b98cc2519cd1e27aa7195769fcf86db1dd7ceaab4d44c490ad69 haldo",
+ result);
+ hex_result = hexmem(result, sizeof(result));
+ assert_se(streq_ptr(hex_result, "039f3df430b19753ffb493e5b90708f75c5210b63c6bcbef3374eb3f0a3f97f7"));
+ hex_result = mfree(hex_result);
+
+ hmac_sha256_by_string("4e8974ad6c08b98cc2519cd1e27aa7195769fcf86db1dd7ceaab4d44c490ad69",
+ "baldo haldo",
+ result);
+ hex_result = hexmem(result, sizeof(result));
+ assert_se(streq_ptr(hex_result, "c4cfaf48077cbb0bbd177a09e59ec4c248f4ca771503410f5b54b98d88d2f47b"));
+ hex_result = mfree(hex_result);
+
+ hmac_sha256_by_string("4e8974ad6c08b98cc2519cd1e27aa7195769fcf86db1dd7ceaab4d44c490ad69",
+ "supercalifragilisticexpialidocious",
+ result);
+ hex_result = hexmem(result, sizeof(result));
+ assert_se(streq_ptr(hex_result, "2c059e7a63c4c3b23f47966a65fd2f8a2f5d7161e2e90d78ff68866b5c375cb7"));
+ hex_result = mfree(hex_result);
+
+ hmac_sha256_by_string("4e8974ad6c08b98cc2519cd1e27aa7195769fcf86db1dd7ceaab4d44c490ad69c47ad5031ba21605e52c6ca68090d66a2dd5ccf84efa4bace15361a8cba63cda",
+ "supercalifragilisticexpialidocious",
+ result);
+ hex_result = hexmem(result, sizeof(result));
+ assert_se(streq_ptr(hex_result, "1dd1d1d45b9d9f9673dc9983c968c46ff3168e03cfeb4156a219eba1af4cff5f"));
+ hex_result = mfree(hex_result);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-hostname-setup.c b/src/test/test-hostname-setup.c
new file mode 100644
index 0000000..94e5ece
--- /dev/null
+++ b/src/test/test-hostname-setup.c
@@ -0,0 +1,64 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "hostname-setup.h"
+#include "string-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+TEST(read_etc_hostname) {
+ _cleanup_(unlink_tempfilep) char path[] = "/tmp/hostname.XXXXXX";
+ char *hostname;
+ int fd;
+
+ fd = mkostemp_safe(path);
+ assert_se(fd > 0);
+ close(fd);
+
+ /* simple hostname */
+ assert_se(write_string_file(path, "foo", WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(read_etc_hostname(path, &hostname) == 0);
+ assert_se(streq(hostname, "foo"));
+ hostname = mfree(hostname);
+
+ /* with comment */
+ assert_se(write_string_file(path, "# comment\nfoo", WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(read_etc_hostname(path, &hostname) == 0);
+ assert_se(hostname);
+ assert_se(streq(hostname, "foo"));
+ hostname = mfree(hostname);
+
+ /* with comment and extra whitespace */
+ assert_se(write_string_file(path, "# comment\n\n foo ", WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(read_etc_hostname(path, &hostname) == 0);
+ assert_se(hostname);
+ assert_se(streq(hostname, "foo"));
+ hostname = mfree(hostname);
+
+ /* cleans up name */
+ assert_se(write_string_file(path, "!foo/bar.com", WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(read_etc_hostname(path, &hostname) == 0);
+ assert_se(hostname);
+ assert_se(streq(hostname, "foobar.com"));
+ hostname = mfree(hostname);
+
+ /* no value set */
+ hostname = (char*) 0x1234;
+ assert_se(write_string_file(path, "# nothing here\n", WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(read_etc_hostname(path, &hostname) == -ENOENT);
+ assert_se(hostname == (char*) 0x1234); /* does not touch argument on error */
+
+ /* nonexisting file */
+ assert_se(read_etc_hostname("/non/existing", &hostname) == -ENOENT);
+ assert_se(hostname == (char*) 0x1234); /* does not touch argument on error */
+}
+
+TEST(hostname_setup) {
+ hostname_setup(false);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-hostname-util.c b/src/test/test-hostname-util.c
new file mode 100644
index 0000000..77e9a19
--- /dev/null
+++ b/src/test/test-hostname-util.c
@@ -0,0 +1,116 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "fileio.h"
+#include "hostname-util.h"
+#include "string-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+TEST(hostname_is_valid) {
+ assert_se(hostname_is_valid("foobar", 0));
+ assert_se(hostname_is_valid("foobar.com", 0));
+ assert_se(!hostname_is_valid("foobar.com.", 0));
+ assert_se(hostname_is_valid("fooBAR", 0));
+ assert_se(hostname_is_valid("fooBAR.com", 0));
+ assert_se(!hostname_is_valid("fooBAR.", 0));
+ assert_se(!hostname_is_valid("fooBAR.com.", 0));
+ assert_se(!hostname_is_valid("fööbar", 0));
+ assert_se(!hostname_is_valid("", 0));
+ assert_se(!hostname_is_valid(".", 0));
+ assert_se(!hostname_is_valid("..", 0));
+ assert_se(!hostname_is_valid("foobar.", 0));
+ assert_se(!hostname_is_valid(".foobar", 0));
+ assert_se(!hostname_is_valid("foo..bar", 0));
+ assert_se(!hostname_is_valid("foo.bar..", 0));
+ assert_se(!hostname_is_valid("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 0));
+ assert_se(!hostname_is_valid("au-xph5-rvgrdsb5hcxc-47et3a5vvkrc-server-wyoz4elpdpe3.openstack.local", 0));
+
+ assert_se(hostname_is_valid("foobar", VALID_HOSTNAME_TRAILING_DOT));
+ assert_se(hostname_is_valid("foobar.com", VALID_HOSTNAME_TRAILING_DOT));
+ assert_se(hostname_is_valid("foobar.com.", VALID_HOSTNAME_TRAILING_DOT));
+ assert_se(hostname_is_valid("fooBAR", VALID_HOSTNAME_TRAILING_DOT));
+ assert_se(hostname_is_valid("fooBAR.com", VALID_HOSTNAME_TRAILING_DOT));
+ assert_se(!hostname_is_valid("fooBAR.", VALID_HOSTNAME_TRAILING_DOT));
+ assert_se(hostname_is_valid("fooBAR.com.", VALID_HOSTNAME_TRAILING_DOT));
+ assert_se(!hostname_is_valid("fööbar", VALID_HOSTNAME_TRAILING_DOT));
+ assert_se(!hostname_is_valid("", VALID_HOSTNAME_TRAILING_DOT));
+ assert_se(!hostname_is_valid(".", VALID_HOSTNAME_TRAILING_DOT));
+ assert_se(!hostname_is_valid("..", VALID_HOSTNAME_TRAILING_DOT));
+ assert_se(!hostname_is_valid("foobar.", VALID_HOSTNAME_TRAILING_DOT));
+ assert_se(!hostname_is_valid(".foobar", VALID_HOSTNAME_TRAILING_DOT));
+ assert_se(!hostname_is_valid("foo..bar", VALID_HOSTNAME_TRAILING_DOT));
+ assert_se(!hostname_is_valid("foo.bar..", VALID_HOSTNAME_TRAILING_DOT));
+ assert_se(!hostname_is_valid("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", VALID_HOSTNAME_TRAILING_DOT));
+}
+
+TEST(hostname_cleanup) {
+ char *s;
+
+ s = strdupa_safe("foobar");
+ assert_se(streq(hostname_cleanup(s), "foobar"));
+ s = strdupa_safe("foobar.com");
+ assert_se(streq(hostname_cleanup(s), "foobar.com"));
+ s = strdupa_safe("foobar.com.");
+ assert_se(streq(hostname_cleanup(s), "foobar.com"));
+ s = strdupa_safe("foo-bar.-com-.");
+ assert_se(streq(hostname_cleanup(s), "foo-bar.com"));
+ s = strdupa_safe("foo-bar-.-com-.");
+ assert_se(streq(hostname_cleanup(s), "foo-bar--com"));
+ s = strdupa_safe("--foo-bar.-com");
+ assert_se(streq(hostname_cleanup(s), "foo-bar.com"));
+ s = strdupa_safe("fooBAR");
+ assert_se(streq(hostname_cleanup(s), "fooBAR"));
+ s = strdupa_safe("fooBAR.com");
+ assert_se(streq(hostname_cleanup(s), "fooBAR.com"));
+ s = strdupa_safe("fooBAR.");
+ assert_se(streq(hostname_cleanup(s), "fooBAR"));
+ s = strdupa_safe("fooBAR.com.");
+ assert_se(streq(hostname_cleanup(s), "fooBAR.com"));
+ s = strdupa_safe("fööbar");
+ assert_se(streq(hostname_cleanup(s), "fbar"));
+ s = strdupa_safe("");
+ assert_se(isempty(hostname_cleanup(s)));
+ s = strdupa_safe(".");
+ assert_se(isempty(hostname_cleanup(s)));
+ s = strdupa_safe("..");
+ assert_se(isempty(hostname_cleanup(s)));
+ s = strdupa_safe("foobar.");
+ assert_se(streq(hostname_cleanup(s), "foobar"));
+ s = strdupa_safe(".foobar");
+ assert_se(streq(hostname_cleanup(s), "foobar"));
+ s = strdupa_safe("foo..bar");
+ assert_se(streq(hostname_cleanup(s), "foo.bar"));
+ s = strdupa_safe("foo.bar..");
+ assert_se(streq(hostname_cleanup(s), "foo.bar"));
+ s = strdupa_safe("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
+ assert_se(streq(hostname_cleanup(s), "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
+ s = strdupa_safe("xxxx........xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
+ assert_se(streq(hostname_cleanup(s), "xxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"));
+}
+
+TEST(hostname_malloc) {
+ _cleanup_free_ char *h = NULL, *l = NULL;
+
+ assert_se(h = gethostname_malloc());
+ log_info("hostname_malloc: \"%s\"", h);
+
+ assert_se(l = gethostname_short_malloc());
+ log_info("hostname_short_malloc: \"%s\"", l);
+}
+
+TEST(default_hostname) {
+ if (!hostname_is_valid(FALLBACK_HOSTNAME, 0)) {
+ log_error("Configured fallback hostname \"%s\" is not valid.", FALLBACK_HOSTNAME);
+ exit(EXIT_FAILURE);
+ }
+
+ _cleanup_free_ char *n = get_default_hostname();
+ assert_se(n);
+ log_info("get_default_hostname: \"%s\"", n);
+ assert_se(hostname_is_valid(n, 0));
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-id128.c b/src/test/test-id128.c
new file mode 100644
index 0000000..ae7df27
--- /dev/null
+++ b/src/test/test-id128.c
@@ -0,0 +1,339 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "sd-daemon.h"
+#include "sd-id128.h"
+
+#include "alloc-util.h"
+#include "fd-util.h"
+#include "id128-util.h"
+#include "macro.h"
+#include "path-util.h"
+#include "rm-rf.h"
+#include "string-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+#define ID128_WALDI SD_ID128_MAKE(01, 02, 03, 04, 05, 06, 07, 08, 09, 0a, 0b, 0c, 0d, 0e, 0f, 10)
+#define STR_WALDI "0102030405060708090a0b0c0d0e0f10"
+#define UUID_WALDI "01020304-0506-0708-090a-0b0c0d0e0f10"
+#define STR_NULL "00000000000000000000000000000000"
+
+TEST(id128) {
+ sd_id128_t id, id2;
+ char t[SD_ID128_STRING_MAX], q[SD_ID128_UUID_STRING_MAX];
+ _cleanup_free_ char *b = NULL;
+ _cleanup_close_ int fd = -EBADF;
+
+ assert_se(sd_id128_randomize(&id) == 0);
+ printf("random: %s\n", sd_id128_to_string(id, t));
+
+ assert_se(sd_id128_from_string(t, &id2) == 0);
+ assert_se(sd_id128_equal(id, id2));
+ assert_se(sd_id128_in_set(id, id));
+ assert_se(sd_id128_in_set(id, id2));
+ assert_se(sd_id128_in_set(id, id2, id));
+ assert_se(sd_id128_in_set(id, ID128_WALDI, id));
+ assert_se(!sd_id128_in_set(id));
+ assert_se(!sd_id128_in_set(id, ID128_WALDI));
+ assert_se(!sd_id128_in_set(id, ID128_WALDI, ID128_WALDI));
+
+ if (sd_booted() > 0 && sd_id128_get_machine(NULL) >= 0) {
+ assert_se(sd_id128_get_machine(&id) == 0);
+ printf("machine: %s\n", sd_id128_to_string(id, t));
+
+ assert_se(sd_id128_get_boot(&id) == 0);
+ printf("boot: %s\n", sd_id128_to_string(id, t));
+ }
+
+ printf("waldi: %s\n", sd_id128_to_string(ID128_WALDI, t));
+ assert_se(streq(t, STR_WALDI));
+
+ assert_se(asprintf(&b, SD_ID128_FORMAT_STR, SD_ID128_FORMAT_VAL(ID128_WALDI)) == 32);
+ printf("waldi2: %s\n", b);
+ assert_se(streq(t, b));
+
+ printf("waldi3: %s\n", sd_id128_to_uuid_string(ID128_WALDI, q));
+ assert_se(streq(q, UUID_WALDI));
+
+ b = mfree(b);
+ assert_se(asprintf(&b, SD_ID128_UUID_FORMAT_STR, SD_ID128_FORMAT_VAL(ID128_WALDI)) == 36);
+ printf("waldi4: %s\n", b);
+ assert_se(streq(q, b));
+
+ assert_se(sd_id128_from_string(STR_WALDI, &id) >= 0);
+ assert_se(sd_id128_equal(id, ID128_WALDI));
+
+ assert_se(sd_id128_from_string(UUID_WALDI, &id) >= 0);
+ assert_se(sd_id128_equal(id, ID128_WALDI));
+
+ assert_se(sd_id128_from_string("", &id) < 0);
+ assert_se(sd_id128_from_string("01020304-0506-0708-090a-0b0c0d0e0f101", &id) < 0);
+ assert_se(sd_id128_from_string("01020304-0506-0708-090a-0b0c0d0e0f10-", &id) < 0);
+ assert_se(sd_id128_from_string("01020304-0506-0708-090a0b0c0d0e0f10", &id) < 0);
+ assert_se(sd_id128_from_string("010203040506-0708-090a-0b0c0d0e0f10", &id) < 0);
+
+ assert_se(id128_from_string_nonzero(STR_WALDI, &id) == 0);
+ assert_se(id128_from_string_nonzero(STR_NULL, &id) == -ENXIO);
+ assert_se(id128_from_string_nonzero("01020304-0506-0708-090a-0b0c0d0e0f101", &id) < 0);
+ assert_se(id128_from_string_nonzero("01020304-0506-0708-090a-0b0c0d0e0f10-", &id) < 0);
+ assert_se(id128_from_string_nonzero("01020304-0506-0708-090a0b0c0d0e0f10", &id) < 0);
+ assert_se(id128_from_string_nonzero("010203040506-0708-090a-0b0c0d0e0f10", &id) < 0);
+
+ assert_se(id128_is_valid(STR_WALDI));
+ assert_se(id128_is_valid(UUID_WALDI));
+ assert_se(!id128_is_valid(""));
+ assert_se(!id128_is_valid("01020304-0506-0708-090a-0b0c0d0e0f101"));
+ assert_se(!id128_is_valid("01020304-0506-0708-090a-0b0c0d0e0f10-"));
+ assert_se(!id128_is_valid("01020304-0506-0708-090a0b0c0d0e0f10"));
+ assert_se(!id128_is_valid("010203040506-0708-090a-0b0c0d0e0f10"));
+
+ fd = open_tmpfile_unlinkable(NULL, O_RDWR|O_CLOEXEC);
+ assert_se(fd >= 0);
+
+ /* First, write as UUID */
+ assert_se(sd_id128_randomize(&id) >= 0);
+ assert_se(id128_write_fd(fd, ID128_FORMAT_UUID, id) >= 0);
+
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(id128_read_fd(fd, ID128_FORMAT_PLAIN, &id2) == -EUCLEAN);
+
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(id128_read_fd(fd, ID128_FORMAT_UUID, &id2) >= 0);
+ assert_se(sd_id128_equal(id, id2));
+
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(id128_read_fd(fd, ID128_FORMAT_ANY, &id2) >= 0);
+ assert_se(sd_id128_equal(id, id2));
+
+ /* Second, write as plain */
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(ftruncate(fd, 0) >= 0);
+
+ assert_se(sd_id128_randomize(&id) >= 0);
+ assert_se(id128_write_fd(fd, ID128_FORMAT_PLAIN, id) >= 0);
+
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(id128_read_fd(fd, ID128_FORMAT_UUID, &id2) == -EUCLEAN);
+
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(id128_read_fd(fd, ID128_FORMAT_PLAIN, &id2) >= 0);
+ assert_se(sd_id128_equal(id, id2));
+
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(id128_read_fd(fd, ID128_FORMAT_ANY, &id2) >= 0);
+ assert_se(sd_id128_equal(id, id2));
+
+ /* Third, write plain without trailing newline */
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(ftruncate(fd, 0) >= 0);
+
+ assert_se(sd_id128_randomize(&id) >= 0);
+ assert_se(write(fd, sd_id128_to_string(id, t), 32) == 32);
+
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(id128_read_fd(fd, ID128_FORMAT_UUID, &id2) == -EUCLEAN);
+
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(id128_read_fd(fd, ID128_FORMAT_PLAIN, &id2) >= 0);
+ assert_se(sd_id128_equal(id, id2));
+
+ /* Fourth, write UUID without trailing newline */
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(ftruncate(fd, 0) >= 0);
+
+ assert_se(sd_id128_randomize(&id) >= 0);
+ assert_se(write(fd, sd_id128_to_uuid_string(id, q), 36) == 36);
+
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(id128_read_fd(fd, ID128_FORMAT_PLAIN, &id2) == -EUCLEAN);
+
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(id128_read_fd(fd, ID128_FORMAT_UUID, &id2) >= 0);
+ assert_se(sd_id128_equal(id, id2));
+
+ /* Fifth, tests for "uninitialized" */
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(ftruncate(fd, 0) >= 0);
+ assert_se(write(fd, "uninitialized", STRLEN("uninitialized")) == STRLEN("uninitialized"));
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(id128_read_fd(fd, ID128_FORMAT_ANY, NULL) == -ENOPKG);
+
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(ftruncate(fd, 0) >= 0);
+ assert_se(write(fd, "uninitialized\n", STRLEN("uninitialized\n")) == STRLEN("uninitialized\n"));
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(id128_read_fd(fd, ID128_FORMAT_ANY, NULL) == -ENOPKG);
+
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(ftruncate(fd, 0) >= 0);
+ assert_se(write(fd, "uninitialized\nfoo", STRLEN("uninitialized\nfoo")) == STRLEN("uninitialized\nfoo"));
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(id128_read_fd(fd, ID128_FORMAT_ANY, NULL) == -EUCLEAN);
+
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(ftruncate(fd, 0) >= 0);
+ assert_se(write(fd, "uninit", STRLEN("uninit")) == STRLEN("uninit"));
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(id128_read_fd(fd, ID128_FORMAT_ANY, NULL) == -EUCLEAN);
+
+ /* build/systemd-id128 -a f03daaeb1c334b43a732172944bf772e show 51df0b4bc3b04c9780e299b98ca373b8 */
+ assert_se(sd_id128_get_app_specific(SD_ID128_MAKE(51,df,0b,4b,c3,b0,4c,97,80,e2,99,b9,8c,a3,73,b8),
+ SD_ID128_MAKE(f0,3d,aa,eb,1c,33,4b,43,a7,32,17,29,44,bf,77,2e), &id) >= 0);
+ assert_se(sd_id128_equal(id, SD_ID128_MAKE(1d,ee,59,54,e7,5c,4d,6f,b9,6c,c6,c0,4c,a1,8a,86)));
+
+ if (sd_booted() > 0 && sd_id128_get_machine(NULL) >= 0) {
+ assert_se(sd_id128_get_machine_app_specific(SD_ID128_MAKE(f0,3d,aa,eb,1c,33,4b,43,a7,32,17,29,44,bf,77,2e), &id) >= 0);
+ assert_se(sd_id128_get_machine_app_specific(SD_ID128_MAKE(f0,3d,aa,eb,1c,33,4b,43,a7,32,17,29,44,bf,77,2e), &id2) >= 0);
+ assert_se(sd_id128_equal(id, id2));
+ assert_se(sd_id128_get_machine_app_specific(SD_ID128_MAKE(51,df,0b,4b,c3,b0,4c,97,80,e2,99,b9,8c,a3,73,b8), &id2) >= 0);
+ assert_se(!sd_id128_equal(id, id2));
+ }
+
+ /* Check return values */
+ assert_se(sd_id128_get_app_specific(SD_ID128_ALLF, SD_ID128_NULL, &id) == -ENXIO);
+ assert_se(sd_id128_get_app_specific(SD_ID128_NULL, SD_ID128_ALLF, &id) == 0);
+}
+
+TEST(sd_id128_get_invocation) {
+ sd_id128_t id;
+ int r;
+
+ /* Query the invocation ID */
+ r = sd_id128_get_invocation(&id);
+ if (r < 0)
+ log_warning_errno(r, "Failed to get invocation ID, ignoring: %m");
+ else
+ log_info("Invocation ID: " SD_ID128_FORMAT_STR, SD_ID128_FORMAT_VAL(id));
+}
+
+TEST(benchmark_sd_id128_get_machine_app_specific) {
+ unsigned iterations = slow_tests_enabled() ? 1000000 : 1000;
+ usec_t t, q;
+
+ if (sd_id128_get_machine(NULL) < 0)
+ return (void) log_tests_skipped("/etc/machine-id is not initialized");
+
+ log_info("/* %s (%u iterations) */", __func__, iterations);
+
+ sd_id128_t id = ID128_WALDI, id2;
+
+ t = now(CLOCK_MONOTONIC);
+
+ for (unsigned i = 0; i < iterations; i++) {
+ id.qwords[1] = i;
+
+ assert_se(sd_id128_get_machine_app_specific(id, &id2) >= 0);
+ }
+
+ q = now(CLOCK_MONOTONIC) - t;
+
+ log_info("%lf μs each", (double) q / iterations);
+}
+
+TEST(id128_at) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ _cleanup_close_ int tfd = -EBADF;
+ _cleanup_free_ char *p = NULL;
+ sd_id128_t id, i;
+
+ tfd = mkdtemp_open(NULL, O_PATH, &t);
+ assert_se(tfd >= 0);
+ assert_se(mkdirat(tfd, "etc", 0755) >= 0);
+ assert_se(symlinkat("etc", tfd, "etc2") >= 0);
+ assert_se(symlinkat("machine-id", tfd, "etc/hoge-id") >= 0);
+
+ assert_se(sd_id128_randomize(&id) == 0);
+
+ assert_se(id128_write_at(tfd, "etc/machine-id", ID128_FORMAT_PLAIN, id) >= 0);
+ if (geteuid() == 0)
+ assert_se(id128_write_at(tfd, "etc/machine-id", ID128_FORMAT_PLAIN, id) >= 0);
+ else
+ assert_se(id128_write_at(tfd, "etc/machine-id", ID128_FORMAT_PLAIN, id) == -EACCES);
+ assert_se(unlinkat(tfd, "etc/machine-id", 0) >= 0);
+ assert_se(id128_write_at(tfd, "etc2/machine-id", ID128_FORMAT_PLAIN, id) >= 0);
+ assert_se(unlinkat(tfd, "etc/machine-id", 0) >= 0);
+ assert_se(id128_write_at(tfd, "etc/hoge-id", ID128_FORMAT_PLAIN, id) >= 0);
+ assert_se(unlinkat(tfd, "etc/machine-id", 0) >= 0);
+ assert_se(id128_write_at(tfd, "etc2/hoge-id", ID128_FORMAT_PLAIN, id) >= 0);
+
+ /* id128_read_at() */
+ i = SD_ID128_NULL; /* Not necessary in real code, but for testing that the id is really assigned. */
+ assert_se(id128_read_at(tfd, "etc/machine-id", ID128_FORMAT_PLAIN, &i) >= 0);
+ assert_se(sd_id128_equal(id, i));
+
+ i = SD_ID128_NULL;
+ assert_se(id128_read_at(tfd, "etc2/machine-id", ID128_FORMAT_PLAIN, &i) >= 0);
+ assert_se(sd_id128_equal(id, i));
+
+ i = SD_ID128_NULL;
+ assert_se(id128_read_at(tfd, "etc/hoge-id", ID128_FORMAT_PLAIN, &i) >= 0);
+ assert_se(sd_id128_equal(id, i));
+
+ i = SD_ID128_NULL;
+ assert_se(id128_read_at(tfd, "etc2/hoge-id", ID128_FORMAT_PLAIN, &i) >= 0);
+ assert_se(sd_id128_equal(id, i));
+
+ /* id128_read() */
+ assert_se(p = path_join(t, "/etc/machine-id"));
+
+ i = SD_ID128_NULL;
+ assert_se(id128_read(p, ID128_FORMAT_PLAIN, &i) >= 0);
+ assert_se(sd_id128_equal(id, i));
+
+ free(p);
+ assert_se(p = path_join(t, "/etc2/machine-id"));
+
+ i = SD_ID128_NULL;
+ assert_se(id128_read(p, ID128_FORMAT_PLAIN, &i) >= 0);
+ assert_se(sd_id128_equal(id, i));
+
+ free(p);
+ assert_se(p = path_join(t, "/etc/hoge-id"));
+
+ i = SD_ID128_NULL;
+ assert_se(id128_read(p, ID128_FORMAT_PLAIN, &i) >= 0);
+ assert_se(sd_id128_equal(id, i));
+
+ free(p);
+ assert_se(p = path_join(t, "/etc2/hoge-id"));
+
+ i = SD_ID128_NULL;
+ assert_se(id128_read(p, ID128_FORMAT_PLAIN, &i) >= 0);
+ assert_se(sd_id128_equal(id, i));
+
+ /* id128_get_machine_at() */
+ i = SD_ID128_NULL;
+ assert_se(id128_get_machine_at(tfd, &i) >= 0);
+ assert_se(sd_id128_equal(id, i));
+
+ /* id128_get_machine() */
+ i = SD_ID128_NULL;
+ assert_se(id128_get_machine(t, &i) >= 0);
+ assert_se(sd_id128_equal(id, i));
+}
+
+TEST(ID128_REFUSE_NULL) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ _cleanup_close_ int tfd = -EBADF;
+ sd_id128_t id;
+
+ tfd = mkdtemp_open(NULL, O_PATH, &t);
+ assert_se(tfd >= 0);
+
+ assert_se(id128_write_at(tfd, "zero-id", ID128_FORMAT_PLAIN | ID128_REFUSE_NULL, (sd_id128_t) {}) == -ENOMEDIUM);
+ assert_se(unlinkat(tfd, "zero-id", 0) >= 0);
+ assert_se(id128_write_at(tfd, "zero-id", ID128_FORMAT_PLAIN, (sd_id128_t) {}) >= 0);
+
+ assert_se(sd_id128_randomize(&id) == 0);
+ assert_se(!sd_id128_equal(id, SD_ID128_NULL));
+ assert_se(id128_read_at(tfd, "zero-id", ID128_FORMAT_PLAIN, &id) >= 0);
+ assert_se(sd_id128_equal(id, SD_ID128_NULL));
+
+ assert_se(id128_read_at(tfd, "zero-id", ID128_FORMAT_PLAIN | ID128_REFUSE_NULL, &id) == -ENOMEDIUM);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-image-policy.c b/src/test/test-image-policy.c
new file mode 100644
index 0000000..d9fe556
--- /dev/null
+++ b/src/test/test-image-policy.c
@@ -0,0 +1,132 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "image-policy.h"
+#include "pretty-print.h"
+#include "string-util.h"
+#include "tests.h"
+#include "pager.h"
+
+static void test_policy(const ImagePolicy *p, const char *name) {
+ _cleanup_free_ char *as_string = NULL, *as_string_simplified = NULL;
+ _cleanup_free_ ImagePolicy *parsed = NULL;
+
+ assert_se(image_policy_to_string(p, /* simplified= */ false, &as_string) >= 0);
+ assert_se(image_policy_to_string(p, /* simplified= */ true, &as_string_simplified) >= 0);
+
+ printf("%s%s", ansi_underline(), name);
+
+ if (!streq(as_string_simplified, name)) {
+ printf(" → %s", as_string_simplified);
+
+ if (!streq(as_string, as_string_simplified))
+ printf(" (aka %s)", as_string);
+ }
+
+ printf("%s\n", ansi_normal());
+
+ assert_se(image_policy_from_string(as_string, &parsed) >= 0);
+ assert_se(image_policy_equal(p, parsed));
+ parsed = image_policy_free(parsed);
+
+ assert_se(image_policy_from_string(as_string_simplified, &parsed) >= 0);
+ assert_se(image_policy_equivalent(p, parsed));
+ parsed = image_policy_free(parsed);
+
+ for (PartitionDesignator d = 0; d < _PARTITION_DESIGNATOR_MAX; d++) {
+ _cleanup_free_ char *k = NULL;
+ PartitionPolicyFlags f;
+
+ f = image_policy_get(p, d);
+ if (f < 0) {
+ f = image_policy_get_exhaustively(p, d);
+ assert_se(f >= 0);
+ assert_se(partition_policy_flags_to_string(f, /* simplified= */ true, &k) >= 0);
+
+ printf("%s\t%s → n/a (exhaustively: %s)%s\n", ansi_grey(), partition_designator_to_string(d), k, ansi_normal());
+ } else {
+ assert_se(partition_policy_flags_to_string(f, /* simplified= */ true, &k) >= 0);
+ printf("\t%s → %s\n", partition_designator_to_string(d), k);
+ }
+ }
+
+ _cleanup_free_ char *w = NULL;
+ assert_se(partition_policy_flags_to_string(image_policy_default(p), /* simplified= */ true, &w) >= 0);
+ printf("\tdefault → %s\n", w);
+}
+
+static void test_policy_string(const char *t) {
+ _cleanup_free_ ImagePolicy *parsed = NULL;
+
+ assert_se(image_policy_from_string(t, &parsed) >= 0);
+ test_policy(parsed, t);
+}
+
+static void test_policy_equiv(const char *s, bool (*func)(const ImagePolicy *p)) {
+ _cleanup_(image_policy_freep) ImagePolicy *p = NULL;
+
+ assert_se(image_policy_from_string(s, &p) >= 0);
+
+ assert_se(func(p));
+ assert_se(func == image_policy_equiv_ignore || !image_policy_equiv_ignore(p));
+ assert_se(func == image_policy_equiv_allow || !image_policy_equiv_allow(p));
+ assert_se(func == image_policy_equiv_deny || !image_policy_equiv_deny(p));
+}
+
+TEST_RET(test_image_policy_to_string) {
+ test_policy(&image_policy_allow, "*");
+ test_policy(&image_policy_ignore, "-");
+ test_policy(&image_policy_deny, "~");
+ test_policy(&image_policy_sysext, "sysext");
+ test_policy(&image_policy_sysext_strict, "sysext-strict");
+ test_policy(&image_policy_confext, "confext");
+ test_policy(&image_policy_container, "container");
+ test_policy(&image_policy_host, "host");
+ test_policy(&image_policy_service, "service");
+ test_policy(NULL, "null");
+
+ test_policy_string("");
+ test_policy_string("-");
+ test_policy_string("*");
+ test_policy_string("~");
+ test_policy_string("swap=open");
+ test_policy_string("swap=open:root=signed");
+ test_policy_string("swap=open:root=signed+read-only-on+growfs-off:=absent");
+ test_policy_string("=-");
+ test_policy_string("=");
+
+ test_policy_equiv("", image_policy_equiv_ignore);
+ test_policy_equiv("-", image_policy_equiv_ignore);
+ test_policy_equiv("*", image_policy_equiv_allow);
+ test_policy_equiv("~", image_policy_equiv_deny);
+ test_policy_equiv("=absent", image_policy_equiv_deny);
+ test_policy_equiv("=open", image_policy_equiv_allow);
+ test_policy_equiv("=verity+signed+encrypted+unprotected+unused+absent", image_policy_equiv_allow);
+ test_policy_equiv("=signed+verity+encrypted+unused+unprotected+absent", image_policy_equiv_allow);
+ test_policy_equiv("=ignore", image_policy_equiv_ignore);
+ test_policy_equiv("=absent+unused", image_policy_equiv_ignore);
+ test_policy_equiv("=unused+absent", image_policy_equiv_ignore);
+ test_policy_equiv("root=ignore:=ignore", image_policy_equiv_ignore);
+
+ assert_se(image_policy_from_string("pfft", NULL) == -EINVAL);
+ assert_se(image_policy_from_string("öäüß", NULL) == -EINVAL);
+ assert_se(image_policy_from_string(":", NULL) == -EINVAL);
+ assert_se(image_policy_from_string("a=", NULL) == -EBADSLT);
+ assert_se(image_policy_from_string("=a", NULL) == -EBADRQC);
+ assert_se(image_policy_from_string("==", NULL) == -EBADRQC);
+ assert_se(image_policy_from_string("root=verity:root=encrypted", NULL) == -ENOTUNIQ);
+ assert_se(image_policy_from_string("root=grbl", NULL) == -EBADRQC);
+ assert_se(image_policy_from_string("wowza=grbl", NULL) == -EBADSLT);
+
+ return 0;
+}
+
+TEST(extend) {
+ assert_se(partition_policy_flags_extend(0) == _PARTITION_POLICY_MASK);
+ assert_se(partition_policy_flags_extend(_PARTITION_POLICY_MASK) == _PARTITION_POLICY_MASK);
+ assert_se(partition_policy_flags_extend(PARTITION_POLICY_UNPROTECTED) == (PARTITION_POLICY_UNPROTECTED|_PARTITION_POLICY_PFLAGS_MASK));
+ assert_se(partition_policy_flags_extend(PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_READ_ONLY_ON) == (PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_READ_ONLY_ON|_PARTITION_POLICY_GROWFS_MASK));
+ assert_se(partition_policy_flags_extend(PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_READ_ONLY_ON|PARTITION_POLICY_GROWFS_OFF) == (PARTITION_POLICY_UNPROTECTED|PARTITION_POLICY_READ_ONLY_ON|PARTITION_POLICY_GROWFS_OFF));
+ assert_se(partition_policy_flags_extend(PARTITION_POLICY_GROWFS_ON) == (PARTITION_POLICY_GROWFS_ON|_PARTITION_POLICY_USE_MASK|_PARTITION_POLICY_READ_ONLY_MASK));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-import-util.c b/src/test/test-import-util.c
new file mode 100644
index 0000000..7930fe5
--- /dev/null
+++ b/src/test/test-import-util.c
@@ -0,0 +1,64 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "import-util.h"
+#include "log.h"
+#include "string-util.h"
+#include "tests.h"
+
+static void test_import_url_last_component_one(const char *input, const char *output, int ret) {
+ _cleanup_free_ char *s = NULL;
+
+ assert_se(import_url_last_component(input, &s) == ret);
+ assert_se(streq_ptr(output, s));
+}
+
+TEST(import_url_last_component) {
+ test_import_url_last_component_one("https://foobar/waldo/quux", "quux", 0);
+ test_import_url_last_component_one("https://foobar/waldo/quux/", "quux", 0);
+ test_import_url_last_component_one("https://foobar/waldo/", "waldo", 0);
+ test_import_url_last_component_one("https://foobar/", NULL, -EADDRNOTAVAIL);
+ test_import_url_last_component_one("https://foobar", NULL, -EADDRNOTAVAIL);
+ test_import_url_last_component_one("https://foobar/waldo/quux?foo=bar", "quux", 0);
+ test_import_url_last_component_one("https://foobar/waldo/quux/?foo=bar", "quux", 0);
+ test_import_url_last_component_one("https://foobar/waldo/quux/?foo=bar#piep", "quux", 0);
+ test_import_url_last_component_one("https://foobar/waldo/quux/#piep", "quux", 0);
+ test_import_url_last_component_one("https://foobar/waldo/quux#piep", "quux", 0);
+ test_import_url_last_component_one("https://", NULL, -EINVAL);
+ test_import_url_last_component_one("", NULL, -EINVAL);
+ test_import_url_last_component_one(":", NULL, -EINVAL);
+ test_import_url_last_component_one(":/", NULL, -EINVAL);
+ test_import_url_last_component_one("x:/", NULL, -EINVAL);
+ test_import_url_last_component_one("x:y", NULL, -EADDRNOTAVAIL);
+ test_import_url_last_component_one("x:y/z", "z", 0);
+}
+
+static void test_import_url_change_suffix_one(const char *input, size_t n, const char *suffix, const char *output, int ret) {
+ _cleanup_free_ char *s = NULL;
+
+ assert_se(import_url_change_suffix(input, n, suffix, &s) == ret);
+ assert_se(streq_ptr(output, s));
+}
+
+TEST(import_url_change_suffix) {
+ test_import_url_change_suffix_one("https://foobar/waldo/quux", 1, "wuff", "https://foobar/waldo/wuff", 0);
+ test_import_url_change_suffix_one("https://foobar/waldo/quux/", 1, "wuff", "https://foobar/waldo/wuff", 0);
+ test_import_url_change_suffix_one("https://foobar/waldo/quux///?mief", 1, "wuff", "https://foobar/waldo/wuff", 0);
+ test_import_url_change_suffix_one("https://foobar/waldo/quux///?mief#opopo", 1, "wuff", "https://foobar/waldo/wuff", 0);
+ test_import_url_change_suffix_one("https://foobar/waldo/quux/quff", 2, "wuff", "https://foobar/waldo/wuff", 0);
+ test_import_url_change_suffix_one("https://foobar/waldo/quux/quff/", 2, "wuff", "https://foobar/waldo/wuff", 0);
+ test_import_url_change_suffix_one("https://foobar/waldo/quux/quff", 0, "wuff", "https://foobar/waldo/quux/quff/wuff", 0);
+ test_import_url_change_suffix_one("https://foobar/waldo/quux/quff?aa?bb##4", 0, "wuff", "https://foobar/waldo/quux/quff/wuff", 0);
+ test_import_url_change_suffix_one("https://", 0, "wuff", NULL, -EINVAL);
+ test_import_url_change_suffix_one("", 0, "wuff", NULL, -EINVAL);
+ test_import_url_change_suffix_one(":", 0, "wuff", NULL, -EINVAL);
+ test_import_url_change_suffix_one(":/", 0, "wuff", NULL, -EINVAL);
+ test_import_url_change_suffix_one("x:/", 0, "wuff", NULL, -EINVAL);
+ test_import_url_change_suffix_one("x:y", 0, "wuff", "x:y/wuff", 0);
+ test_import_url_change_suffix_one("x:y/z", 0, "wuff", "x:y/z/wuff", 0);
+ test_import_url_change_suffix_one("x:y/z/", 0, "wuff", "x:y/z/wuff", 0);
+ test_import_url_change_suffix_one("x:y/z/", 1, "wuff", "x:y/wuff", 0);
+ test_import_url_change_suffix_one("x:y/z/", 2, "wuff", "x:y/wuff", 0);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-in-addr-prefix-util.c b/src/test/test-in-addr-prefix-util.c
new file mode 100644
index 0000000..661ca8f
--- /dev/null
+++ b/src/test/test-in-addr-prefix-util.c
@@ -0,0 +1,121 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "in-addr-prefix-util.h"
+#include "tests.h"
+
+static void test_in_addr_prefix_to_string_one(int f, const char *addr, unsigned prefixlen) {
+ union in_addr_union ua;
+ assert_se(in_addr_from_string(f, addr, &ua) >= 0);
+
+ const char *r = IN_ADDR_PREFIX_TO_STRING(f, &ua, prefixlen);
+ assert_se(r);
+ printf("%s: %s/%u == %s\n", __func__, addr, prefixlen, r);
+ assert_se(startswith(r, addr));
+
+ assert_se(streq(r, IN_ADDR_PREFIX_TO_STRING(f, &ua, prefixlen)));
+ assert_se(streq(IN_ADDR_PREFIX_TO_STRING(f, &ua, prefixlen), r));
+}
+
+TEST(in_addr_to_string_prefix) {
+ test_in_addr_prefix_to_string_one(AF_INET, "192.168.0.1", 0);
+ test_in_addr_prefix_to_string_one(AF_INET, "192.168.0.1", 1);
+ test_in_addr_prefix_to_string_one(AF_INET, "192.168.0.1", 31);
+ test_in_addr_prefix_to_string_one(AF_INET, "192.168.0.1", 32);
+ test_in_addr_prefix_to_string_one(AF_INET, "192.168.0.1", 256);
+ test_in_addr_prefix_to_string_one(AF_INET, "10.11.12.13", UINT_MAX);
+ test_in_addr_prefix_to_string_one(AF_INET6, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 0);
+ test_in_addr_prefix_to_string_one(AF_INET6, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", UINT_MAX);
+ test_in_addr_prefix_to_string_one(AF_INET6, "::1", 11);
+ test_in_addr_prefix_to_string_one(AF_INET6, "fe80::", 33);
+}
+
+static void test_config_parse_in_addr_prefixes_one(int family, const union in_addr_union *addr, uint8_t prefixlen, Set **prefixes) {
+ const char *str = IN_ADDR_PREFIX_TO_STRING(family, addr, prefixlen);
+ assert_se(str);
+
+ assert_se(config_parse_in_addr_prefixes("unit", "filename", 1, "Service", 1, "IPAddressAllow", 0, str, prefixes, NULL) >= 0);
+
+ assert_se(streq(str, IN_ADDR_PREFIX_TO_STRING(family, addr, prefixlen)));
+ assert_se(streq(IN_ADDR_PREFIX_TO_STRING(family, addr, prefixlen), str));
+}
+
+static void test_config_parse_in_addr_prefixes(Set **ret) {
+ _cleanup_set_free_ Set *prefixes = NULL;
+
+ log_info("/* %s() */", __func__);
+
+ for (uint32_t i = 0; i < 256; i++) {
+ /* ipv4 link-local address */
+ test_config_parse_in_addr_prefixes_one(AF_INET, &(union in_addr_union) {
+ .in.s_addr = htobe32((UINT32_C(169) << 24) |
+ (UINT32_C(254) << 16) |
+ (i << 8)),
+ }, 24, &prefixes);
+
+ /* ipv6 multicast address */
+ test_config_parse_in_addr_prefixes_one(AF_INET6, &(union in_addr_union) {
+ .in6.s6_addr[0] = 0xff,
+ .in6.s6_addr[1] = i,
+ }, 16, &prefixes);
+
+ for (uint32_t j = 0; j < 256; j++) {
+ test_config_parse_in_addr_prefixes_one(AF_INET, &(union in_addr_union) {
+ .in.s_addr = htobe32((UINT32_C(169) << 24) |
+ (UINT32_C(254) << 16) |
+ (i << 8) | j),
+ }, 32, &prefixes);
+
+ test_config_parse_in_addr_prefixes_one(AF_INET6, &(union in_addr_union) {
+ .in6.s6_addr[0] = 0xff,
+ .in6.s6_addr[1] = i,
+ .in6.s6_addr[2] = j,
+ }, 24, &prefixes);
+ }
+ }
+
+ *ret = TAKE_PTR(prefixes);
+}
+
+static void test_in_addr_prefixes_reduce(Set *prefixes) {
+ log_info("/* %s() */", __func__);
+
+ assert_se(set_size(prefixes) == 2 * 256 * 257);
+ assert_se(!in_addr_prefixes_is_any(prefixes));
+
+ assert_se(in_addr_prefixes_reduce(prefixes) >= 0);
+ assert_se(set_size(prefixes) == 2 * 256);
+ assert_se(!in_addr_prefixes_is_any(prefixes));
+
+ assert_se(config_parse_in_addr_prefixes("unit", "filename", 1, "Service", 1, "IPAddressAllow", 0, "link-local", &prefixes, NULL) == 0);
+ assert_se(set_size(prefixes) == 2 * 256 + 2);
+ assert_se(!in_addr_prefixes_is_any(prefixes));
+
+ assert_se(in_addr_prefixes_reduce(prefixes) >= 0);
+ assert_se(set_size(prefixes) == 256 + 2);
+ assert_se(!in_addr_prefixes_is_any(prefixes));
+
+ assert_se(config_parse_in_addr_prefixes("unit", "filename", 1, "Service", 1, "IPAddressAllow", 0, "multicast", &prefixes, NULL) == 0);
+ assert_se(set_size(prefixes) == 256 + 4);
+ assert_se(!in_addr_prefixes_is_any(prefixes));
+
+ assert_se(in_addr_prefixes_reduce(prefixes) >= 0);
+ assert_se(set_size(prefixes) == 4);
+ assert_se(!in_addr_prefixes_is_any(prefixes));
+
+ assert_se(config_parse_in_addr_prefixes("unit", "filename", 1, "Service", 1, "IPAddressAllow", 0, "any", &prefixes, NULL) == 0);
+ assert_se(set_size(prefixes) == 6);
+ assert_se(in_addr_prefixes_is_any(prefixes));
+
+ assert_se(in_addr_prefixes_reduce(prefixes) >= 0);
+ assert_se(set_size(prefixes) == 2);
+ assert_se(in_addr_prefixes_is_any(prefixes));
+}
+
+TEST(in_addr_prefixes) {
+ _cleanup_set_free_ Set *prefixes = NULL;
+
+ test_config_parse_in_addr_prefixes(&prefixes);
+ test_in_addr_prefixes_reduce(prefixes);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-in-addr-util.c b/src/test/test-in-addr-util.c
new file mode 100644
index 0000000..93ab1c5
--- /dev/null
+++ b/src/test/test-in-addr-util.c
@@ -0,0 +1,408 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fnmatch.h>
+#include <netinet/in.h>
+
+#include "in-addr-util.h"
+#include "strv.h"
+#include "tests.h"
+
+static void test_in_addr_prefix_from_string_one(
+ const char *p,
+ int family,
+ int ret,
+ const union in_addr_union *u,
+ unsigned char prefixlen,
+ int ret_refuse,
+ unsigned char prefixlen_refuse) {
+
+ union in_addr_union q;
+ unsigned char l;
+ int f, r;
+
+ r = in_addr_prefix_from_string(p, family, &q, &l);
+ assert_se(r == ret);
+
+ if (r < 0)
+ return;
+
+ assert_se(in_addr_equal(family, &q, u));
+ assert_se(l == prefixlen);
+
+ r = in_addr_prefix_from_string_auto(p, &f, &q, &l);
+ assert_se(r >= 0);
+
+ assert_se(f == family);
+ assert_se(in_addr_equal(family, &q, u));
+ assert_se(l == prefixlen);
+
+ r = in_addr_prefix_from_string_auto_internal(p, PREFIXLEN_REFUSE, &f, &q, &l);
+ assert_se(r == ret_refuse);
+
+ if (r >= 0) {
+ assert_se(f == family);
+ assert_se(in_addr_equal(family, &q, u));
+ assert_se(l == prefixlen_refuse);
+ }
+}
+
+TEST(in_addr_prefix_from_string) {
+ test_in_addr_prefix_from_string_one("", AF_INET, -EINVAL, NULL, 0, -EINVAL, 0);
+ test_in_addr_prefix_from_string_one("/", AF_INET, -EINVAL, NULL, 0, -EINVAL, 0);
+ test_in_addr_prefix_from_string_one("/8", AF_INET, -EINVAL, NULL, 0, -EINVAL, 0);
+ test_in_addr_prefix_from_string_one("1.2.3.4", AF_INET, 0, &(union in_addr_union) { .in = (struct in_addr) { .s_addr = htobe32(0x01020304) } }, 32, -ENOANO, 0);
+ test_in_addr_prefix_from_string_one("1.2.3.4/0", AF_INET, 0, &(union in_addr_union) { .in = (struct in_addr) { .s_addr = htobe32(0x01020304) } }, 0, 0, 0);
+ test_in_addr_prefix_from_string_one("1.2.3.4/1", AF_INET, 0, &(union in_addr_union) { .in = (struct in_addr) { .s_addr = htobe32(0x01020304) } }, 1, 0, 1);
+ test_in_addr_prefix_from_string_one("1.2.3.4/2", AF_INET, 0, &(union in_addr_union) { .in = (struct in_addr) { .s_addr = htobe32(0x01020304) } }, 2, 0, 2);
+ test_in_addr_prefix_from_string_one("1.2.3.4/32", AF_INET, 0, &(union in_addr_union) { .in = (struct in_addr) { .s_addr = htobe32(0x01020304) } }, 32, 0, 32);
+ test_in_addr_prefix_from_string_one("1.2.3.4/33", AF_INET, -ERANGE, NULL, 0, -ERANGE, 0);
+ test_in_addr_prefix_from_string_one("1.2.3.4/-1", AF_INET, -ERANGE, NULL, 0, -ERANGE, 0);
+ test_in_addr_prefix_from_string_one("::1", AF_INET, -EINVAL, NULL, 0, -EINVAL, 0);
+
+ test_in_addr_prefix_from_string_one("", AF_INET6, -EINVAL, NULL, 0, -EINVAL, 0);
+ test_in_addr_prefix_from_string_one("/", AF_INET6, -EINVAL, NULL, 0, -EINVAL, 0);
+ test_in_addr_prefix_from_string_one("/8", AF_INET6, -EINVAL, NULL, 0, -EINVAL, 0);
+ test_in_addr_prefix_from_string_one("::1", AF_INET6, 0, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 128, -ENOANO, 0);
+ test_in_addr_prefix_from_string_one("::1/0", AF_INET6, 0, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 0, 0, 0);
+ test_in_addr_prefix_from_string_one("::1/1", AF_INET6, 0, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 1, 0, 1);
+ test_in_addr_prefix_from_string_one("::1/2", AF_INET6, 0, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 2, 0, 2);
+ test_in_addr_prefix_from_string_one("::1/32", AF_INET6, 0, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 32, 0, 32);
+ test_in_addr_prefix_from_string_one("::1/33", AF_INET6, 0, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 33, 0, 33);
+ test_in_addr_prefix_from_string_one("::1/64", AF_INET6, 0, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 64, 0, 64);
+ test_in_addr_prefix_from_string_one("::1/128", AF_INET6, 0, &(union in_addr_union) { .in6 = IN6ADDR_LOOPBACK_INIT }, 128, 0, 128);
+ test_in_addr_prefix_from_string_one("::1/129", AF_INET6, -ERANGE, NULL, 0, -ERANGE, 0);
+ test_in_addr_prefix_from_string_one("::1/-1", AF_INET6, -ERANGE, NULL, 0, -ERANGE, 0);
+}
+
+static void test_in_addr_prefix_to_string_valid(int family, const char *p) {
+ union in_addr_union u;
+ unsigned char l;
+
+ log_info("%s: %s", __func__, p);
+
+ assert_se(in_addr_prefix_from_string(p, family, &u, &l) >= 0);
+ assert_se(streq(p, IN_ADDR_PREFIX_TO_STRING(family, &u, l)));
+}
+
+static void test_in_addr_prefix_to_string_unoptimized(int family, const char *p) {
+ union in_addr_union u1, u2;
+ unsigned char len1, len2;
+
+ log_info("%s: %s", __func__, p);
+
+ assert_se(in_addr_prefix_from_string(p, family, &u1, &len1) >= 0);
+ const char *str1 = IN_ADDR_PREFIX_TO_STRING(family, &u1, len1);
+ assert_se(str1);
+ assert_se(in_addr_prefix_from_string(str1, family, &u2, &len2) >= 0);
+ const char *str2 = IN_ADDR_PREFIX_TO_STRING(family, &u2, len2);
+ assert_se(str2);
+
+ assert_se(streq(str1, str2));
+ assert_se(len1 == len2);
+ assert_se(in_addr_equal(family, &u1, &u2) > 0);
+}
+
+TEST(in_addr_prefix_to_string) {
+ test_in_addr_prefix_to_string_valid(AF_INET, "0.0.0.0/32");
+ test_in_addr_prefix_to_string_valid(AF_INET, "1.2.3.4/0");
+ test_in_addr_prefix_to_string_valid(AF_INET, "1.2.3.4/24");
+ test_in_addr_prefix_to_string_valid(AF_INET, "1.2.3.4/32");
+ test_in_addr_prefix_to_string_valid(AF_INET, "255.255.255.255/32");
+
+ test_in_addr_prefix_to_string_valid(AF_INET6, "::1/128");
+ test_in_addr_prefix_to_string_valid(AF_INET6, "fd00:abcd::1/64");
+ test_in_addr_prefix_to_string_valid(AF_INET6, "fd00:abcd::1234:1/64");
+ test_in_addr_prefix_to_string_valid(AF_INET6, "1111:2222:3333:4444:5555:6666:7777:8888/128");
+
+ test_in_addr_prefix_to_string_unoptimized(AF_INET, "0.0.0.0");
+ test_in_addr_prefix_to_string_unoptimized(AF_INET, "192.168.0.1");
+
+ test_in_addr_prefix_to_string_unoptimized(AF_INET6, "fd00:0000:0000:0000:0000:0000:0000:0001/64");
+ test_in_addr_prefix_to_string_unoptimized(AF_INET6, "fd00:1111::0000:2222:3333:4444:0001/64");
+}
+
+TEST(in_addr_random_prefix) {
+ _cleanup_free_ char *str = NULL;
+ union in_addr_union a;
+
+ assert_se(in_addr_from_string(AF_INET, "192.168.10.1", &a) >= 0);
+
+ assert_se(in_addr_random_prefix(AF_INET, &a, 31, 32) >= 0);
+ assert_se(in_addr_to_string(AF_INET, &a, &str) >= 0);
+ assert_se(STR_IN_SET(str, "192.168.10.0", "192.168.10.1"));
+ str = mfree(str);
+
+ assert_se(in_addr_random_prefix(AF_INET, &a, 24, 26) >= 0);
+ assert_se(in_addr_to_string(AF_INET, &a, &str) >= 0);
+ assert_se(startswith(str, "192.168.10."));
+ str = mfree(str);
+
+ assert_se(in_addr_random_prefix(AF_INET, &a, 16, 24) >= 0);
+ assert_se(in_addr_to_string(AF_INET, &a, &str) >= 0);
+ assert_se(fnmatch("192.168.[0-9]*.0", str, 0) == 0);
+ str = mfree(str);
+
+ assert_se(in_addr_random_prefix(AF_INET, &a, 8, 24) >= 0);
+ assert_se(in_addr_to_string(AF_INET, &a, &str) >= 0);
+ assert_se(fnmatch("192.[0-9]*.[0-9]*.0", str, 0) == 0);
+ str = mfree(str);
+
+ assert_se(in_addr_random_prefix(AF_INET, &a, 8, 16) >= 0);
+ assert_se(in_addr_to_string(AF_INET, &a, &str) >= 0);
+ assert_se(fnmatch("192.[0-9]*.0.0", str, 0) == 0);
+ str = mfree(str);
+
+ assert_se(in_addr_from_string(AF_INET6, "fd00::1", &a) >= 0);
+
+ assert_se(in_addr_random_prefix(AF_INET6, &a, 16, 64) >= 0);
+ assert_se(in_addr_to_string(AF_INET6, &a, &str) >= 0);
+ assert_se(startswith(str, "fd00:"));
+ str = mfree(str);
+
+ assert_se(in_addr_random_prefix(AF_INET6, &a, 8, 16) >= 0);
+ assert_se(in_addr_to_string(AF_INET6, &a, &str) >= 0);
+ assert_se(fnmatch("fd??::", str, 0) == 0);
+ str = mfree(str);
+}
+
+TEST(in_addr_is_null) {
+ union in_addr_union i = {};
+
+ assert_se(in_addr_is_null(AF_INET, &i) == true);
+ assert_se(in_addr_is_null(AF_INET6, &i) == true);
+
+ i.in.s_addr = 0x1000000;
+ assert_se(in_addr_is_null(AF_INET, &i) == false);
+ assert_se(in_addr_is_null(AF_INET6, &i) == false);
+
+ assert_se(in_addr_is_null(-1, &i) == -EAFNOSUPPORT);
+}
+
+static void test_in_addr_prefix_intersect_one(unsigned f, const char *a, unsigned apl, const char *b, unsigned bpl, int result) {
+ union in_addr_union ua, ub;
+
+ assert_se(in_addr_from_string(f, a, &ua) >= 0);
+ assert_se(in_addr_from_string(f, b, &ub) >= 0);
+
+ assert_se(in_addr_prefix_intersect(f, &ua, apl, &ub, bpl) == result);
+}
+
+TEST(in_addr_prefix_intersect) {
+ test_in_addr_prefix_intersect_one(AF_INET, "255.255.255.255", 32, "255.255.255.254", 32, 0);
+ test_in_addr_prefix_intersect_one(AF_INET, "255.255.255.255", 0, "255.255.255.255", 32, 1);
+ test_in_addr_prefix_intersect_one(AF_INET, "0.0.0.0", 0, "47.11.8.15", 32, 1);
+
+ test_in_addr_prefix_intersect_one(AF_INET, "1.1.1.1", 24, "1.1.1.1", 24, 1);
+ test_in_addr_prefix_intersect_one(AF_INET, "2.2.2.2", 24, "1.1.1.1", 24, 0);
+
+ test_in_addr_prefix_intersect_one(AF_INET, "1.1.1.1", 24, "1.1.1.127", 25, 1);
+ test_in_addr_prefix_intersect_one(AF_INET, "1.1.1.1", 24, "1.1.1.127", 26, 1);
+ test_in_addr_prefix_intersect_one(AF_INET, "1.1.1.1", 25, "1.1.1.127", 25, 1);
+ test_in_addr_prefix_intersect_one(AF_INET, "1.1.1.1", 25, "1.1.1.255", 25, 0);
+
+ test_in_addr_prefix_intersect_one(AF_INET6, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 128, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe", 128, 0);
+ test_in_addr_prefix_intersect_one(AF_INET6, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 0, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 128, 1);
+ test_in_addr_prefix_intersect_one(AF_INET6, "::", 0, "beef:beef:beef:beef:beef:beef:beef:beef", 128, 1);
+
+ test_in_addr_prefix_intersect_one(AF_INET6, "1::2", 64, "1::2", 64, 1);
+ test_in_addr_prefix_intersect_one(AF_INET6, "2::2", 64, "1::2", 64, 0);
+
+ test_in_addr_prefix_intersect_one(AF_INET6, "1::1", 120, "1::007f", 121, 1);
+ test_in_addr_prefix_intersect_one(AF_INET6, "1::1", 120, "1::007f", 122, 1);
+ test_in_addr_prefix_intersect_one(AF_INET6, "1::1", 121, "1::007f", 121, 1);
+ test_in_addr_prefix_intersect_one(AF_INET6, "1::1", 121, "1::00ff", 121, 0);
+}
+
+static void test_in_addr_prefix_next_one(unsigned f, const char *before, unsigned pl, const char *after) {
+ union in_addr_union ubefore, uafter, t;
+
+ log_debug("/* %s(%s, prefixlen=%u) */", __func__, before, pl);
+
+ assert_se(in_addr_from_string(f, before, &ubefore) >= 0);
+
+ t = ubefore;
+ assert_se((in_addr_prefix_next(f, &t, pl) >= 0) == !!after);
+
+ if (after) {
+ assert_se(in_addr_from_string(f, after, &uafter) >= 0);
+ assert_se(in_addr_equal(f, &t, &uafter) > 0);
+ }
+}
+
+TEST(in_addr_prefix_next) {
+ test_in_addr_prefix_next_one(AF_INET, "192.168.0.0", 24, "192.168.1.0");
+ test_in_addr_prefix_next_one(AF_INET, "192.168.0.0", 16, "192.169.0.0");
+ test_in_addr_prefix_next_one(AF_INET, "192.168.0.0", 20, "192.168.16.0");
+
+ test_in_addr_prefix_next_one(AF_INET, "0.0.0.0", 32, "0.0.0.1");
+ test_in_addr_prefix_next_one(AF_INET, "255.255.255.254", 32, "255.255.255.255");
+ test_in_addr_prefix_next_one(AF_INET, "255.255.255.255", 32, NULL);
+ test_in_addr_prefix_next_one(AF_INET, "255.255.255.0", 24, NULL);
+
+ test_in_addr_prefix_next_one(AF_INET6, "4400::", 128, "4400::0001");
+ test_in_addr_prefix_next_one(AF_INET6, "4400::", 120, "4400::0100");
+ test_in_addr_prefix_next_one(AF_INET6, "4400::", 127, "4400::0002");
+ test_in_addr_prefix_next_one(AF_INET6, "4400::", 8, "4500::");
+ test_in_addr_prefix_next_one(AF_INET6, "4400::", 7, "4600::");
+
+ test_in_addr_prefix_next_one(AF_INET6, "::", 128, "::1");
+
+ test_in_addr_prefix_next_one(AF_INET6, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 128, NULL);
+ test_in_addr_prefix_next_one(AF_INET6, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00", 120, NULL);
+}
+
+static void test_in_addr_prefix_nth_one(unsigned f, const char *before, unsigned pl, uint64_t nth, const char *after) {
+ union in_addr_union ubefore, uafter, t;
+
+ log_debug("/* %s(%s, prefixlen=%u, nth=%"PRIu64") */", __func__, before, pl, nth);
+
+ assert_se(in_addr_from_string(f, before, &ubefore) >= 0);
+
+ t = ubefore;
+ assert_se((in_addr_prefix_nth(f, &t, pl, nth) >= 0) == !!after);
+
+ if (after) {
+ assert_se(in_addr_from_string(f, after, &uafter) >= 0);
+ assert_se(in_addr_equal(f, &t, &uafter) > 0);
+ }
+}
+
+TEST(in_addr_prefix_nth) {
+ test_in_addr_prefix_nth_one(AF_INET, "192.168.0.0", 24, 0, "192.168.0.0");
+ test_in_addr_prefix_nth_one(AF_INET, "192.168.0.123", 24, 0, "192.168.0.0");
+ test_in_addr_prefix_nth_one(AF_INET, "192.168.0.123", 24, 1, "192.168.1.0");
+ test_in_addr_prefix_nth_one(AF_INET, "192.168.0.0", 24, 4, "192.168.4.0");
+ test_in_addr_prefix_nth_one(AF_INET, "192.168.0.0", 25, 1, "192.168.0.128");
+ test_in_addr_prefix_nth_one(AF_INET, "192.168.255.0", 25, 1, "192.168.255.128");
+ test_in_addr_prefix_nth_one(AF_INET, "192.168.255.0", 24, 0, "192.168.255.0");
+ test_in_addr_prefix_nth_one(AF_INET, "255.255.255.255", 32, 1, NULL);
+ test_in_addr_prefix_nth_one(AF_INET, "255.255.255.255", 0, 1, NULL);
+
+ test_in_addr_prefix_nth_one(AF_INET6, "4400::", 8, 1, "4500::");
+ test_in_addr_prefix_nth_one(AF_INET6, "4400::", 7, 1, "4600::");
+ test_in_addr_prefix_nth_one(AF_INET6, "4400::", 64, 1, "4400:0:0:1::");
+ test_in_addr_prefix_nth_one(AF_INET6, "4400::", 64, 2, "4400:0:0:2::");
+ test_in_addr_prefix_nth_one(AF_INET6, "4400::", 64, 0xbad, "4400:0:0:0bad::");
+ test_in_addr_prefix_nth_one(AF_INET6, "4400:0:0:ffff::", 64, 1, "4400:0:1::");
+ test_in_addr_prefix_nth_one(AF_INET6, "4400::", 56, ((uint64_t)1<<48) -1, "44ff:ffff:ffff:ff00::");
+ test_in_addr_prefix_nth_one(AF_INET6, "0000::", 8, 255, "ff00::");
+ test_in_addr_prefix_nth_one(AF_INET6, "0000::", 8, 256, NULL);
+ test_in_addr_prefix_nth_one(AF_INET6, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 128, 1, NULL);
+ test_in_addr_prefix_nth_one(AF_INET6, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 0, 1, NULL);
+ test_in_addr_prefix_nth_one(AF_INET6, "1234:5678:90ab:cdef:1234:5678:90ab:cdef", 12, 1, "1240::");
+}
+
+static void test_in_addr_prefix_range_one(
+ int family,
+ const char *in,
+ unsigned prefixlen,
+ const char *expected_start,
+ const char *expected_end) {
+
+ union in_addr_union a, s, e;
+
+ log_debug("/* %s(%s, prefixlen=%u) */", __func__, in, prefixlen);
+
+ assert_se(in_addr_from_string(family, in, &a) >= 0);
+ assert_se((in_addr_prefix_range(family, &a, prefixlen, &s, &e) >= 0) == !!expected_start);
+
+ if (expected_start) {
+ union in_addr_union es;
+
+ assert_se(in_addr_from_string(family, expected_start, &es) >= 0);
+ assert_se(in_addr_equal(family, &s, &es) > 0);
+ }
+ if (expected_end) {
+ union in_addr_union ee;
+
+ assert_se(in_addr_from_string(family, expected_end, &ee) >= 0);
+ assert_se(in_addr_equal(family, &e, &ee) > 0);
+ }
+}
+
+TEST(in_addr_prefix_range) {
+ test_in_addr_prefix_range_one(AF_INET, "192.168.123.123", 24, "192.168.123.0", "192.168.124.0");
+ test_in_addr_prefix_range_one(AF_INET, "192.168.123.123", 16, "192.168.0.0", "192.169.0.0");
+
+ test_in_addr_prefix_range_one(AF_INET6, "dead:beef::", 64, "dead:beef::", "dead:beef:0:1::");
+ test_in_addr_prefix_range_one(AF_INET6, "dead:0:0:beef::", 64, "dead:0:0:beef::", "dead:0:0:bef0::");
+ test_in_addr_prefix_range_one(AF_INET6, "2001::", 48, "2001::", "2001:0:1::");
+ test_in_addr_prefix_range_one(AF_INET6, "2001::", 56, "2001::", "2001:0:0:0100::");
+ test_in_addr_prefix_range_one(AF_INET6, "2001::", 65, "2001::", "2001::8000:0:0:0");
+ test_in_addr_prefix_range_one(AF_INET6, "2001::", 66, "2001::", "2001::4000:0:0:0");
+ test_in_addr_prefix_range_one(AF_INET6, "2001::", 127, "2001::", "2001::2");
+}
+
+static void test_in_addr_to_string_one(int f, const char *addr) {
+ union in_addr_union ua;
+ _cleanup_free_ char *r;
+
+ assert_se(in_addr_from_string(f, addr, &ua) >= 0);
+ assert_se(in_addr_to_string(f, &ua, &r) >= 0);
+ printf("%s: %s == %s\n", __func__, addr, r);
+ assert_se(streq(addr, r));
+
+ assert_se(streq(r, IN_ADDR_TO_STRING(f, &ua)));
+}
+
+TEST(in_addr_to_string) {
+ test_in_addr_to_string_one(AF_INET, "192.168.0.1");
+ test_in_addr_to_string_one(AF_INET, "10.11.12.13");
+ test_in_addr_to_string_one(AF_INET6, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+ test_in_addr_to_string_one(AF_INET6, "::1");
+ test_in_addr_to_string_one(AF_INET6, "fe80::");
+}
+
+TEST(in_addr_prefixlen_to_netmask) {
+ union in_addr_union addr;
+ static const char *const ipv4_netmasks[] = {
+ "0.0.0.0", "128.0.0.0", "192.0.0.0", "224.0.0.0", "240.0.0.0",
+ "248.0.0.0", "252.0.0.0", "254.0.0.0", "255.0.0.0",
+ "255.128.0.0", "255.192.0.0", "255.224.0.0", "255.240.0.0",
+ "255.248.0.0", "255.252.0.0", "255.254.0.0", "255.255.0.0",
+ "255.255.128.0", "255.255.192.0", "255.255.224.0", "255.255.240.0",
+ "255.255.248.0", "255.255.252.0", "255.255.254.0", "255.255.255.0",
+ "255.255.255.128", "255.255.255.192", "255.255.255.224", "255.255.255.240",
+ "255.255.255.248", "255.255.255.252", "255.255.255.254", "255.255.255.255",
+ };
+
+ static const char *const ipv6_netmasks[] = {
+ [0] = "::",
+ [1] = "8000::",
+ [2] = "c000::",
+ [7] = "fe00::",
+ [8] = "ff00::",
+ [9] = "ff80::",
+ [16] = "ffff::",
+ [17] = "ffff:8000::",
+ [32] = "ffff:ffff::",
+ [33] = "ffff:ffff:8000::",
+ [64] = "ffff:ffff:ffff:ffff::",
+ [65] = "ffff:ffff:ffff:ffff:8000::",
+ [96] = "ffff:ffff:ffff:ffff:ffff:ffff::",
+ [97] = "ffff:ffff:ffff:ffff:ffff:ffff:8000:0",
+ [127] = "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe",
+ [128] = "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"
+ };
+
+ for (unsigned char prefixlen = 0; prefixlen <= 32; prefixlen++) {
+ _cleanup_free_ char *result = NULL;
+
+ assert_se(in_addr_prefixlen_to_netmask(AF_INET, &addr, prefixlen) >= 0);
+ assert_se(in_addr_to_string(AF_INET, &addr, &result) >= 0);
+ printf("test_in_addr_prefixlen_to_netmask: %s == %s\n", ipv4_netmasks[prefixlen], result);
+ assert_se(streq(ipv4_netmasks[prefixlen], result));
+ }
+
+ for (unsigned char prefixlen = 0; prefixlen <= 128; prefixlen++) {
+ _cleanup_free_ char *result = NULL;
+
+ assert_se(in_addr_prefixlen_to_netmask(AF_INET6, &addr, prefixlen) >= 0);
+ assert_se(in_addr_to_string(AF_INET6, &addr, &result) >= 0);
+ printf("test_in_addr_prefixlen_to_netmask: %s\n", result);
+ if (ipv6_netmasks[prefixlen])
+ assert_se(streq(ipv6_netmasks[prefixlen], result));
+ }
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-install-file.c b/src/test/test-install-file.c
new file mode 100644
index 0000000..8206eb0
--- /dev/null
+++ b/src/test/test-install-file.c
@@ -0,0 +1,64 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "fileio.h"
+#include "install-file.h"
+#include "path-util.h"
+#include "rm-rf.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+#include "umask-util.h"
+
+TEST(install_file) {
+ _cleanup_(rm_rf_physical_and_freep) char *p = NULL;
+ _cleanup_free_ char *a = NULL, *b = NULL, *c = NULL;
+ struct stat stat1, stat2;
+
+ assert_se(mkdtemp_malloc(NULL, &p) >= 0);
+ assert_se(a = path_join(p, "foo"));
+ assert_se(b = path_join(p, "bar"));
+
+ WITH_UMASK(0077)
+ assert_se(write_string_file(a, "wups", WRITE_STRING_FILE_CREATE) >= 0);
+
+ assert_se(lstat(a, &stat1) >= 0);
+ assert_se(S_ISREG(stat1.st_mode));
+
+ assert_se(install_file(AT_FDCWD, a, AT_FDCWD, b, 0) >= 0);
+ assert_se(install_file(AT_FDCWD, b, AT_FDCWD, a, INSTALL_FSYNC) >= 0);
+
+ assert_se(write_string_file(b, "ttss", WRITE_STRING_FILE_CREATE) >= 0);
+ assert_se(install_file(AT_FDCWD, a, AT_FDCWD, b, INSTALL_FSYNC_FULL) == -EEXIST);
+ assert_se(install_file(AT_FDCWD, a, AT_FDCWD, b, INSTALL_FSYNC_FULL|INSTALL_REPLACE) >= 0);
+
+ assert_se(stat(b, &stat2) >= 0);
+ assert_se(stat1.st_dev == stat2.st_dev);
+ assert_se(stat1.st_ino == stat2.st_ino);
+ assert_se((stat2.st_mode & 0222) != 0); /* writable */
+
+ assert_se(install_file(AT_FDCWD, b, AT_FDCWD, a, INSTALL_FSYNC_FULL|INSTALL_REPLACE|INSTALL_READ_ONLY) >= 0);
+
+ assert_se(stat(a, &stat2) >= 0);
+ assert_se(stat1.st_dev == stat2.st_dev);
+ assert_se(stat1.st_ino == stat2.st_ino);
+ assert_se((stat2.st_mode & 0222) == 0); /* read-only */
+
+ assert_se(mkdir(b, 0755) >= 0);
+ assert_se(c = path_join(b, "dir"));
+ assert_se(mkdir(c, 0755) >= 0);
+ free(c);
+ assert_se(c = path_join(b, "reg"));
+ assert_se(mknod(c, S_IFREG|0755, 0) >= 0);
+ free(c);
+ assert_se(c = path_join(b, "fifo"));
+ assert_se(mknod(c, S_IFIFO|0755, 0) >= 0);
+
+ assert_se(install_file(AT_FDCWD, b, AT_FDCWD, a, INSTALL_FSYNC_FULL) == -EEXIST);
+ assert_se(install_file(AT_FDCWD, b, AT_FDCWD, a, INSTALL_FSYNC_FULL|INSTALL_REPLACE) == 0);
+
+ assert_se(write_string_file(b, "ttss", WRITE_STRING_FILE_CREATE) >= 0);
+
+ assert_se(install_file(AT_FDCWD, b, AT_FDCWD, a, INSTALL_FSYNC_FULL) == -EEXIST);
+ assert_se(install_file(AT_FDCWD, b, AT_FDCWD, a, INSTALL_FSYNC_FULL|INSTALL_REPLACE) == 0);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-install-root.c b/src/test/test-install-root.c
new file mode 100644
index 0000000..efd75b2
--- /dev/null
+++ b/src/test/test-install-root.c
@@ -0,0 +1,1299 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "install.h"
+#include "mkdir.h"
+#include "rm-rf.h"
+#include "special.h"
+#include "string-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+static char *root = NULL;
+
+STATIC_DESTRUCTOR_REGISTER(root, rm_rf_physical_and_freep);
+
+TEST(basic_mask_and_enable) {
+ const char *p;
+ UnitFileState state;
+ InstallChange *changes = NULL;
+ size_t n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "a.service", NULL) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "b.service", NULL) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "c.service", NULL) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "d.service", NULL) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "e.service", NULL) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "f.service", NULL) == -ENOENT);
+
+ p = strjoina(root, "/usr/lib/systemd/system/a.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "a.service", NULL) >= 0);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "a.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ p = strjoina(root, "/usr/lib/systemd/system/b.service");
+ assert_se(symlink("a.service", p) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "b.service", NULL) >= 0);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "b.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+
+ p = strjoina(root, "/usr/lib/systemd/system/c.service");
+ assert_se(symlink("/usr/lib/systemd/system/a.service", p) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "c.service", NULL) >= 0);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "c.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+
+ p = strjoina(root, "/usr/lib/systemd/system/d.service");
+ assert_se(symlink("c.service", p) >= 0);
+
+ /* This one is interesting, as d follows a relative, then an absolute symlink */
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "d.service", NULL) >= 0);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "d.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+
+ assert_se(unit_file_mask(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("a.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/dev/null"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/a.service");
+ assert_se(streq(changes[0].path, p));
+
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "a.service", &state) >= 0 && state == UNIT_FILE_MASKED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "b.service", &state) >= 0 && state == UNIT_FILE_MASKED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "c.service", &state) >= 0 && state == UNIT_FILE_MASKED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "d.service", &state) >= 0 && state == UNIT_FILE_MASKED);
+
+ /* Enabling a masked unit should fail! */
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("a.service"), &changes, &n_changes) == -ERFKILL);
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_unmask(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("a.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_UNLINK);
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/a.service");
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("a.service"), &changes, &n_changes) == 1);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/usr/lib/systemd/system/a.service"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/a.service");
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "a.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "b.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "c.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "d.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+
+ /* Enabling it again should succeed but be a NOP */
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("a.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 0);
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_disable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("a.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_UNLINK);
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/a.service");
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "a.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "b.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "c.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "d.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+
+ /* Disabling a disabled unit must succeed but be a NOP */
+ assert_se(unit_file_disable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("a.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 0);
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ /* Let's enable this indirectly via a symlink */
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("d.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/usr/lib/systemd/system/a.service"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/a.service");
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "a.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "b.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "c.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "d.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+
+ /* Let's try to reenable */
+
+ assert_se(unit_file_reenable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("b.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 2);
+ assert_se(changes[0].type == INSTALL_CHANGE_UNLINK);
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/a.service");
+ assert_se(streq(changes[0].path, p));
+ assert_se(changes[1].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[1].source, "/usr/lib/systemd/system/a.service"));
+ assert_se(streq(changes[1].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "a.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "b.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "c.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "d.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+
+ /* Test masking with relative symlinks */
+
+ p = strjoina(root, "/usr/lib/systemd/system/e.service");
+ assert_se(symlink("../../../../../../dev/null", p) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "e.service", NULL) >= 0);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "e.service", &state) >= 0 && state == UNIT_FILE_MASKED);
+
+ assert_se(unlink(p) == 0);
+ assert_se(symlink("/usr/../dev/null", p) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "e.service", NULL) >= 0);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "e.service", &state) >= 0 && state == UNIT_FILE_MASKED);
+
+ assert_se(unlink(p) == 0);
+
+ /* Test enabling with unknown dependency target */
+
+ p = strjoina(root, "/usr/lib/systemd/system/f.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=x.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "f.service", NULL) >= 0);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "f.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("f.service"), &changes, &n_changes) == 1);
+ assert_se(n_changes == 2);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/usr/lib/systemd/system/f.service"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/x.target.wants/f.service");
+ assert_se(streq(changes[0].path, p));
+ assert_se(changes[1].type == INSTALL_CHANGE_DESTINATION_NOT_PRESENT);
+ p = strjoina(root, "/usr/lib/systemd/system/f.service");
+ assert_se(streq(changes[1].source, p));
+ assert_se(streq(changes[1].path, "x.target"));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "f.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+}
+
+TEST(linked_units) {
+ const char *p, *q;
+ UnitFileState state;
+ InstallChange *changes = NULL;
+ size_t n_changes = 0, i;
+
+ /*
+ * We'll test three cases here:
+ *
+ * a) a unit file in /opt, that we use "systemctl link" and
+ * "systemctl enable" on to make it available to the system
+ *
+ * b) a unit file in /opt, that is statically linked into
+ * /usr/lib/systemd/system, that "enable" should work on
+ * correctly.
+ *
+ * c) a unit file in /opt, that is linked into
+ * /etc/systemd/system, and where "enable" should result in
+ * -ELOOP, since using information from /etc to generate
+ * information in /etc should not be allowed.
+ */
+
+ p = strjoina(root, "/opt/linked.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, "/opt/linked2.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, "/opt/linked3.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "linked.service", NULL) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "linked2.service", NULL) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "linked3.service", NULL) == -ENOENT);
+
+ p = strjoina(root, "/usr/lib/systemd/system/linked2.service");
+ assert_se(symlink("/opt/linked2.service", p) >= 0);
+
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/linked3.service");
+ assert_se(symlink("/opt/linked3.service", p) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "linked.service", &state) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "linked2.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "linked3.service", &state) >= 0 && state == UNIT_FILE_LINKED);
+
+ /* First, let's link the unit into the search path */
+ assert_se(unit_file_link(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("/opt/linked.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/opt/linked.service"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/linked.service");
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "linked.service", &state) >= 0 && state == UNIT_FILE_LINKED);
+
+ /* Let's unlink it from the search path again */
+ assert_se(unit_file_disable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("linked.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_UNLINK);
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/linked.service");
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "linked.service", NULL) == -ENOENT);
+
+ /* Now, let's not just link it, but also enable it */
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("/opt/linked.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 2);
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/linked.service");
+ q = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/linked.service");
+ for (i = 0 ; i < n_changes; i++) {
+ assert_se(changes[i].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[i].source, "/opt/linked.service"));
+
+ if (p && streq(changes[i].path, p))
+ p = NULL;
+ else if (q && streq(changes[i].path, q))
+ q = NULL;
+ else
+ assert_not_reached();
+ }
+ assert_se(!p && !q);
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "linked.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+
+ /* And let's unlink it again */
+ assert_se(unit_file_disable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("linked.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 2);
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/linked.service");
+ q = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/linked.service");
+ for (i = 0; i < n_changes; i++) {
+ assert_se(changes[i].type == INSTALL_CHANGE_UNLINK);
+
+ if (p && streq(changes[i].path, p))
+ p = NULL;
+ else if (q && streq(changes[i].path, q))
+ q = NULL;
+ else
+ assert_not_reached();
+ }
+ assert_se(!p && !q);
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "linked.service", NULL) == -ENOENT);
+
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("linked2.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 2);
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/linked2.service");
+ q = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/linked2.service");
+ for (i = 0 ; i < n_changes; i++) {
+ assert_se(changes[i].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[i].source, "/opt/linked2.service"));
+
+ if (p && streq(changes[i].path, p))
+ p = NULL;
+ else if (q && streq(changes[i].path, q))
+ q = NULL;
+ else
+ assert_not_reached();
+ }
+ assert_se(!p && !q);
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("linked3.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(startswith(changes[0].path, root));
+ assert_se(endswith(changes[0].path, "linked3.service"));
+ assert_se(streq(changes[0].source, "/opt/linked3.service"));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+}
+
+TEST(default) {
+ _cleanup_free_ char *def = NULL;
+ InstallChange *changes = NULL;
+ size_t n_changes = 0;
+ const char *p;
+
+ p = strjoina(root, "/usr/lib/systemd/system/test-default-real.target");
+ assert_se(write_string_file(p, "# pretty much empty", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system/test-default.target");
+ assert_se(symlink("test-default-real.target", p) >= 0);
+
+ assert_se(unit_file_get_default(RUNTIME_SCOPE_SYSTEM, root, &def) == -ENOENT);
+
+ assert_se(unit_file_set_default(RUNTIME_SCOPE_SYSTEM, 0, root, "idontexist.target", &changes, &n_changes) == -ENOENT);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == -ENOENT);
+ assert_se(streq_ptr(changes[0].path, "idontexist.target"));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_default(RUNTIME_SCOPE_SYSTEM, root, &def) == -ENOENT);
+
+ assert_se(unit_file_set_default(RUNTIME_SCOPE_SYSTEM, 0, root, "test-default.target", &changes, &n_changes) >= 0);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/usr/lib/systemd/system/test-default-real.target"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR "/" SPECIAL_DEFAULT_TARGET);
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_default(RUNTIME_SCOPE_SYSTEM, root, &def) >= 0);
+ assert_se(streq_ptr(def, "test-default-real.target"));
+}
+
+TEST(add_dependency) {
+ InstallChange *changes = NULL;
+ size_t n_changes = 0;
+ const char *p;
+
+ p = strjoina(root, "/usr/lib/systemd/system/real-add-dependency-test-target.target");
+ assert_se(write_string_file(p, "# pretty much empty", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system/add-dependency-test-target.target");
+ assert_se(symlink("real-add-dependency-test-target.target", p) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system/real-add-dependency-test-service.service");
+ assert_se(write_string_file(p, "# pretty much empty", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system/add-dependency-test-service.service");
+ assert_se(symlink("real-add-dependency-test-service.service", p) >= 0);
+
+ assert_se(unit_file_add_dependency(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("add-dependency-test-service.service"), "add-dependency-test-target.target", UNIT_WANTS, &changes, &n_changes) >= 0);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/usr/lib/systemd/system/real-add-dependency-test-service.service"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/real-add-dependency-test-target.target.wants/real-add-dependency-test-service.service");
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+}
+
+TEST(template_enable) {
+ InstallChange *changes = NULL;
+ size_t n_changes = 0;
+ UnitFileState state;
+ const char *p;
+
+ log_info("== %s ==", __func__);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@.service", &state) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@def.service", &state) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@foo.service", &state) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@foo.service", &state) == -ENOENT);
+
+ p = strjoina(root, "/usr/lib/systemd/system/template@.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "DefaultInstance=def\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system/template-symlink@.service");
+ assert_se(symlink("template@.service", p) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@def.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@foo.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@def.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@foo.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ log_info("== %s with template@.service enabled ==", __func__);
+
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("template@.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/usr/lib/systemd/system/template@.service"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/template@def.service");
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@def.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@foo.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@def.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@foo.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ assert_se(unit_file_disable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("template@.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_UNLINK);
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@def.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@foo.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@def.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@foo.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ log_info("== %s with template@foo.service enabled ==", __func__);
+
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("template@foo.service"), &changes, &n_changes) >= 0);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/usr/lib/systemd/system/template@.service"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/template@foo.service");
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@.service", &state) >= 0 && state == UNIT_FILE_INDIRECT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@def.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@foo.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@foo.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@def.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@foo.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+
+ assert_se(unit_file_disable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("template@foo.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_UNLINK);
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@def.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@foo.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@quux.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@def.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@foo.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@quux.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ log_info("== %s with template-symlink@quux.service enabled ==", __func__);
+
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("template-symlink@quux.service"), &changes, &n_changes) >= 0);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/usr/lib/systemd/system/template@.service"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/template@quux.service");
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@.service", &state) >= 0 && state == UNIT_FILE_INDIRECT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@def.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@foo.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template@quux.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@def.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@foo.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "template-symlink@quux.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+}
+
+TEST(indirect) {
+ InstallChange *changes = NULL;
+ size_t n_changes = 0;
+ UnitFileState state;
+ const char *p;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "indirecta.service", &state) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "indirectb.service", &state) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "indirectc.service", &state) == -ENOENT);
+
+ p = strjoina(root, "/usr/lib/systemd/system/indirecta.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "Also=indirectb.service\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system/indirectb.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system/indirectc.service");
+ assert_se(symlink("indirecta.service", p) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "indirecta.service", &state) >= 0 && state == UNIT_FILE_INDIRECT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "indirectb.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "indirectc.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("indirectc.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/usr/lib/systemd/system/indirectb.service"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/indirectb.service");
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "indirecta.service", &state) >= 0 && state == UNIT_FILE_INDIRECT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "indirectb.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "indirectc.service", &state) >= 0 && state == UNIT_FILE_ALIAS);
+
+ assert_se(unit_file_disable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("indirectc.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_UNLINK);
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/indirectb.service");
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+}
+
+TEST(preset_and_list) {
+ InstallChange *changes = NULL;
+ size_t n_changes = 0, i;
+ const char *p, *q;
+ UnitFileState state;
+ bool got_yes = false, got_no = false;
+ UnitFileList *fl;
+ _cleanup_hashmap_free_ Hashmap *h = NULL;
+
+ CLEANUP_ARRAY(changes, n_changes, install_changes_free);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "preset-yes.service", &state) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "preset-no.service", &state) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "preset-ignore.service", &state) == -ENOENT);
+
+ p = strjoina(root, "/usr/lib/systemd/system/preset-yes.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system/preset-no.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system/preset-ignore.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system-preset/test.preset");
+ assert_se(write_string_file(p,
+ "enable *-yes.*\n"
+ "ignore *-ignore.*\n"
+ "disable *\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "preset-yes.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "preset-no.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "preset-ignore.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ assert_se(unit_file_preset(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("preset-yes.service"), UNIT_FILE_PRESET_FULL, &changes, &n_changes) >= 0);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/usr/lib/systemd/system/preset-yes.service"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/preset-yes.service");
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "preset-yes.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "preset-no.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "preset-ignore.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ assert_se(unit_file_disable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("preset-yes.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_UNLINK);
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/preset-yes.service");
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "preset-yes.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "preset-no.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "preset-ignore.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ assert_se(unit_file_preset(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("preset-no.service"), UNIT_FILE_PRESET_FULL, &changes, &n_changes) >= 0);
+ assert_se(n_changes == 0);
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "preset-yes.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "preset-no.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "preset-ignore.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ assert_se(unit_file_preset_all(RUNTIME_SCOPE_SYSTEM, 0, root, UNIT_FILE_PRESET_FULL, &changes, &n_changes) >= 0);
+
+ assert_se(n_changes > 0);
+
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/preset-yes.service");
+
+ for (i = 0; i < n_changes; i++) {
+
+ if (changes[i].type == INSTALL_CHANGE_SYMLINK) {
+ assert_se(streq(changes[i].source, "/usr/lib/systemd/system/preset-yes.service"));
+ assert_se(streq(changes[i].path, p));
+ } else
+ assert_se(changes[i].type == INSTALL_CHANGE_UNLINK);
+ }
+
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "preset-yes.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "preset-no.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "preset-ignore.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ assert_se(h = hashmap_new(&unit_file_list_hash_ops_free));
+ assert_se(unit_file_get_list(RUNTIME_SCOPE_SYSTEM, root, h, NULL, NULL) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system/preset-yes.service");
+ q = strjoina(root, "/usr/lib/systemd/system/preset-no.service");
+
+ HASHMAP_FOREACH(fl, h) {
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, basename(fl->path), &state) >= 0);
+ assert_se(fl->state == state);
+
+ if (streq(fl->path, p)) {
+ got_yes = true;
+ assert_se(fl->state == UNIT_FILE_ENABLED);
+ } else if (streq(fl->path, q)) {
+ got_no = true;
+ assert_se(fl->state == UNIT_FILE_DISABLED);
+ } else
+ assert_se(IN_SET(fl->state, UNIT_FILE_DISABLED, UNIT_FILE_STATIC, UNIT_FILE_INDIRECT, UNIT_FILE_ALIAS));
+ }
+
+ assert_se(got_yes && got_no);
+
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("preset-ignore.service"), &changes, &n_changes) >= 0);
+ assert_se(unit_file_preset(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("preset-ignore.service"), UNIT_FILE_PRESET_FULL, &changes, &n_changes) >= 0);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "preset-ignore.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+}
+
+TEST(revert) {
+ const char *p;
+ UnitFileState state;
+ InstallChange *changes = NULL;
+ size_t n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "xx.service", NULL) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "yy.service", NULL) == -ENOENT);
+
+ p = strjoina(root, "/usr/lib/systemd/system/xx.service");
+ assert_se(write_string_file(p, "# Empty\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "xx.service", NULL) >= 0);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "xx.service", &state) >= 0 && state == UNIT_FILE_STATIC);
+
+ /* Initially there's nothing to revert */
+ assert_se(unit_file_revert(RUNTIME_SCOPE_SYSTEM, root, STRV_MAKE("xx.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 0);
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/xx.service");
+ assert_se(write_string_file(p, "# Empty override\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ /* Revert the override file */
+ assert_se(unit_file_revert(RUNTIME_SCOPE_SYSTEM, root, STRV_MAKE("xx.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_UNLINK);
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/xx.service.d/dropin.conf");
+ assert_se(write_string_file(p, "# Empty dropin\n", WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755) >= 0);
+
+ /* Revert the dropin file */
+ assert_se(unit_file_revert(RUNTIME_SCOPE_SYSTEM, root, STRV_MAKE("xx.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 2);
+ assert_se(changes[0].type == INSTALL_CHANGE_UNLINK);
+ assert_se(streq(changes[0].path, p));
+
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/xx.service.d");
+ assert_se(changes[1].type == INSTALL_CHANGE_UNLINK);
+ assert_se(streq(changes[1].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+}
+
+TEST(preset_order) {
+ InstallChange *changes = NULL;
+ size_t n_changes = 0;
+ const char *p;
+ UnitFileState state;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "prefix-1.service", &state) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "prefix-2.service", &state) == -ENOENT);
+
+ p = strjoina(root, "/usr/lib/systemd/system/prefix-1.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system/prefix-2.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system-preset/test.preset");
+ assert_se(write_string_file(p,
+ "enable prefix-1.service\n"
+ "disable prefix-*.service\n"
+ "enable prefix-2.service\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "prefix-1.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "prefix-2.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ assert_se(unit_file_preset(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("prefix-1.service"), UNIT_FILE_PRESET_FULL, &changes, &n_changes) >= 0);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/usr/lib/systemd/system/prefix-1.service"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/prefix-1.service");
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "prefix-1.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "prefix-2.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ assert_se(unit_file_preset(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("prefix-2.service"), UNIT_FILE_PRESET_FULL, &changes, &n_changes) >= 0);
+ assert_se(n_changes == 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "prefix-1.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "prefix-2.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+}
+
+TEST(static_instance) {
+ UnitFileState state;
+ const char *p;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "static-instance@.service", &state) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "static-instance@foo.service", &state) == -ENOENT);
+
+ p = strjoina(root, "/usr/lib/systemd/system/static-instance@.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "static-instance@.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "static-instance@foo.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ p = strjoina(root, "/usr/lib/systemd/system/static-instance@foo.service");
+ assert_se(symlink("static-instance@.service", p) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "static-instance@.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "static-instance@foo.service", &state) >= 0 && state == UNIT_FILE_STATIC);
+}
+
+TEST(with_dropin) {
+ const char *p;
+ UnitFileState state;
+ InstallChange *changes = NULL;
+ size_t n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-1.service", &state) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-2.service", &state) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-3.service", &state) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-4a.service", &state) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-4b.service", &state) == -ENOENT);
+
+ p = strjoina(root, "/usr/lib/systemd/system/with-dropin-1.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system/with-dropin-1.service.d/dropin.conf");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=graphical.target\n", WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-1.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/with-dropin-2.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system/with-dropin-2.service.d/dropin.conf");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=graphical.target\n", WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-2.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ p = strjoina(root, "/usr/lib/systemd/system/with-dropin-3.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/with-dropin-3.service.d/dropin.conf");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=graphical.target\n", WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-3.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ p = strjoina(root, "/usr/lib/systemd/system/with-dropin-4a.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/with-dropin-4a.service.d/dropin.conf");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "Also=with-dropin-4b.service\n", WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-4a.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ p = strjoina(root, "/usr/lib/systemd/system/with-dropin-4b.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-4b.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("with-dropin-1.service"), &changes, &n_changes) == 1);
+ assert_se(n_changes == 2);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(changes[1].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/usr/lib/systemd/system/with-dropin-1.service"));
+ assert_se(streq(changes[1].source, "/usr/lib/systemd/system/with-dropin-1.service"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/with-dropin-1.service");
+ assert_se(streq(changes[0].path, p));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/graphical.target.wants/with-dropin-1.service");
+ assert_se(streq(changes[1].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("with-dropin-2.service"), &changes, &n_changes) == 1);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-2.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(n_changes == 2);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(changes[1].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, SYSTEM_CONFIG_UNIT_DIR"/with-dropin-2.service"));
+ assert_se(streq(changes[1].source, SYSTEM_CONFIG_UNIT_DIR"/with-dropin-2.service"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/with-dropin-2.service");
+ assert_se(streq(changes[0].path, p));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/graphical.target.wants/with-dropin-2.service");
+ assert_se(streq(changes[1].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("with-dropin-3.service"), &changes, &n_changes) == 1);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-3.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(n_changes == 2);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(changes[1].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/usr/lib/systemd/system/with-dropin-3.service"));
+ assert_se(streq(changes[1].source, "/usr/lib/systemd/system/with-dropin-3.service"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/with-dropin-3.service");
+ assert_se(streq(changes[0].path, p));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/graphical.target.wants/with-dropin-3.service");
+ assert_se(streq(changes[1].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("with-dropin-4a.service"), &changes, &n_changes) == 2);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-3.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(n_changes == 2);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(changes[1].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/usr/lib/systemd/system/with-dropin-4a.service"));
+ assert_se(streq(changes[1].source, "/usr/lib/systemd/system/with-dropin-4b.service"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/with-dropin-4a.service");
+ assert_se(streq(changes[0].path, p));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/with-dropin-4b.service");
+ assert_se(streq(changes[1].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-1.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-2.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-3.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-4a.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-4b.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+}
+
+TEST(with_dropin_template) {
+ const char *p;
+ UnitFileState state;
+ InstallChange *changes = NULL;
+ size_t n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-1@.service", &state) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-2@.service", &state) == -ENOENT);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-3@.service", &state) == -ENOENT);
+
+ p = strjoina(root, "/usr/lib/systemd/system/with-dropin-1@.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system/with-dropin-1@.service.d/dropin.conf");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=graphical.target\n", WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-1@.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ p = strjoina(root, "/usr/lib/systemd/system/with-dropin-2@.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system/with-dropin-2@instance-1.service.d/dropin.conf");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "WantedBy=graphical.target\n", WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-2@.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ p = strjoina(root, "/usr/lib/systemd/system/with-dropin-3@.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "DefaultInstance=instance-1\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system/with-dropin-3@.service.d/dropin.conf");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "DefaultInstance=instance-2\n", WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_MKDIR_0755) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-3@.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("with-dropin-1@instance-1.service"), &changes, &n_changes) == 1);
+ assert_se(n_changes == 2);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(changes[1].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/usr/lib/systemd/system/with-dropin-1@.service"));
+ assert_se(streq(changes[1].source, "/usr/lib/systemd/system/with-dropin-1@.service"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/with-dropin-1@instance-1.service");
+ assert_se(streq(changes[0].path, p));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/graphical.target.wants/with-dropin-1@instance-1.service");
+ assert_se(streq(changes[1].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("with-dropin-2@instance-1.service"), &changes, &n_changes) == 1);
+ assert_se(n_changes == 2);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(changes[1].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/usr/lib/systemd/system/with-dropin-2@.service"));
+ assert_se(streq(changes[1].source, "/usr/lib/systemd/system/with-dropin-2@.service"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/with-dropin-2@instance-1.service");
+ assert_se(streq(changes[0].path, p));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/graphical.target.wants/with-dropin-2@instance-1.service");
+ assert_se(streq(changes[1].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("with-dropin-2@instance-2.service"), &changes, &n_changes) == 1);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/usr/lib/systemd/system/with-dropin-2@.service"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/with-dropin-2@instance-2.service");
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("with-dropin-3@.service"), &changes, &n_changes) == 1);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ assert_se(streq(changes[0].source, "/usr/lib/systemd/system/with-dropin-3@.service"));
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/with-dropin-3@instance-2.service");
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-1@instance-1.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-2@instance-1.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-2@instance-2.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-3@instance-1.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "with-dropin-3@instance-2.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+}
+
+TEST(preset_multiple_instances) {
+ InstallChange *changes = NULL;
+ size_t n_changes = 0;
+ const char *p;
+ UnitFileState state;
+
+ /* Set up template service files and preset file */
+ p = strjoina(root, "/usr/lib/systemd/system/foo@.service");
+ assert_se(write_string_file(p,
+ "[Install]\n"
+ "DefaultInstance=def\n"
+ "WantedBy=multi-user.target\n", WRITE_STRING_FILE_CREATE) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "foo@.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ p = strjoina(root, "/usr/lib/systemd/system-preset/test.preset");
+ assert_se(write_string_file(p,
+ "enable foo@.service bar0 bar1 bartest\n"
+ "enable emptylist@.service\n" /* This line ensures the old functionality for templated unit still works */
+ "disable *\n" , WRITE_STRING_FILE_CREATE) >= 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "foo@bar0.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ /* Preset a single instantiated unit specified in the list */
+ assert_se(unit_file_preset(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("foo@bar0.service"), UNIT_FILE_PRESET_FULL, &changes, &n_changes) >= 0);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "foo@bar0.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_SYMLINK);
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/foo@bar0.service");
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ assert_se(unit_file_disable(RUNTIME_SCOPE_SYSTEM, 0, root, STRV_MAKE("foo@bar0.service"), &changes, &n_changes) >= 0);
+ assert_se(n_changes == 1);
+ assert_se(changes[0].type == INSTALL_CHANGE_UNLINK);
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/multi-user.target.wants/foo@bar0.service");
+ assert_se(streq(changes[0].path, p));
+ install_changes_free(changes, n_changes);
+ changes = NULL; n_changes = 0;
+
+ /* Check for preset-all case, only instances on the list should be enabled, not including the default instance */
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "foo@def.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "foo@bar1.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "foo@bartest.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+
+ assert_se(unit_file_preset_all(RUNTIME_SCOPE_SYSTEM, 0, root, UNIT_FILE_PRESET_FULL, &changes, &n_changes) >= 0);
+ assert_se(n_changes > 0);
+
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "foo@def.service", &state) >= 0 && state == UNIT_FILE_DISABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "foo@bar0.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "foo@bar1.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+ assert_se(unit_file_get_state(RUNTIME_SCOPE_SYSTEM, root, "foo@bartest.service", &state) >= 0 && state == UNIT_FILE_ENABLED);
+
+ install_changes_free(changes, n_changes);
+}
+
+static void verify_one(
+ const InstallInfo *i,
+ const char *alias,
+ int expected,
+ const char *updated_name) {
+ int r;
+ static const InstallInfo *last_info = NULL;
+ _cleanup_free_ char *alias2 = NULL;
+
+ if (i != last_info)
+ log_info("-- %s --", (last_info = i)->name);
+
+ r = unit_file_verify_alias(i, alias, &alias2, NULL, NULL);
+ log_info_errno(r, "alias %s ← %s: %d/%m (expected %d)%s%s%s",
+ i->name, alias, r, expected,
+ alias2 ? " [" : "", strempty(alias2),
+ alias2 ? "]" : "");
+ assert_se(r == expected);
+
+ /* This is test for "instance propagation". This propagation matters mostly for WantedBy= and
+ * RequiredBy= settings, and less so for Alias=. The only case where it should happen is when we have
+ * an Alias=alias@.service an instantiated template template@instance. In that case the instance name
+ * should be propagated into the alias as alias@instance. */
+ assert_se(streq_ptr(alias2, updated_name));
+}
+
+TEST(verify_alias) {
+ const InstallInfo
+ plain_service = { .name = (char*) "plain.service" },
+ bare_template = { .name = (char*) "template1@.service" },
+ di_template = { .name = (char*) "template2@.service",
+ .default_instance = (char*) "di" },
+ inst_template = { .name = (char*) "template3@inst.service" },
+ di_inst_template = { .name = (char*) "template4@inst.service",
+ .default_instance = (char*) "di" };
+
+ verify_one(&plain_service, "alias.service", 0, NULL);
+ verify_one(&plain_service, "alias.socket", -EXDEV, NULL);
+ verify_one(&plain_service, "alias@.service", -EXDEV, NULL);
+ verify_one(&plain_service, "alias@inst.service", -EXDEV, NULL);
+
+ /* Setting WantedBy= and RequiredBy= through Alias= is supported for the sake of backwards
+ * compatibility. */
+ verify_one(&plain_service, "foo.target.wants/plain.service", 0, NULL);
+ verify_one(&plain_service, "foo.target.wants/plain.socket", -EXDEV, NULL);
+ verify_one(&plain_service, "foo.target.wants/plain@.service", -EXDEV, NULL);
+ verify_one(&plain_service, "foo.target.wants/service", -EXDEV, NULL);
+ verify_one(&plain_service, "foo.target.requires/plain.service", 0, NULL);
+ verify_one(&plain_service, "foo.target.requires/plain.socket", -EXDEV, NULL);
+ verify_one(&plain_service, "foo.target.requires/plain@.service", -EXDEV, NULL);
+ verify_one(&plain_service, "foo.target.requires/service", -EXDEV, NULL);
+ verify_one(&plain_service, "asdf.requires/plain.service", -EXDEV, NULL); /* invalid unit name component */
+ /* The newly-added UpheldBy= (.upholds/) and other suffixes should be rejected */
+ verify_one(&plain_service, "foo.target.upholds/plain.service", -EXDEV, NULL);
+ verify_one(&plain_service, "foo.target.upholds/plain.socket", -EXDEV, NULL);
+ verify_one(&plain_service, "foo.target.upholds/plain@.service", -EXDEV, NULL);
+ verify_one(&plain_service, "foo.target.upholds/service", -EXDEV, NULL);
+ verify_one(&plain_service, "foo.service/plain.service", -EXDEV, NULL); /* missing dir suffix */
+ verify_one(&plain_service, "foo.target.conf/plain.service", -EXDEV, NULL);
+
+ verify_one(&bare_template, "alias.service", -EXDEV, NULL);
+ verify_one(&bare_template, "alias.socket", -EXDEV, NULL);
+ verify_one(&bare_template, "alias@.socket", -EXDEV, NULL);
+ verify_one(&bare_template, "alias@inst.socket", -EXDEV, NULL);
+ /* A general alias alias@.service → template1@.service. */
+ verify_one(&bare_template, "alias@.service", 0, NULL);
+ /* Only a specific instance is aliased, see the discussion in https://github.com/systemd/systemd/pull/13119. */
+ verify_one(&bare_template, "alias@inst.service", 0, NULL);
+ verify_one(&bare_template, "foo.target.wants/plain.service", -EXDEV, NULL);
+ verify_one(&bare_template, "foo.target.wants/plain.socket", -EXDEV, NULL);
+ verify_one(&bare_template, "foo.target.wants/plain@.service", -EXDEV, NULL);
+ /* Name mismatch: we cannot allow this, because plain@foo.service would be pulled in by foo.target,
+ * but would not be resolveable on its own, since systemd doesn't know how to load the fragment. */
+ verify_one(&bare_template, "foo.target.wants/plain@foo.service", -EXDEV, NULL);
+ verify_one(&bare_template, "foo.target.wants/template1@foo.service", 0, NULL);
+ verify_one(&bare_template, "foo.target.wants/service", -EXDEV, NULL);
+ verify_one(&bare_template, "foo.target.requires/plain.service", -EXDEV, NULL);
+ verify_one(&bare_template, "foo.target.requires/plain.socket", -EXDEV, NULL);
+ verify_one(&bare_template, "foo.target.requires/plain@.service", -EXDEV, NULL); /* instance missing */
+ verify_one(&bare_template, "foo.target.requires/template1@inst.service", 0, NULL);
+ verify_one(&bare_template, "foo.target.requires/service", -EXDEV, NULL);
+ verify_one(&bare_template, "foo.target.conf/plain.service", -EXDEV, NULL);
+ verify_one(&bare_template, "FOO@.target.requires/plain@.service", -EXDEV, NULL); /* template name mismatch */
+ verify_one(&bare_template, "FOO@inst.target.requires/plain@.service", -EXDEV, NULL);
+ verify_one(&bare_template, "FOO@inst.target.requires/plain@inst.service", -EXDEV, NULL);
+ verify_one(&bare_template, "FOO@.target.requires/template1@.service", 0, NULL); /* instance propagated */
+ verify_one(&bare_template, "FOO@inst.target.requires/template1@.service", -EXDEV, NULL); /* instance missing */
+ verify_one(&bare_template, "FOO@inst.target.requires/template1@inst.service", 0, NULL); /* instance provided */
+
+ verify_one(&di_template, "alias.service", -EXDEV, NULL);
+ verify_one(&di_template, "alias.socket", -EXDEV, NULL);
+ verify_one(&di_template, "alias@.socket", -EXDEV, NULL);
+ verify_one(&di_template, "alias@inst.socket", -EXDEV, NULL);
+ verify_one(&di_template, "alias@inst.service", 0, NULL);
+ verify_one(&di_template, "alias@.service", 0, NULL);
+ verify_one(&di_template, "alias@di.service", 0, NULL);
+ verify_one(&di_template, "foo.target.wants/plain.service", -EXDEV, NULL);
+ verify_one(&di_template, "foo.target.wants/plain.socket", -EXDEV, NULL);
+ verify_one(&di_template, "foo.target.wants/plain@.service", -EXDEV, NULL);
+ verify_one(&di_template, "foo.target.wants/plain@di.service", -EXDEV, NULL);
+ verify_one(&di_template, "foo.target.wants/template2@di.service", 0, NULL);
+ verify_one(&di_template, "foo.target.wants/service", -EXDEV, NULL);
+ verify_one(&di_template, "foo.target.requires/plain.service", -EXDEV, NULL);
+ verify_one(&di_template, "foo.target.requires/plain.socket", -EXDEV, NULL);
+ verify_one(&di_template, "foo.target.requires/plain@.service", -EXDEV, NULL);
+ verify_one(&di_template, "foo.target.requires/plain@di.service", -EXDEV, NULL);
+ verify_one(&di_template, "foo.target.requires/plain@foo.service", -EXDEV, NULL);
+ verify_one(&di_template, "foo.target.requires/template2@.service", -EXDEV, NULL); /* instance missing */
+ verify_one(&di_template, "foo.target.requires/template2@di.service", 0, NULL);
+ verify_one(&di_template, "foo.target.requires/service", -EXDEV, NULL);
+ verify_one(&di_template, "foo.target.conf/plain.service", -EXDEV, NULL);
+
+ verify_one(&inst_template, "alias.service", -EXDEV, NULL);
+ verify_one(&inst_template, "alias.socket", -EXDEV, NULL);
+ verify_one(&inst_template, "alias@.socket", -EXDEV, NULL);
+ verify_one(&inst_template, "alias@inst.socket", -EXDEV, NULL);
+ verify_one(&inst_template, "alias@inst.service", 0, NULL);
+ verify_one(&inst_template, "alias@.service", 0, "alias@inst.service");
+ verify_one(&inst_template, "alias@di.service", -EXDEV, NULL);
+ verify_one(&inst_template, "bar.target.wants/plain.service", -EXDEV, NULL);
+ verify_one(&inst_template, "bar.target.wants/plain.socket", -EXDEV, NULL);
+ verify_one(&inst_template, "bar.target.wants/plain@.service", -EXDEV, NULL);
+ verify_one(&inst_template, "bar.target.wants/plain@di.service", -EXDEV, NULL);
+ verify_one(&inst_template, "bar.target.wants/plain@inst.service", -EXDEV, NULL);
+ verify_one(&inst_template, "bar.target.wants/template3@foo.service", -EXDEV, NULL);
+ verify_one(&inst_template, "bar.target.wants/template3@inst.service", 0, NULL);
+ verify_one(&inst_template, "bar.target.wants/service", -EXDEV, NULL);
+ verify_one(&inst_template, "bar.target.requires/plain.service", -EXDEV, NULL);
+ verify_one(&inst_template, "bar.target.requires/plain.socket", -EXDEV, NULL);
+ verify_one(&inst_template, "bar.target.requires/plain@.service", -EXDEV, NULL);
+ verify_one(&inst_template, "bar.target.requires/plain@di.service", -EXDEV, NULL);
+ verify_one(&inst_template, "bar.target.requires/plain@inst.service", -EXDEV, NULL);
+ verify_one(&inst_template, "bar.target.requires/template3@foo.service", -EXDEV, NULL);
+ verify_one(&inst_template, "bar.target.requires/template3@inst.service", 0, NULL);
+ verify_one(&inst_template, "bar.target.requires/service", -EXDEV, NULL);
+ verify_one(&inst_template, "bar.target.conf/plain.service", -EXDEV, NULL);
+ verify_one(&inst_template, "BAR@.target.requires/plain@.service", -EXDEV, NULL); /* template name mismatch */
+ verify_one(&inst_template, "BAR@inst.target.requires/plain@.service", -EXDEV, NULL);
+ verify_one(&inst_template, "BAR@inst.target.requires/plain@inst.service", -EXDEV, NULL);
+ verify_one(&inst_template, "BAR@.target.requires/template3@.service", -EXDEV, NULL); /* instance missing */
+ verify_one(&inst_template, "BAR@inst.target.requires/template3@.service", -EXDEV, NULL); /* instance missing */
+ verify_one(&inst_template, "BAR@inst.target.requires/template3@inst.service", 0, NULL); /* instance provided */
+ verify_one(&inst_template, "BAR@inst.target.requires/template3@ins2.service", -EXDEV, NULL); /* instance mismatch */
+
+ /* explicit alias overrides DefaultInstance */
+ verify_one(&di_inst_template, "alias.service", -EXDEV, NULL);
+ verify_one(&di_inst_template, "alias.socket", -EXDEV, NULL);
+ verify_one(&di_inst_template, "alias@.socket", -EXDEV, NULL);
+ verify_one(&di_inst_template, "alias@inst.socket", -EXDEV, NULL);
+ verify_one(&di_inst_template, "alias@inst.service", 0, NULL);
+ verify_one(&di_inst_template, "alias@.service", 0, "alias@inst.service");
+ verify_one(&di_inst_template, "alias@di.service", -EXDEV, NULL);
+ verify_one(&di_inst_template, "goo.target.wants/plain.service", -EXDEV, NULL);
+ verify_one(&di_inst_template, "goo.target.wants/plain.socket", -EXDEV, NULL);
+ verify_one(&di_inst_template, "goo.target.wants/plain@.service", -EXDEV, NULL);
+ verify_one(&di_inst_template, "goo.target.wants/plain@di.service", -EXDEV, NULL);
+ verify_one(&di_inst_template, "goo.target.wants/template4@foo.service", -EXDEV, NULL);
+ verify_one(&di_inst_template, "goo.target.wants/template4@inst.service", 0, NULL);
+ verify_one(&di_inst_template, "goo.target.wants/template4@di.service", -EXDEV, NULL);
+ verify_one(&di_inst_template, "goo.target.wants/service", -EXDEV, NULL);
+ verify_one(&di_inst_template, "goo.target.requires/plain.service", -EXDEV, NULL);
+ verify_one(&di_inst_template, "goo.target.requires/plain.socket", -EXDEV, NULL);
+ verify_one(&di_inst_template, "goo.target.requires/plain@.service", -EXDEV, NULL);
+ verify_one(&di_inst_template, "goo.target.requires/plain@di.service", -EXDEV, NULL);
+ verify_one(&di_inst_template, "goo.target.requires/plain@inst.service", -EXDEV, NULL);
+ verify_one(&di_inst_template, "goo.target.requires/template4@foo.service", -EXDEV, NULL);
+ verify_one(&di_inst_template, "goo.target.requires/template4@inst.service", 0, NULL);
+ verify_one(&di_inst_template, "goo.target.requires/service", -EXDEV, NULL);
+ verify_one(&di_inst_template, "goo.target.conf/plain.service", -EXDEV, NULL);
+}
+
+static int intro(void) {
+ const char *p;
+
+ assert_se(mkdtemp_malloc("/tmp/rootXXXXXX", &root) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system/");
+ assert_se(mkdir_p(p, 0755) >= 0);
+
+ p = strjoina(root, SYSTEM_CONFIG_UNIT_DIR"/");
+ assert_se(mkdir_p(p, 0755) >= 0);
+
+ p = strjoina(root, "/run/systemd/system/");
+ assert_se(mkdir_p(p, 0755) >= 0);
+
+ p = strjoina(root, "/opt/");
+ assert_se(mkdir_p(p, 0755) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system-preset/");
+ assert_se(mkdir_p(p, 0755) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system/multi-user.target");
+ assert_se(write_string_file(p, "# pretty much empty", WRITE_STRING_FILE_CREATE) >= 0);
+
+ p = strjoina(root, "/usr/lib/systemd/system/graphical.target");
+ assert_se(write_string_file(p, "# pretty much empty", WRITE_STRING_FILE_CREATE) >= 0);
+
+ return EXIT_SUCCESS;
+}
+
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_INFO, intro);
diff --git a/src/test/test-install.c b/src/test/test-install.c
new file mode 100644
index 0000000..b54252e
--- /dev/null
+++ b/src/test/test-install.c
@@ -0,0 +1,270 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+#include <string.h>
+
+#include "install.h"
+#include "tests.h"
+
+static void dump_changes(InstallChange *c, unsigned n) {
+ unsigned i;
+
+ assert_se(n == 0 || c);
+
+ for (i = 0; i < n; i++) {
+ if (c[i].type == INSTALL_CHANGE_UNLINK)
+ printf("rm '%s'\n", c[i].path);
+ else if (c[i].type == INSTALL_CHANGE_SYMLINK)
+ printf("ln -s '%s' '%s'\n", c[i].source, c[i].path);
+ }
+}
+
+int main(int argc, char* argv[]) {
+ _cleanup_hashmap_free_ Hashmap *h = NULL;
+ UnitFileList *p;
+ int r;
+ const char *const files[] = { "avahi-daemon.service", NULL };
+ const char *const files2[] = { "/home/lennart/test.service", NULL };
+ InstallChange *changes = NULL;
+ size_t n_changes = 0;
+ UnitFileState state = 0;
+
+ test_setup_logging(LOG_DEBUG);
+
+ h = hashmap_new(&unit_file_list_hash_ops_free);
+ r = unit_file_get_list(RUNTIME_SCOPE_SYSTEM, NULL, h, NULL, NULL);
+ assert_se(r == 0);
+
+ HASHMAP_FOREACH(p, h) {
+ UnitFileState s = _UNIT_FILE_STATE_INVALID;
+
+ r = unit_file_get_state(RUNTIME_SCOPE_SYSTEM, NULL, basename(p->path), &s);
+
+ assert_se((r < 0 && p->state == UNIT_FILE_BAD) ||
+ (p->state == s));
+
+ fprintf(stderr, "%s (%s)\n",
+ p->path,
+ unit_file_state_to_string(p->state));
+ }
+
+ log_info("/*** enable **/");
+
+ r = unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, NULL, (char**) files, &changes, &n_changes);
+ assert_se(r >= 0);
+
+ log_info("/*** enable2 **/");
+
+ r = unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, NULL, (char**) files, &changes, &n_changes);
+ assert_se(r >= 0);
+
+ dump_changes(changes, n_changes);
+ install_changes_free(changes, n_changes);
+
+ r = unit_file_get_state(RUNTIME_SCOPE_SYSTEM, NULL, files[0], &state);
+ assert_se(r >= 0);
+ assert_se(state == UNIT_FILE_ENABLED);
+
+ log_info("/*** disable ***/");
+ changes = NULL;
+ n_changes = 0;
+
+ r = unit_file_disable(RUNTIME_SCOPE_SYSTEM, 0, NULL, (char**) files, &changes, &n_changes);
+ assert_se(r >= 0);
+
+ dump_changes(changes, n_changes);
+ install_changes_free(changes, n_changes);
+
+ r = unit_file_get_state(RUNTIME_SCOPE_SYSTEM, NULL, files[0], &state);
+ assert_se(r >= 0);
+ assert_se(state == UNIT_FILE_DISABLED);
+
+ log_info("/*** mask ***/");
+ changes = NULL;
+ n_changes = 0;
+
+ r = unit_file_mask(RUNTIME_SCOPE_SYSTEM, 0, NULL, (char**) files, &changes, &n_changes);
+ assert_se(r >= 0);
+ log_info("/*** mask2 ***/");
+ r = unit_file_mask(RUNTIME_SCOPE_SYSTEM, 0, NULL, (char**) files, &changes, &n_changes);
+ assert_se(r >= 0);
+
+ dump_changes(changes, n_changes);
+ install_changes_free(changes, n_changes);
+
+ r = unit_file_get_state(RUNTIME_SCOPE_SYSTEM, NULL, files[0], &state);
+ assert_se(r >= 0);
+ assert_se(state == UNIT_FILE_MASKED);
+
+ log_info("/*** unmask ***/");
+ changes = NULL;
+ n_changes = 0;
+
+ r = unit_file_unmask(RUNTIME_SCOPE_SYSTEM, 0, NULL, (char**) files, &changes, &n_changes);
+ assert_se(r >= 0);
+ log_info("/*** unmask2 ***/");
+ r = unit_file_unmask(RUNTIME_SCOPE_SYSTEM, 0, NULL, (char**) files, &changes, &n_changes);
+ assert_se(r >= 0);
+
+ dump_changes(changes, n_changes);
+ install_changes_free(changes, n_changes);
+
+ r = unit_file_get_state(RUNTIME_SCOPE_SYSTEM, NULL, files[0], &state);
+ assert_se(r >= 0);
+ assert_se(state == UNIT_FILE_DISABLED);
+
+ log_info("/*** mask ***/");
+ changes = NULL;
+ n_changes = 0;
+
+ r = unit_file_mask(RUNTIME_SCOPE_SYSTEM, 0, NULL, (char**) files, &changes, &n_changes);
+ assert_se(r >= 0);
+
+ dump_changes(changes, n_changes);
+ install_changes_free(changes, n_changes);
+
+ r = unit_file_get_state(RUNTIME_SCOPE_SYSTEM, NULL, files[0], &state);
+ assert_se(r >= 0);
+ assert_se(state == UNIT_FILE_MASKED);
+
+ log_info("/*** disable ***/");
+ changes = NULL;
+ n_changes = 0;
+
+ r = unit_file_disable(RUNTIME_SCOPE_SYSTEM, 0, NULL, (char**) files, &changes, &n_changes);
+ assert_se(r >= 0);
+ log_info("/*** disable2 ***/");
+ r = unit_file_disable(RUNTIME_SCOPE_SYSTEM, 0, NULL, (char**) files, &changes, &n_changes);
+ assert_se(r >= 0);
+
+ dump_changes(changes, n_changes);
+ install_changes_free(changes, n_changes);
+
+ r = unit_file_get_state(RUNTIME_SCOPE_SYSTEM, NULL, files[0], &state);
+ assert_se(r >= 0);
+ assert_se(state == UNIT_FILE_MASKED);
+
+ log_info("/*** umask ***/");
+ changes = NULL;
+ n_changes = 0;
+
+ r = unit_file_unmask(RUNTIME_SCOPE_SYSTEM, 0, NULL, (char**) files, &changes, &n_changes);
+ assert_se(r >= 0);
+
+ dump_changes(changes, n_changes);
+ install_changes_free(changes, n_changes);
+
+ r = unit_file_get_state(RUNTIME_SCOPE_SYSTEM, NULL, files[0], &state);
+ assert_se(r >= 0);
+ assert_se(state == UNIT_FILE_DISABLED);
+
+ log_info("/*** enable files2 ***/");
+ changes = NULL;
+ n_changes = 0;
+
+ r = unit_file_enable(RUNTIME_SCOPE_SYSTEM, 0, NULL, (char**) files2, &changes, &n_changes);
+ assert_se(r >= 0);
+
+ dump_changes(changes, n_changes);
+ install_changes_free(changes, n_changes);
+
+ r = unit_file_get_state(RUNTIME_SCOPE_SYSTEM, NULL, basename(files2[0]), &state);
+ assert_se(r >= 0);
+ assert_se(state == UNIT_FILE_ENABLED);
+
+ log_info("/*** disable files2 ***/");
+ changes = NULL;
+ n_changes = 0;
+
+ r = unit_file_disable(RUNTIME_SCOPE_SYSTEM, 0, NULL, STRV_MAKE(basename(files2[0])), &changes, &n_changes);
+ assert_se(r >= 0);
+
+ dump_changes(changes, n_changes);
+ install_changes_free(changes, n_changes);
+
+ r = unit_file_get_state(RUNTIME_SCOPE_SYSTEM, NULL, basename(files2[0]), &state);
+ assert_se(r < 0);
+
+ log_info("/*** link files2 ***/");
+ changes = NULL;
+ n_changes = 0;
+
+ r = unit_file_link(RUNTIME_SCOPE_SYSTEM, 0, NULL, (char**) files2, &changes, &n_changes);
+ assert_se(r >= 0);
+
+ dump_changes(changes, n_changes);
+ install_changes_free(changes, n_changes);
+
+ r = unit_file_get_state(RUNTIME_SCOPE_SYSTEM, NULL, basename(files2[0]), &state);
+ assert_se(r >= 0);
+ assert_se(state == UNIT_FILE_LINKED);
+
+ log_info("/*** disable files2 ***/");
+ changes = NULL;
+ n_changes = 0;
+
+ r = unit_file_disable(RUNTIME_SCOPE_SYSTEM, 0, NULL, STRV_MAKE(basename(files2[0])), &changes, &n_changes);
+ assert_se(r >= 0);
+
+ dump_changes(changes, n_changes);
+ install_changes_free(changes, n_changes);
+
+ r = unit_file_get_state(RUNTIME_SCOPE_SYSTEM, NULL, basename(files2[0]), &state);
+ assert_se(r < 0);
+
+ log_info("/*** link files2 ***/");
+ changes = NULL;
+ n_changes = 0;
+
+ r = unit_file_link(RUNTIME_SCOPE_SYSTEM, 0, NULL, (char**) files2, &changes, &n_changes);
+ assert_se(r >= 0);
+
+ dump_changes(changes, n_changes);
+ install_changes_free(changes, n_changes);
+
+ r = unit_file_get_state(RUNTIME_SCOPE_SYSTEM, NULL, basename(files2[0]), &state);
+ assert_se(r >= 0);
+ assert_se(state == UNIT_FILE_LINKED);
+
+ log_info("/*** reenable files2 ***/");
+ changes = NULL;
+ n_changes = 0;
+
+ r = unit_file_reenable(RUNTIME_SCOPE_SYSTEM, 0, NULL, (char**) files2, &changes, &n_changes);
+ assert_se(r >= 0);
+
+ dump_changes(changes, n_changes);
+ install_changes_free(changes, n_changes);
+
+ r = unit_file_get_state(RUNTIME_SCOPE_SYSTEM, NULL, basename(files2[0]), &state);
+ assert_se(r >= 0);
+ assert_se(state == UNIT_FILE_ENABLED);
+
+ log_info("/*** disable files2 ***/");
+ changes = NULL;
+ n_changes = 0;
+
+ r = unit_file_disable(RUNTIME_SCOPE_SYSTEM, 0, NULL, STRV_MAKE(basename(files2[0])), &changes, &n_changes);
+ assert_se(r >= 0);
+
+ dump_changes(changes, n_changes);
+ install_changes_free(changes, n_changes);
+
+ r = unit_file_get_state(RUNTIME_SCOPE_SYSTEM, NULL, basename(files2[0]), &state);
+ assert_se(r < 0);
+ log_info("/*** preset files ***/");
+ changes = NULL;
+ n_changes = 0;
+
+ r = unit_file_preset(RUNTIME_SCOPE_SYSTEM, 0, NULL, (char**) files, UNIT_FILE_PRESET_FULL, &changes, &n_changes);
+ assert_se(r >= 0);
+
+ dump_changes(changes, n_changes);
+ install_changes_free(changes, n_changes);
+
+ r = unit_file_get_state(RUNTIME_SCOPE_SYSTEM, NULL, basename(files[0]), &state);
+ assert_se(r >= 0);
+ assert_se(state == UNIT_FILE_ENABLED);
+
+ return 0;
+}
diff --git a/src/test/test-io-util.c b/src/test/test-io-util.c
new file mode 100644
index 0000000..fa3ef86
--- /dev/null
+++ b/src/test/test-io-util.c
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "fd-util.h"
+#include "io-util.h"
+#include "macro.h"
+#include "tests.h"
+
+static void test_sparse_write_one(int fd, const char *buffer, size_t n) {
+ char check[n];
+
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(ftruncate(fd, 0) >= 0);
+ assert_se(sparse_write(fd, buffer, n, 4) == (ssize_t) n);
+
+ assert_se(lseek(fd, 0, SEEK_CUR) == (off_t) n);
+ assert_se(ftruncate(fd, n) >= 0);
+
+ assert_se(lseek(fd, 0, SEEK_SET) == 0);
+ assert_se(read(fd, check, n) == (ssize_t) n);
+
+ assert_se(memcmp(buffer, check, n) == 0);
+}
+
+TEST(sparse_write) {
+ const char test_a[] = "test";
+ const char test_b[] = "\0\0\0\0test\0\0\0\0";
+ const char test_c[] = "\0\0test\0\0\0\0";
+ const char test_d[] = "\0\0test\0\0\0test\0\0\0\0test\0\0\0\0\0test\0\0\0test\0\0\0\0test\0\0\0\0\0\0\0\0";
+ const char test_e[] = "test\0\0\0\0test";
+ _cleanup_close_ int fd = -EBADF;
+ char fn[] = "/tmp/sparseXXXXXX";
+
+ fd = mkostemp(fn, O_CLOEXEC);
+ assert_se(fd >= 0);
+ (void) unlink(fn);
+
+ test_sparse_write_one(fd, test_a, sizeof(test_a));
+ test_sparse_write_one(fd, test_b, sizeof(test_b));
+ test_sparse_write_one(fd, test_c, sizeof(test_c));
+ test_sparse_write_one(fd, test_d, sizeof(test_d));
+ test_sparse_write_one(fd, test_e, sizeof(test_e));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-ip-protocol-list.c b/src/test/test-ip-protocol-list.c
new file mode 100644
index 0000000..dfff015
--- /dev/null
+++ b/src/test/test-ip-protocol-list.c
@@ -0,0 +1,71 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+
+#include "macro.h"
+#include "ip-protocol-list.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "tests.h"
+
+static void test_int(int i) {
+ char str[DECIMAL_STR_MAX(int)];
+
+ assert_se(ip_protocol_from_name(ip_protocol_to_name(i)) == i);
+
+ xsprintf(str, "%i", i);
+ assert_se(ip_protocol_from_name(ip_protocol_to_name(parse_ip_protocol(str))) == i);
+}
+
+static void test_int_fail(int i, int error) {
+ char str[DECIMAL_STR_MAX(int)];
+
+ assert_se(!ip_protocol_to_name(i));
+
+ xsprintf(str, "%i", i);
+ assert_se(parse_ip_protocol(str) == error);
+}
+
+static void test_str(const char *s) {
+ assert_se(streq(ip_protocol_to_name(ip_protocol_from_name(s)), s));
+ assert_se(streq(ip_protocol_to_name(parse_ip_protocol(s)), s));
+}
+
+static void test_str_fail(const char *s, int error) {
+ assert_se(ip_protocol_from_name(s) == -EINVAL);
+ assert_se(parse_ip_protocol(s) == error);
+}
+
+TEST(integer) {
+ test_int(IPPROTO_TCP);
+ test_int(IPPROTO_DCCP);
+ test_int_fail(-1, -ERANGE);
+ test_int_fail(1024 * 1024, -EPROTONOSUPPORT);
+}
+
+TEST(string) {
+ test_str("sctp");
+ test_str("udp");
+ test_str_fail("hoge", -EINVAL);
+ test_str_fail("-1", -ERANGE);
+ test_str_fail("1000000000", -EPROTONOSUPPORT);
+}
+
+TEST(parse_ip_protocol) {
+ assert_se(parse_ip_protocol("sctp") == IPPROTO_SCTP);
+ assert_se(parse_ip_protocol("ScTp") == IPPROTO_SCTP);
+ assert_se(parse_ip_protocol("ip") == IPPROTO_IP);
+ assert_se(parse_ip_protocol("") == IPPROTO_IP);
+ assert_se(parse_ip_protocol("1") == 1);
+ assert_se(parse_ip_protocol("0") == 0);
+ assert_se(parse_ip_protocol("-10") == -ERANGE);
+ assert_se(parse_ip_protocol("100000000") == -EPROTONOSUPPORT);
+}
+
+TEST(parse_ip_protocol_full) {
+ assert_se(parse_ip_protocol_full("-1", true) == -ERANGE);
+ assert_se(parse_ip_protocol_full("0", true) == 0);
+ assert_se(parse_ip_protocol_full("11", true) == 11);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-ipcrm.c b/src/test/test-ipcrm.c
new file mode 100644
index 0000000..238f0bf
--- /dev/null
+++ b/src/test/test-ipcrm.c
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "clean-ipc.h"
+#include "errno-util.h"
+#include "main-func.h"
+#include "tests.h"
+#include "user-util.h"
+
+static int run(int argc, char *argv[]) {
+ uid_t uid;
+ int r;
+ const char* name = argv[1] ?: NOBODY_USER_NAME;
+
+ test_setup_logging(LOG_INFO);
+
+ r = get_user_creds(&name, &uid, NULL, NULL, NULL, 0);
+ if (r == -ESRCH)
+ return log_tests_skipped("Failed to resolve user");
+ if (r < 0)
+ return log_error_errno(r, "Failed to resolve \"%s\": %m", name);
+
+ r = clean_ipc_by_uid(uid);
+ if (ERRNO_IS_PRIVILEGE(r))
+ return log_tests_skipped("No privileges");
+
+ return r;
+}
+
+DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);
diff --git a/src/test/test-job-type.c b/src/test/test-job-type.c
new file mode 100644
index 0000000..519fff7
--- /dev/null
+++ b/src/test/test-job-type.c
@@ -0,0 +1,81 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+
+#include "service.h"
+#include "tests.h"
+#include "unit.h"
+
+int main(int argc, char *argv[]) {
+ const ServiceState test_states[] = { SERVICE_DEAD, SERVICE_RUNNING };
+
+ test_setup_logging(LOG_DEBUG);
+
+ for (size_t i = 0; i < ELEMENTSOF(test_states); i++) {
+ /* fake a unit */
+ Service s = {
+ .meta.load_state = UNIT_LOADED,
+ .type = SERVICE_SIMPLE,
+ .state = test_states[i],
+ };
+ Unit *u = UNIT(&s);
+
+ printf("\nWith collapsing for service state %s\n"
+ "=========================================\n", service_state_to_string(s.state));
+ for (JobType a = 0; a < _JOB_TYPE_MAX_MERGING; a++) {
+ for (JobType b = 0; b < _JOB_TYPE_MAX_MERGING; b++) {
+
+ JobType ab = a;
+ bool merged_ab = job_type_merge_and_collapse(&ab, b, u) >= 0;
+
+ if (!job_type_is_mergeable(a, b)) {
+ assert_se(!merged_ab);
+ printf("Not mergeable: %s + %s\n", job_type_to_string(a), job_type_to_string(b));
+ continue;
+ }
+
+ assert_se(merged_ab);
+ printf("%s + %s = %s\n", job_type_to_string(a), job_type_to_string(b), job_type_to_string(ab));
+
+ for (JobType c = 0; c < _JOB_TYPE_MAX_MERGING; c++) {
+
+ /* Verify transitivity of mergeability of job types */
+ assert_se(!job_type_is_mergeable(a, b) ||
+ !job_type_is_mergeable(b, c) ||
+ job_type_is_mergeable(a, c));
+
+ /* Verify that merged entries can be merged with the same entries
+ * they can be merged with separately */
+ assert_se(!job_type_is_mergeable(a, c) || job_type_is_mergeable(ab, c));
+ assert_se(!job_type_is_mergeable(b, c) || job_type_is_mergeable(ab, c));
+
+ /* Verify that if a merged with b is not mergeable with c, then
+ * either a or b is not mergeable with c either. */
+ assert_se(job_type_is_mergeable(ab, c) || !job_type_is_mergeable(a, c) || !job_type_is_mergeable(b, c));
+
+ JobType bc = b;
+ if (job_type_merge_and_collapse(&bc, c, u) >= 0) {
+
+ /* Verify associativity */
+
+ JobType ab_c = ab;
+ assert_se(job_type_merge_and_collapse(&ab_c, c, u) == 0);
+
+ JobType bc_a = bc;
+ assert_se(job_type_merge_and_collapse(&bc_a, a, u) == 0);
+
+ JobType a_bc = a;
+ assert_se(job_type_merge_and_collapse(&a_bc, bc, u) == 0);
+
+ assert_se(ab_c == bc_a);
+ assert_se(ab_c == a_bc);
+
+ printf("%s + %s + %s = %s\n", job_type_to_string(a), job_type_to_string(b), job_type_to_string(c), job_type_to_string(ab_c));
+ }
+ }
+ }
+ }
+ }
+
+ return 0;
+}
diff --git a/src/test/test-journal-importer.c b/src/test/test-journal-importer.c
new file mode 100644
index 0000000..ec9e49e
--- /dev/null
+++ b/src/test/test-journal-importer.c
@@ -0,0 +1,71 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include "alloc-util.h"
+#include "log.h"
+#include "journal-importer.h"
+#include "path-util.h"
+#include "string-util.h"
+#include "tests.h"
+
+static void assert_iovec_entry(const struct iovec *iovec, const char* content) {
+ assert_se(strlen(content) == iovec->iov_len);
+ assert_se(memcmp(content, iovec->iov_base, iovec->iov_len) == 0);
+}
+
+#define COREDUMP_PROC_GROUP \
+ "COREDUMP_PROC_CGROUP=1:name=systemd:/\n" \
+ "0::/user.slice/user-1002.slice/user@1002.service/gnome-terminal-server.service\n"
+
+TEST(basic_parsing) {
+ _cleanup_(journal_importer_cleanup) JournalImporter imp = JOURNAL_IMPORTER_INIT(-1);
+ _cleanup_free_ char *journal_data_path = NULL;
+ int r;
+
+ assert_se(get_testdata_dir("journal-data/journal-1.txt", &journal_data_path) >= 0);
+ imp.fd = open(journal_data_path, O_RDONLY|O_CLOEXEC);
+ assert_se(imp.fd >= 0);
+
+ do
+ r = journal_importer_process_data(&imp);
+ while (r == 0 && !journal_importer_eof(&imp));
+ assert_se(r == 1);
+
+ /* We read one entry, so we should get EOF on next read, but not yet */
+ assert_se(!journal_importer_eof(&imp));
+
+ assert_se(imp.iovw.count == 6);
+ assert_iovec_entry(&imp.iovw.iovec[0], "_BOOT_ID=1531fd22ec84429e85ae888b12fadb91");
+ assert_iovec_entry(&imp.iovw.iovec[1], "_TRANSPORT=journal");
+ assert_iovec_entry(&imp.iovw.iovec[2], COREDUMP_PROC_GROUP);
+ assert_iovec_entry(&imp.iovw.iovec[3], "COREDUMP_RLIMIT=-1");
+ assert_iovec_entry(&imp.iovw.iovec[4], COREDUMP_PROC_GROUP);
+ assert_iovec_entry(&imp.iovw.iovec[5], "_SOURCE_REALTIME_TIMESTAMP=1478389147837945");
+
+ /* Let's check if we get EOF now */
+ r = journal_importer_process_data(&imp);
+ assert_se(r == 0);
+ assert_se(journal_importer_eof(&imp));
+}
+
+TEST(bad_input) {
+ _cleanup_(journal_importer_cleanup) JournalImporter imp = JOURNAL_IMPORTER_INIT(-1);
+ _cleanup_free_ char *journal_data_path = NULL;
+ int r;
+
+ assert_se(get_testdata_dir("journal-data/journal-1.txt", &journal_data_path) >= 0);
+ imp.fd = open(journal_data_path, O_RDONLY|O_CLOEXEC);
+ assert_se(imp.fd >= 0);
+
+ do
+ r = journal_importer_process_data(&imp);
+ while (!journal_importer_eof(&imp));
+ assert_se(r == 0); /* If we don't have enough input, 0 is returned */
+
+ assert_se(journal_importer_eof(&imp));
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-json.c b/src/test/test-json.c
new file mode 100644
index 0000000..c120a70
--- /dev/null
+++ b/src/test/test-json.c
@@ -0,0 +1,816 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <float.h>
+
+#include "alloc-util.h"
+#include "escape.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "json-internal.h"
+#include "json.h"
+#include "math-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+
+static void test_tokenizer_one(const char *data, ...) {
+ unsigned line = 0, column = 0;
+ void *state = NULL;
+ va_list ap;
+
+ _cleanup_free_ char *cdata = NULL;
+ assert_se(cdata = cescape(data));
+ log_info("/* %s data=\"%s\" */", __func__, cdata);
+
+ va_start(ap, data);
+
+ for (;;) {
+ unsigned token_line, token_column;
+ _cleanup_free_ char *str = NULL;
+ JsonValue v = JSON_VALUE_NULL;
+ int t, tt;
+
+ t = json_tokenize(&data, &str, &v, &token_line, &token_column, &state, &line, &column);
+ tt = va_arg(ap, int);
+
+ assert_se(t == tt);
+
+ if (t == JSON_TOKEN_END || t < 0)
+ break;
+
+ else if (t == JSON_TOKEN_STRING) {
+ const char *nn;
+
+ nn = va_arg(ap, const char *);
+ assert_se(streq_ptr(nn, str));
+
+ } else if (t == JSON_TOKEN_REAL) {
+ double d;
+
+ d = va_arg(ap, double);
+
+ assert_se(fabs(d - v.real) < 1e-10 ||
+ fabs((d - v.real) / v.real) < 1e-10);
+
+ } else if (t == JSON_TOKEN_INTEGER) {
+ int64_t i;
+
+ i = va_arg(ap, int64_t);
+ assert_se(i == v.integer);
+
+ } else if (t == JSON_TOKEN_UNSIGNED) {
+ uint64_t u;
+
+ u = va_arg(ap, uint64_t);
+ assert_se(u == v.unsig);
+
+ } else if (t == JSON_TOKEN_BOOLEAN) {
+ bool b;
+
+ b = va_arg(ap, int);
+ assert_se(b == v.boolean);
+ }
+ }
+
+ va_end(ap);
+}
+
+typedef void (*Test)(JsonVariant *);
+
+static void test_variant_one(const char *data, Test test) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL;
+ _cleanup_free_ char *s = NULL;
+ int r;
+
+ _cleanup_free_ char *cdata;
+ assert_se(cdata = cescape(data));
+ log_info("/* %s data=\"%s\" */", __func__, cdata);
+
+ r = json_parse(data, 0, &v, NULL, NULL);
+ assert_se(r == 0);
+ assert_se(v);
+
+ r = json_variant_format(v, 0, &s);
+ assert_se(r >= 0);
+ assert_se(s);
+ assert_se((size_t) r == strlen(s));
+
+ log_info("formatted normally: %s", s);
+
+ r = json_parse(data, JSON_PARSE_SENSITIVE, &w, NULL, NULL);
+ assert_se(r == 0);
+ assert_se(w);
+ assert_se(json_variant_has_type(v, json_variant_type(w)));
+ assert_se(json_variant_has_type(w, json_variant_type(v)));
+ assert_se(json_variant_equal(v, w));
+
+ s = mfree(s);
+ w = json_variant_unref(w);
+
+ r = json_variant_format(v, JSON_FORMAT_PRETTY, &s);
+ assert_se(r >= 0);
+ assert_se(s);
+ assert_se((size_t) r == strlen(s));
+
+ log_info("formatted prettily:\n%s", s);
+
+ r = json_parse(data, 0, &w, NULL, NULL);
+ assert_se(r == 0);
+ assert_se(w);
+
+ assert_se(json_variant_has_type(v, json_variant_type(w)));
+ assert_se(json_variant_has_type(w, json_variant_type(v)));
+ assert_se(json_variant_equal(v, w));
+
+ s = mfree(s);
+ r = json_variant_format(v, JSON_FORMAT_COLOR, &s);
+ assert_se(r >= 0);
+ assert_se(s);
+ assert_se((size_t) r == strlen(s));
+ printf("Normal with color: %s\n", s);
+
+ s = mfree(s);
+ r = json_variant_format(v, JSON_FORMAT_COLOR|JSON_FORMAT_PRETTY, &s);
+ assert_se(r >= 0);
+ assert_se(s);
+ assert_se((size_t) r == strlen(s));
+ printf("Pretty with color:\n%s\n", s);
+
+ if (test)
+ test(v);
+}
+
+static void test_1(JsonVariant *v) {
+ JsonVariant *p, *q;
+ unsigned i;
+
+ log_info("/* %s */", __func__);
+
+ /* 3 keys + 3 values */
+ assert_se(json_variant_elements(v) == 6);
+
+ /* has k */
+ p = json_variant_by_key(v, "k");
+ assert_se(p && json_variant_type(p) == JSON_VARIANT_STRING);
+
+ /* k equals v */
+ assert_se(streq(json_variant_string(p), "v"));
+
+ /* has foo */
+ p = json_variant_by_key(v, "foo");
+ assert_se(p && json_variant_type(p) == JSON_VARIANT_ARRAY && json_variant_elements(p) == 3);
+
+ /* check foo[0] = 1, foo[1] = 2, foo[2] = 3 */
+ for (i = 0; i < 3; ++i) {
+ q = json_variant_by_index(p, i);
+ assert_se(q && json_variant_type(q) == JSON_VARIANT_UNSIGNED && json_variant_unsigned(q) == (i+1));
+ assert_se(q && json_variant_has_type(q, JSON_VARIANT_INTEGER) && json_variant_integer(q) == (i+1));
+ }
+
+ /* has bar */
+ p = json_variant_by_key(v, "bar");
+ assert_se(p && json_variant_type(p) == JSON_VARIANT_OBJECT && json_variant_elements(p) == 2);
+
+ /* zap is null */
+ q = json_variant_by_key(p, "zap");
+ assert_se(q && json_variant_type(q) == JSON_VARIANT_NULL);
+}
+
+static void test_2(JsonVariant *v) {
+ JsonVariant *p, *q;
+
+ log_info("/* %s */", __func__);
+
+ /* 2 keys + 2 values */
+ assert_se(json_variant_elements(v) == 4);
+
+ /* has mutant */
+ p = json_variant_by_key(v, "mutant");
+ assert_se(p && json_variant_type(p) == JSON_VARIANT_ARRAY && json_variant_elements(p) == 4);
+
+ /* mutant[0] == 1 */
+ q = json_variant_by_index(p, 0);
+ assert_se(q && json_variant_type(q) == JSON_VARIANT_UNSIGNED && json_variant_unsigned(q) == 1);
+ assert_se(q && json_variant_has_type(q, JSON_VARIANT_INTEGER) && json_variant_integer(q) == 1);
+
+ /* mutant[1] == null */
+ q = json_variant_by_index(p, 1);
+ assert_se(q && json_variant_type(q) == JSON_VARIANT_NULL);
+
+ /* mutant[2] == "1" */
+ q = json_variant_by_index(p, 2);
+ assert_se(q && json_variant_type(q) == JSON_VARIANT_STRING && streq(json_variant_string(q), "1"));
+
+ /* mutant[3] == JSON_VARIANT_OBJECT */
+ q = json_variant_by_index(p, 3);
+ assert_se(q && json_variant_type(q) == JSON_VARIANT_OBJECT && json_variant_elements(q) == 2);
+
+ /* has 1 */
+ p = json_variant_by_key(q, "1");
+ assert_se(p && json_variant_type(p) == JSON_VARIANT_ARRAY && json_variant_elements(p) == 2);
+
+ /* "1"[0] == 1 */
+ q = json_variant_by_index(p, 0);
+ assert_se(q && json_variant_type(q) == JSON_VARIANT_UNSIGNED && json_variant_unsigned(q) == 1);
+ assert_se(q && json_variant_has_type(q, JSON_VARIANT_INTEGER) && json_variant_integer(q) == 1);
+
+ /* "1"[1] == "1" */
+ q = json_variant_by_index(p, 1);
+ assert_se(q && json_variant_type(q) == JSON_VARIANT_STRING && streq(json_variant_string(q), "1"));
+
+ /* has thisisaverylongproperty */
+ p = json_variant_by_key(v, "thisisaverylongproperty");
+ assert_se(p && json_variant_type(p) == JSON_VARIANT_REAL && fabs(json_variant_real(p) - 1.27) < 0.001);
+}
+
+static void test_zeroes(JsonVariant *v) {
+ /* Make sure zero is how we expect it. */
+ log_info("/* %s */", __func__);
+
+ assert_se(json_variant_elements(v) == 13);
+
+ for (size_t i = 0; i < json_variant_elements(v); i++) {
+ JsonVariant *w;
+ size_t j;
+
+ assert_se(w = json_variant_by_index(v, i));
+
+ assert_se(json_variant_integer(w) == 0);
+ assert_se(json_variant_unsigned(w) == 0U);
+
+ assert_se(iszero_safe(json_variant_real(w)));
+
+ assert_se(json_variant_is_integer(w));
+ assert_se(json_variant_is_unsigned(w));
+ assert_se(json_variant_is_real(w));
+ assert_se(json_variant_is_number(w));
+
+ assert_se(!json_variant_is_negative(w));
+
+ assert_se(IN_SET(json_variant_type(w), JSON_VARIANT_INTEGER, JSON_VARIANT_UNSIGNED, JSON_VARIANT_REAL));
+
+ for (j = 0; j < json_variant_elements(v); j++) {
+ JsonVariant *q;
+
+ assert_se(q = json_variant_by_index(v, j));
+
+ assert_se(json_variant_equal(w, q));
+ }
+ }
+}
+
+TEST(build) {
+ _cleanup_(json_variant_unrefp) JsonVariant *a = NULL, *b = NULL;
+ _cleanup_free_ char *s = NULL, *t = NULL;
+
+ assert_se(json_build(&a, JSON_BUILD_STRING("hallo")) >= 0);
+ assert_se(json_build(&b, JSON_BUILD_LITERAL(" \"hallo\" ")) >= 0);
+ assert_se(json_variant_equal(a, b));
+
+ b = json_variant_unref(b);
+
+ assert_se(json_build(&b, JSON_BUILD_VARIANT(a)) >= 0);
+ assert_se(json_variant_equal(a, b));
+
+ b = json_variant_unref(b);
+ assert_se(json_build(&b, JSON_BUILD_STRING("pief")) >= 0);
+ assert_se(!json_variant_equal(a, b));
+
+ a = json_variant_unref(a);
+ b = json_variant_unref(b);
+
+ assert_se(json_build(&a, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("one", JSON_BUILD_INTEGER(7)),
+ JSON_BUILD_PAIR("two", JSON_BUILD_REAL(2.0)),
+ JSON_BUILD_PAIR("three", JSON_BUILD_INTEGER(0)))) >= 0);
+
+ assert_se(json_build(&b, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("two", JSON_BUILD_INTEGER(2)),
+ JSON_BUILD_PAIR("three", JSON_BUILD_REAL(0)),
+ JSON_BUILD_PAIR("one", JSON_BUILD_REAL(7)))) >= 0);
+
+ assert_se(json_variant_equal(a, b));
+
+ a = json_variant_unref(a);
+ b = json_variant_unref(b);
+
+ const char* arr_1234[] = {"one", "two", "three", "four", NULL};
+ assert_se(json_build(&a, JSON_BUILD_ARRAY(JSON_BUILD_OBJECT(JSON_BUILD_PAIR("x", JSON_BUILD_BOOLEAN(true)),
+ JSON_BUILD_PAIR("y", JSON_BUILD_OBJECT(JSON_BUILD_PAIR("this", JSON_BUILD_NULL)))),
+ JSON_BUILD_VARIANT(NULL),
+ JSON_BUILD_LITERAL(NULL),
+ JSON_BUILD_STRING(NULL),
+ JSON_BUILD_NULL,
+ JSON_BUILD_INTEGER(77),
+ JSON_BUILD_ARRAY(JSON_BUILD_VARIANT(JSON_VARIANT_STRING_CONST("foobar")),
+ JSON_BUILD_VARIANT(JSON_VARIANT_STRING_CONST("zzz"))),
+ JSON_BUILD_STRV((char**) arr_1234))) >= 0);
+
+ assert_se(json_variant_format(a, 0, &s) >= 0);
+ log_info("GOT: %s", s);
+ assert_se(json_parse(s, 0, &b, NULL, NULL) >= 0);
+ assert_se(json_variant_equal(a, b));
+
+ a = json_variant_unref(a);
+ b = json_variant_unref(b);
+
+ assert_se(json_build(&a, JSON_BUILD_REAL(M_PI)) >= 0);
+
+ s = mfree(s);
+ assert_se(json_variant_format(a, 0, &s) >= 0);
+ log_info("GOT: %s", s);
+ assert_se(json_parse(s, 0, &b, NULL, NULL) >= 0);
+ assert_se(json_variant_format(b, 0, &t) >= 0);
+ log_info("GOT: %s", t);
+
+ assert_se(streq(s, t));
+
+ a = json_variant_unref(a);
+ b = json_variant_unref(b);
+
+ assert_se(json_build(&a, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("x", JSON_BUILD_STRING("y")),
+ JSON_BUILD_PAIR("z", JSON_BUILD_CONST_STRING("a")),
+ JSON_BUILD_PAIR("b", JSON_BUILD_CONST_STRING("c"))
+ )) >= 0);
+
+ assert_se(json_build(&b, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("x", JSON_BUILD_STRING("y")),
+ JSON_BUILD_PAIR_CONDITION(false, "p", JSON_BUILD_STRING("q")),
+ JSON_BUILD_PAIR_CONDITION(true, "z", JSON_BUILD_CONST_STRING("a")),
+ JSON_BUILD_PAIR_CONDITION(false, "j", JSON_BUILD_ARRAY(JSON_BUILD_STRING("k"), JSON_BUILD_CONST_STRING("u"), JSON_BUILD_CONST_STRING("i"))),
+ JSON_BUILD_PAIR("b", JSON_BUILD_CONST_STRING("c"))
+ )) >= 0);
+
+ assert_se(json_variant_equal(a, b));
+}
+
+TEST(json_parse_file_empty) {
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+
+ assert_se(fopen_unlocked("/dev/null", "re", &f) >= 0);
+ assert_se(json_parse_file(f, "waldo", 0, &v, NULL, NULL) == -ENODATA);
+ assert_se(v == NULL);
+}
+
+TEST(json_parse_file_invalid) {
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+
+ assert_se(f = fmemopen_unlocked((void*) "kookoo", 6, "r"));
+ assert_se(json_parse_file(f, "waldo", 0, &v, NULL, NULL) == -EINVAL);
+ assert_se(v == NULL);
+}
+
+TEST(source) {
+ static const char data[] =
+ "\n"
+ "\n"
+ "{\n"
+ "\"foo\" : \"bar\", \n"
+ "\"qüüx\" : [ 1, 2, 3,\n"
+ "4,\n"
+ "5 ],\n"
+ "\"miep\" : { \"hallo\" : 1 },\n"
+ "\n"
+ "\"zzzzzz\" \n"
+ ":\n"
+ "[ true, \n"
+ "false, 7.5, {} ]\n"
+ "}\n";
+
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+
+ printf("--- original begin ---\n"
+ "%s"
+ "--- original end ---\n", data);
+
+ assert_se(f = fmemopen_unlocked((void*) data, strlen(data), "r"));
+
+ assert_se(json_parse_file(f, "waldo", 0, &v, NULL, NULL) >= 0);
+
+ printf("--- non-pretty begin ---\n");
+ json_variant_dump(v, 0, stdout, NULL);
+ printf("\n--- non-pretty end ---\n");
+
+ printf("--- pretty begin ---\n");
+ json_variant_dump(v, JSON_FORMAT_PRETTY|JSON_FORMAT_COLOR|JSON_FORMAT_SOURCE, stdout, NULL);
+ printf("--- pretty end ---\n");
+}
+
+TEST(depth) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ int r;
+
+ v = JSON_VARIANT_STRING_CONST("start");
+
+ /* Let's verify that the maximum depth checks work */
+
+ for (unsigned i = 0;; i++) {
+ _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
+
+ assert_se(i <= UINT16_MAX);
+ if (i & 1)
+ r = json_variant_new_array(&w, &v, 1);
+ else
+ r = json_variant_new_object(&w, (JsonVariant*[]) { JSON_VARIANT_STRING_CONST("key"), v }, 2);
+ if (r == -ELNRNG) {
+ log_info("max depth at %u", i);
+ break;
+ }
+#if HAS_FEATURE_MEMORY_SANITIZER
+ /* msan doesn't like the stack nesting to be too deep. Let's quit early. */
+ if (i >= 128) {
+ log_info("quitting early at depth %u", i);
+ break;
+ }
+#endif
+
+ assert_se(r >= 0);
+
+ json_variant_unref(v);
+ v = TAKE_PTR(w);
+ }
+
+ json_variant_dump(v, 0, stdout, NULL);
+ fputs("\n", stdout);
+}
+
+TEST(normalize) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL;
+ _cleanup_free_ char *t = NULL;
+
+ assert_se(json_build(&v, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("b", JSON_BUILD_STRING("x")),
+ JSON_BUILD_PAIR("c", JSON_BUILD_CONST_STRING("y")),
+ JSON_BUILD_PAIR("a", JSON_BUILD_CONST_STRING("z")))) >= 0);
+
+ assert_se(!json_variant_is_sorted(v));
+ assert_se(!json_variant_is_normalized(v));
+
+ assert_se(json_variant_format(v, 0, &t) >= 0);
+ assert_se(streq(t, "{\"b\":\"x\",\"c\":\"y\",\"a\":\"z\"}"));
+ t = mfree(t);
+
+ assert_se(json_build(&w, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("bar", JSON_BUILD_STRING("zzz")),
+ JSON_BUILD_PAIR("foo", JSON_BUILD_VARIANT(v)))) >= 0);
+
+ assert_se(json_variant_is_sorted(w));
+ assert_se(!json_variant_is_normalized(w));
+
+ assert_se(json_variant_format(w, 0, &t) >= 0);
+ assert_se(streq(t, "{\"bar\":\"zzz\",\"foo\":{\"b\":\"x\",\"c\":\"y\",\"a\":\"z\"}}"));
+ t = mfree(t);
+
+ assert_se(json_variant_sort(&v) >= 0);
+ assert_se(json_variant_is_sorted(v));
+ assert_se(json_variant_is_normalized(v));
+
+ assert_se(json_variant_format(v, 0, &t) >= 0);
+ assert_se(streq(t, "{\"a\":\"z\",\"b\":\"x\",\"c\":\"y\"}"));
+ t = mfree(t);
+
+ assert_se(json_variant_normalize(&w) >= 0);
+ assert_se(json_variant_is_sorted(w));
+ assert_se(json_variant_is_normalized(w));
+
+ assert_se(json_variant_format(w, 0, &t) >= 0);
+ assert_se(streq(t, "{\"bar\":\"zzz\",\"foo\":{\"a\":\"z\",\"b\":\"x\",\"c\":\"y\"}}"));
+ t = mfree(t);
+}
+
+TEST(bisect) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+
+ /* Tests the bisection logic in json_variant_by_key() */
+
+ for (char c = 'z'; c >= 'a'; c--) {
+
+ if ((c % 3) == 0)
+ continue;
+
+ _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
+ assert_se(json_variant_new_stringn(&w, (char[4]) { '<', c, c, '>' }, 4) >= 0);
+ assert_se(json_variant_set_field(&v, (char[2]) { c, 0 }, w) >= 0);
+ }
+
+ json_variant_dump(v, JSON_FORMAT_COLOR|JSON_FORMAT_PRETTY, NULL, NULL);
+
+ assert_se(!json_variant_is_sorted(v));
+ assert_se(!json_variant_is_normalized(v));
+ assert_se(json_variant_normalize(&v) >= 0);
+ assert_se(json_variant_is_sorted(v));
+ assert_se(json_variant_is_normalized(v));
+
+ json_variant_dump(v, JSON_FORMAT_COLOR|JSON_FORMAT_PRETTY, NULL, NULL);
+
+ for (char c = 'a'; c <= 'z'; c++) {
+ JsonVariant *k;
+ const char *z;
+
+ k = json_variant_by_key(v, (char[2]) { c, 0 });
+ assert_se(!k == ((c % 3) == 0));
+
+ if (!k)
+ continue;
+
+ assert_se(json_variant_is_string(k));
+
+ z = (char[5]){ '<', c, c, '>', 0};
+ assert_se(streq(json_variant_string(k), z));
+ }
+}
+
+static void test_float_match(JsonVariant *v) {
+ const double delta = 0.0001;
+
+ assert_se(json_variant_is_array(v));
+ assert_se(json_variant_elements(v) == 11);
+ assert_se(fabs(1.0 - (DBL_MIN / json_variant_real(json_variant_by_index(v, 0)))) <= delta);
+ assert_se(fabs(1.0 - (DBL_MAX / json_variant_real(json_variant_by_index(v, 1)))) <= delta);
+ assert_se(json_variant_is_null(json_variant_by_index(v, 2))); /* nan is not supported by json → null */
+ assert_se(json_variant_is_null(json_variant_by_index(v, 3))); /* +inf is not supported by json → null */
+ assert_se(json_variant_is_null(json_variant_by_index(v, 4))); /* -inf is not supported by json → null */
+ assert_se(json_variant_is_null(json_variant_by_index(v, 5)) ||
+ fabs(1.0 - (HUGE_VAL / json_variant_real(json_variant_by_index(v, 5)))) <= delta); /* HUGE_VAL might be +inf, but might also be something else */
+ assert_se(json_variant_is_real(json_variant_by_index(v, 6)) &&
+ json_variant_is_integer(json_variant_by_index(v, 6)) &&
+ json_variant_integer(json_variant_by_index(v, 6)) == 0);
+ assert_se(json_variant_is_real(json_variant_by_index(v, 7)) &&
+ json_variant_is_integer(json_variant_by_index(v, 7)) &&
+ json_variant_integer(json_variant_by_index(v, 7)) == 10);
+ assert_se(json_variant_is_real(json_variant_by_index(v, 8)) &&
+ json_variant_is_integer(json_variant_by_index(v, 8)) &&
+ json_variant_integer(json_variant_by_index(v, 8)) == -10);
+ assert_se(json_variant_is_real(json_variant_by_index(v, 9)) &&
+ !json_variant_is_integer(json_variant_by_index(v, 9)));
+ assert_se(fabs(1.0 - (DBL_MIN / 2 / json_variant_real(json_variant_by_index(v, 9)))) <= delta);
+ assert_se(json_variant_is_real(json_variant_by_index(v, 10)) &&
+ !json_variant_is_integer(json_variant_by_index(v, 10)));
+ assert_se(fabs(1.0 - (-DBL_MIN / 2 / json_variant_real(json_variant_by_index(v, 10)))) <= delta);
+}
+
+TEST(float) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL;
+ _cleanup_free_ char *text = NULL;
+
+ assert_se(json_build(&v, JSON_BUILD_ARRAY(
+ JSON_BUILD_REAL(DBL_MIN),
+ JSON_BUILD_REAL(DBL_MAX),
+ JSON_BUILD_REAL(NAN),
+ JSON_BUILD_REAL(INFINITY),
+ JSON_BUILD_REAL(-INFINITY),
+ JSON_BUILD_REAL(HUGE_VAL),
+ JSON_BUILD_REAL(0),
+ JSON_BUILD_REAL(10),
+ JSON_BUILD_REAL(-10),
+ JSON_BUILD_REAL(DBL_MIN / 2),
+ JSON_BUILD_REAL(-DBL_MIN / 2))) >= 0);
+
+ json_variant_dump(v, JSON_FORMAT_COLOR|JSON_FORMAT_PRETTY, NULL, NULL);
+
+ test_float_match(v);
+
+ assert_se(json_variant_format(v, 0, &text) >= 0);
+ assert_se(json_parse(text, 0, &w, NULL, NULL) >= 0);
+
+ json_variant_dump(w, JSON_FORMAT_COLOR|JSON_FORMAT_PRETTY, NULL, NULL);
+
+ test_float_match(w);
+}
+
+static void test_equal_text(JsonVariant *v, const char *text) {
+ _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
+
+ assert_se(json_parse(text, 0, &w, NULL, NULL) >= 0);
+ assert_se(json_variant_equal(v, w) || (!v && json_variant_is_null(w)));
+}
+
+TEST(set_field) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+
+ test_equal_text(v, "null");
+ assert_se(json_variant_set_field(&v, "foo", NULL) >= 0);
+ test_equal_text(v, "{\"foo\" : null}");
+ assert_se(json_variant_set_field(&v, "bar", JSON_VARIANT_STRING_CONST("quux")) >= 0);
+ test_equal_text(v, "{\"foo\" : null, \"bar\" : \"quux\"}");
+ assert_se(json_variant_set_field(&v, "foo", JSON_VARIANT_STRING_CONST("quux2")) >= 0);
+ test_equal_text(v, "{\"foo\" : \"quux2\", \"bar\" : \"quux\"}");
+ assert_se(json_variant_set_field(&v, "bar", NULL) >= 0);
+ test_equal_text(v, "{\"foo\" : \"quux2\", \"bar\" : null}");
+}
+
+TEST(tokenizer) {
+ test_tokenizer_one("x", -EINVAL);
+ test_tokenizer_one("", JSON_TOKEN_END);
+ test_tokenizer_one(" ", JSON_TOKEN_END);
+ test_tokenizer_one("0", JSON_TOKEN_UNSIGNED, (uint64_t) 0, JSON_TOKEN_END);
+ test_tokenizer_one("-0", JSON_TOKEN_INTEGER, (int64_t) 0, JSON_TOKEN_END);
+ test_tokenizer_one("1234", JSON_TOKEN_UNSIGNED, (uint64_t) 1234, JSON_TOKEN_END);
+ test_tokenizer_one("-1234", JSON_TOKEN_INTEGER, (int64_t) -1234, JSON_TOKEN_END);
+ test_tokenizer_one("18446744073709551615", JSON_TOKEN_UNSIGNED, (uint64_t) UINT64_MAX, JSON_TOKEN_END);
+ test_tokenizer_one("-9223372036854775808", JSON_TOKEN_INTEGER, (int64_t) INT64_MIN, JSON_TOKEN_END);
+ test_tokenizer_one("18446744073709551616", JSON_TOKEN_REAL, (double) 18446744073709551616.0L, JSON_TOKEN_END);
+ test_tokenizer_one("-9223372036854775809", JSON_TOKEN_REAL, (double) -9223372036854775809.0L, JSON_TOKEN_END);
+ test_tokenizer_one("-1234", JSON_TOKEN_INTEGER, (int64_t) -1234, JSON_TOKEN_END);
+ test_tokenizer_one("3.141", JSON_TOKEN_REAL, (double) 3.141, JSON_TOKEN_END);
+ test_tokenizer_one("0.0", JSON_TOKEN_REAL, (double) 0.0, JSON_TOKEN_END);
+ test_tokenizer_one("7e3", JSON_TOKEN_REAL, (double) 7e3, JSON_TOKEN_END);
+ test_tokenizer_one("-7e-3", JSON_TOKEN_REAL, (double) -7e-3, JSON_TOKEN_END);
+ test_tokenizer_one("true", JSON_TOKEN_BOOLEAN, true, JSON_TOKEN_END);
+ test_tokenizer_one("false", JSON_TOKEN_BOOLEAN, false, JSON_TOKEN_END);
+ test_tokenizer_one("null", JSON_TOKEN_NULL, JSON_TOKEN_END);
+ test_tokenizer_one("{}", JSON_TOKEN_OBJECT_OPEN, JSON_TOKEN_OBJECT_CLOSE, JSON_TOKEN_END);
+ test_tokenizer_one("\t {\n} \n", JSON_TOKEN_OBJECT_OPEN, JSON_TOKEN_OBJECT_CLOSE, JSON_TOKEN_END);
+ test_tokenizer_one("[]", JSON_TOKEN_ARRAY_OPEN, JSON_TOKEN_ARRAY_CLOSE, JSON_TOKEN_END);
+ test_tokenizer_one("\t [] \n\n", JSON_TOKEN_ARRAY_OPEN, JSON_TOKEN_ARRAY_CLOSE, JSON_TOKEN_END);
+ test_tokenizer_one("\"\"", JSON_TOKEN_STRING, "", JSON_TOKEN_END);
+ test_tokenizer_one("\"foo\"", JSON_TOKEN_STRING, "foo", JSON_TOKEN_END);
+ test_tokenizer_one("\"foo\\nfoo\"", JSON_TOKEN_STRING, "foo\nfoo", JSON_TOKEN_END);
+ test_tokenizer_one("{\"foo\" : \"bar\"}", JSON_TOKEN_OBJECT_OPEN, JSON_TOKEN_STRING, "foo", JSON_TOKEN_COLON, JSON_TOKEN_STRING, "bar", JSON_TOKEN_OBJECT_CLOSE, JSON_TOKEN_END);
+ test_tokenizer_one("{\"foo\" : [true, false]}", JSON_TOKEN_OBJECT_OPEN, JSON_TOKEN_STRING, "foo", JSON_TOKEN_COLON, JSON_TOKEN_ARRAY_OPEN, JSON_TOKEN_BOOLEAN, true, JSON_TOKEN_COMMA, JSON_TOKEN_BOOLEAN, false, JSON_TOKEN_ARRAY_CLOSE, JSON_TOKEN_OBJECT_CLOSE, JSON_TOKEN_END);
+ test_tokenizer_one("\"\xef\xbf\xbd\"", JSON_TOKEN_STRING, "\xef\xbf\xbd", JSON_TOKEN_END);
+ test_tokenizer_one("\"\\ufffd\"", JSON_TOKEN_STRING, "\xef\xbf\xbd", JSON_TOKEN_END);
+ test_tokenizer_one("\"\\uf\"", -EINVAL);
+ test_tokenizer_one("\"\\ud800a\"", -EINVAL);
+ test_tokenizer_one("\"\\udc00\\udc00\"", -EINVAL);
+ test_tokenizer_one("\"\\ud801\\udc37\"", JSON_TOKEN_STRING, "\xf0\x90\x90\xb7", JSON_TOKEN_END);
+
+ test_tokenizer_one("[1, 2, -3]", JSON_TOKEN_ARRAY_OPEN, JSON_TOKEN_UNSIGNED, (uint64_t) 1, JSON_TOKEN_COMMA, JSON_TOKEN_UNSIGNED, (uint64_t) 2, JSON_TOKEN_COMMA, JSON_TOKEN_INTEGER, (int64_t) -3, JSON_TOKEN_ARRAY_CLOSE, JSON_TOKEN_END);
+}
+
+TEST(variant) {
+ test_variant_one("{\"k\": \"v\", \"foo\": [1, 2, 3], \"bar\": {\"zap\": null}}", test_1);
+ test_variant_one("{\"mutant\": [1, null, \"1\", {\"1\": [1, \"1\"]}], \"thisisaverylongproperty\": 1.27}", test_2);
+ test_variant_one("{\"foo\" : \"\\u0935\\u093f\\u0935\\u0947\\u0915\\u0916\\u094d\\u092f\\u093e\\u0924\\u093f\\u0930\\u0935\\u093f\\u092a\\u094d\\u0932\\u0935\\u093e\\u0020\\u0939\\u093e\\u0928\\u094b\\u092a\\u093e\\u092f\\u0903\\u0964\"}", NULL);
+
+ test_variant_one("[ 0, -0, 0.0, -0.0, 0.000, -0.000, 0e0, -0e0, 0e+0, -0e-0, 0e-0, -0e000, 0e+000 ]", test_zeroes);
+}
+
+TEST(json_variant_merge_objectb) {
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL;
+
+ assert_se(json_build(&v, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("b", JSON_BUILD_STRING("x")),
+ JSON_BUILD_PAIR("c", JSON_BUILD_CONST_STRING("y")),
+ JSON_BUILD_PAIR("a", JSON_BUILD_CONST_STRING("z")))) >= 0);
+
+ assert_se(json_variant_merge_objectb(&w, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("b", JSON_BUILD_STRING("x")))) >= 0);
+ assert_se(json_variant_merge_objectb(&w, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("c", JSON_BUILD_STRING("y")))) >= 0);
+ assert_se(json_variant_merge_objectb(&w, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("a", JSON_BUILD_STRING("z")))) >= 0);
+
+ assert_se(json_variant_equal(v, w));
+}
+
+static void json_array_append_with_source_one(bool source) {
+ _cleanup_(json_variant_unrefp) JsonVariant *a, *b;
+
+ /* Parse two sources, each with a different name and line/column numbers */
+
+ assert_se(json_parse_with_source(" [41]", source ? "string 1" : NULL, 0,
+ &a, NULL, NULL) >= 0);
+ assert_se(json_parse_with_source("\n\n [42]", source ? "string 2" : NULL, 0,
+ &b, NULL, NULL) >= 0);
+
+ assert_se(json_variant_is_array(a));
+ assert_se(json_variant_elements(a) == 1);
+ assert_se(json_variant_is_array(b));
+ assert_se(json_variant_elements(b) == 1);
+
+ /* Verify source information */
+
+ const char *s1, *s2;
+ unsigned line1, col1, line2, col2;
+ assert_se(json_variant_get_source(a, &s1, &line1, &col1) >= 0);
+ assert_se(json_variant_get_source(b, &s2, &line2, &col2) >= 0);
+
+ assert_se(streq_ptr(s1, source ? "string 1" : NULL));
+ assert_se(streq_ptr(s2, source ? "string 2" : NULL));
+ assert_se(line1 == 1);
+ assert_se(col1 == 2);
+ assert_se(line2 == 3);
+ assert_se(col2 == 4);
+
+ /* Append one elem from the second array (and source) to the first. */
+
+ JsonVariant *elem;
+ assert_se(elem = json_variant_by_index(b, 0));
+ assert_se(json_variant_is_integer(elem));
+ assert_se(json_variant_elements(elem) == 0);
+
+ assert_se(json_variant_append_array(&a, elem) >= 0);
+
+ assert_se(json_variant_is_array(a));
+ assert_se(json_variant_elements(a) == 2);
+
+ /* Verify that source information was propagated correctly */
+
+ assert_se(json_variant_get_source(elem, &s1, &line1, &col1) >= 0);
+ assert_se(elem = json_variant_by_index(a, 1));
+ assert_se(json_variant_get_source(elem, &s2, &line2, &col2) >= 0);
+
+ assert_se(streq_ptr(s1, source ? "string 2" : NULL));
+ assert_se(streq_ptr(s2, source ? "string 2" : NULL));
+ assert_se(line1 == 3);
+ assert_se(col1 == 5);
+ assert_se(line2 == 3);
+ assert_se(col2 == 5);
+}
+
+TEST(json_array_append_with_source) {
+ json_array_append_with_source_one(true);
+}
+
+TEST(json_array_append_without_source) {
+ json_array_append_with_source_one(false);
+}
+
+TEST(json_array_append_nodup) {
+ _cleanup_(json_variant_unrefp) JsonVariant *l = NULL, *s = NULL, *wd = NULL, *nd = NULL;
+
+ assert_se(json_build(&l, JSON_BUILD_STRV(STRV_MAKE("foo", "bar", "baz", "bar", "baz", "foo", "qux", "baz"))) >= 0);
+ assert_se(json_build(&s, JSON_BUILD_STRV(STRV_MAKE("foo", "bar", "baz", "qux"))) >= 0);
+
+ assert_se(!json_variant_equal(l, s));
+ assert_se(json_variant_elements(l) == 8);
+ assert_se(json_variant_elements(s) == 4);
+
+ JsonVariant *i;
+ JSON_VARIANT_ARRAY_FOREACH(i, l) {
+ assert_se(json_variant_append_array(&wd, i) >= 0);
+ assert_se(json_variant_append_array_nodup(&nd, i) >= 0);
+ }
+
+ assert_se(json_variant_elements(wd) == 8);
+ assert_se(json_variant_equal(l, wd));
+ assert_se(!json_variant_equal(s, wd));
+
+ assert_se(json_variant_elements(nd) == 4);
+ assert_se(!json_variant_equal(l, nd));
+ assert_se(json_variant_equal(s, nd));
+}
+
+TEST(json_dispatch) {
+ struct foobar {
+ uint64_t a, b;
+ int64_t c, d;
+ uint32_t e, f;
+ int32_t g, h;
+ uint16_t i, j;
+ int16_t k, l;
+ } foobar = {};
+
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+
+ assert_se(json_build(&v, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("a", JSON_BUILD_UNSIGNED(UINT64_MAX)),
+ JSON_BUILD_PAIR("b", JSON_BUILD_STRING("18446744073709551615")),
+ JSON_BUILD_PAIR("c", JSON_BUILD_INTEGER(INT64_MIN)),
+ JSON_BUILD_PAIR("d", JSON_BUILD_STRING("-9223372036854775808")),
+ JSON_BUILD_PAIR("e", JSON_BUILD_UNSIGNED(UINT32_MAX)),
+ JSON_BUILD_PAIR("f", JSON_BUILD_STRING("4294967295")),
+ JSON_BUILD_PAIR("g", JSON_BUILD_INTEGER(INT32_MIN)),
+ JSON_BUILD_PAIR("h", JSON_BUILD_STRING("-2147483648")),
+ JSON_BUILD_PAIR("i", JSON_BUILD_UNSIGNED(UINT16_MAX)),
+ JSON_BUILD_PAIR("j", JSON_BUILD_STRING("65535")),
+ JSON_BUILD_PAIR("k", JSON_BUILD_INTEGER(INT16_MIN)),
+ JSON_BUILD_PAIR("l", JSON_BUILD_STRING("-32768")))) >= 0);
+
+ assert_se(json_variant_dump(v, JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO, stdout, /* prefix= */ NULL) >= 0);
+
+ JsonDispatch table[] = {
+ { "a", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(struct foobar, a) },
+ { "b", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint64, offsetof(struct foobar, b) },
+ { "c", _JSON_VARIANT_TYPE_INVALID, json_dispatch_int64, offsetof(struct foobar, c) },
+ { "d", _JSON_VARIANT_TYPE_INVALID, json_dispatch_int64, offsetof(struct foobar, d) },
+ { "e", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint32, offsetof(struct foobar, e) },
+ { "f", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint32, offsetof(struct foobar, f) },
+ { "g", _JSON_VARIANT_TYPE_INVALID, json_dispatch_int32, offsetof(struct foobar, g) },
+ { "h", _JSON_VARIANT_TYPE_INVALID, json_dispatch_int32, offsetof(struct foobar, h) },
+ { "i", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint16, offsetof(struct foobar, i) },
+ { "j", _JSON_VARIANT_TYPE_INVALID, json_dispatch_uint16, offsetof(struct foobar, j) },
+ { "k", _JSON_VARIANT_TYPE_INVALID, json_dispatch_int16, offsetof(struct foobar, k) },
+ { "l", _JSON_VARIANT_TYPE_INVALID, json_dispatch_int16, offsetof(struct foobar, l) },
+ {}
+ };
+
+ assert_se(json_dispatch(v, table, JSON_LOG, &foobar) >= 0);
+
+ assert_se(foobar.a == UINT64_MAX);
+ assert_se(foobar.b == UINT64_MAX);
+ assert_se(foobar.c == INT64_MIN);
+ assert_se(foobar.d == INT64_MIN);
+
+ assert_se(foobar.e == UINT32_MAX);
+ assert_se(foobar.f == UINT32_MAX);
+ assert_se(foobar.g == INT32_MIN);
+ assert_se(foobar.h == INT32_MIN);
+
+ assert_se(foobar.i == UINT16_MAX);
+ assert_se(foobar.j == UINT16_MAX);
+ assert_se(foobar.k == INT16_MIN);
+ assert_se(foobar.l == INT16_MIN);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-kbd-util.c b/src/test/test-kbd-util.c
new file mode 100644
index 0000000..0a166c6
--- /dev/null
+++ b/src/test/test-kbd-util.c
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "kbd-util.h"
+#include "log.h"
+#include "strv.h"
+#include "tests.h"
+
+int main(int argc, char *argv[]) {
+ _cleanup_strv_free_ char **maps = NULL;
+ int r;
+
+ log_show_color(true);
+ test_setup_logging(LOG_DEBUG);
+
+ r = get_keymaps(&maps);
+ if (r < 0) {
+ log_error_errno(r, "Failed to acquire keymaps: %m");
+ return 0;
+ }
+
+ STRV_FOREACH(m, maps) {
+ log_info("Found keymap: %s", *m);
+ assert_se(keymap_exists(*m) > 0);
+ }
+
+ return 0;
+}
diff --git a/src/test/test-libcrypt-util.c b/src/test/test-libcrypt-util.c
new file mode 100644
index 0000000..f88a9f9
--- /dev/null
+++ b/src/test/test-libcrypt-util.c
@@ -0,0 +1,126 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#if HAVE_CRYPT_H
+# include <crypt.h>
+#else
+# include <unistd.h>
+#endif
+
+#include "strv.h"
+#include "tests.h"
+#include "libcrypt-util.h"
+
+static void test_crypt_preferred_method(void) {
+ log_info("/* %s */", __func__);
+
+ log_info("crypt_preferred_method: %s",
+#if HAVE_CRYPT_PREFERRED_METHOD
+ crypt_preferred_method()
+#else
+ "(not available)"
+#endif
+ );
+}
+
+static void test_make_salt(void) {
+ log_info("/* %s */", __func__);
+
+ for (int i = 0; i < 10; i++) {
+ _cleanup_free_ char *t;
+
+ assert_se(make_salt(&t) == 0);
+ log_info("%s", t);
+ }
+}
+
+static int test_hash_password(void) {
+ log_info("/* %s */", __func__);
+
+ /* As a warm-up exercise, check if we can hash passwords. */
+
+ bool have_sane_hash = false;
+
+ FOREACH_STRING(hash,
+ "ew3bU1.hoKk4o",
+ "$1$gc5rWpTB$wK1aul1PyBn9AX1z93stk1",
+ "$2b$12$BlqcGkB/7BFvNMXKGxDea.5/8D6FTny.cbNcHW/tqcrcyo6ZJd8u2",
+ "$5$lGhDrcrao9zb5oIK$05KlOVG3ocknx/ThreqXE/gk.XzFFBMTksc4t2CPDUD",
+ "$6$c7wB/3GiRk0VHf7e$zXJ7hN0aLZapE.iO4mn/oHu6.prsXTUG/5k1AxpgR85ELolyAcaIGRgzfwJs3isTChMDBjnthZyaMCfCNxo9I.",
+ "$y$j9T$$9cKOWsAm4m97WiYk61lPPibZpy3oaGPIbsL4koRe/XD") {
+ int b;
+
+ b = test_password_one(hash, "ppp");
+ log_info("%s: %s", hash, yes_no(b));
+#if defined(XCRYPT_VERSION_MAJOR)
+ /* xcrypt is supposed to always implement all methods. */
+ assert_se(b);
+#endif
+
+ if (b && IN_SET(hash[1], '6', 'y'))
+ have_sane_hash = true;
+ }
+
+ return have_sane_hash;
+}
+
+static void test_hash_password_full(void) {
+ log_info("/* %s */", __func__);
+
+ _cleanup_free_ void *cd_data = NULL;
+ int cd_size = 0;
+
+ log_info("sizeof(struct crypt_data): %zu bytes", sizeof(struct crypt_data));
+
+ for (unsigned c = 0; c < 2; c++)
+ FOREACH_STRING(i, "abc123", "h⸿sło") {
+ _cleanup_free_ char *hashed;
+
+ if (c == 0)
+ assert_se(hash_password_full(i, &cd_data, &cd_size, &hashed) == 0);
+ else
+ assert_se(hash_password_full(i, NULL, NULL, &hashed) == 0);
+ log_debug("\"%s\" → \"%s\"", i, hashed);
+ log_info("crypt_r[a] buffer size: %i bytes", cd_size);
+
+ assert_se(test_password_one(hashed, i) == true);
+ assert_se(test_password_one(i, hashed) <= 0); /* We get an error for non-utf8 */
+ assert_se(test_password_one(hashed, "foobar") == false);
+ assert_se(test_password_many(STRV_MAKE(hashed), i) == true);
+ assert_se(test_password_many(STRV_MAKE(hashed), "foobar") == false);
+ assert_se(test_password_many(STRV_MAKE(hashed, hashed, hashed), "foobar") == false);
+ assert_se(test_password_many(STRV_MAKE("$y$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH",
+ hashed,
+ "$y$j9T$SAayASazWZIQeJd9AS02m/$"),
+ i) == true);
+ assert_se(test_password_many(STRV_MAKE("$W$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", /* no such method exists... */
+ hashed,
+ "$y$j9T$SAayASazWZIQeJd9AS02m/$"),
+ i) == true);
+ assert_se(test_password_many(STRV_MAKE("$y$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH",
+ hashed,
+ "$y$j9T$SAayASazWZIQeJd9AS02m/$"),
+ "") == false);
+ assert_se(test_password_many(STRV_MAKE("$W$j9T$dlCXwkX0GC5L6B8Gf.4PN/$VCyEH", /* no such method exists... */
+ hashed,
+ "$y$j9T$SAayASazWZIQeJd9AS02m/$"),
+ "") == false);
+ }
+}
+
+int main(int argc, char *argv[]) {
+ test_setup_logging(LOG_DEBUG);
+
+#if defined(__powerpc__) && !defined(XCRYPT_VERSION_MAJOR)
+ return log_tests_skipped("crypt_r() causes a buffer overflow on ppc64el, see https://github.com/systemd/systemd/pull/16981#issuecomment-691203787");
+#endif
+
+ test_crypt_preferred_method();
+ test_make_salt();
+
+ if (!test_hash_password())
+ return log_tests_skipped("crypt doesn't support yescrypt or sha512crypt");
+
+ test_hash_password_full();
+
+ return 0;
+}
diff --git a/src/test/test-libmount.c b/src/test/test-libmount.c
new file mode 100644
index 0000000..9ba428e
--- /dev/null
+++ b/src/test/test-libmount.c
@@ -0,0 +1,110 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "fd-util.h"
+#include "escape.h"
+#include "libmount-util.h"
+#include "tests.h"
+
+static void test_libmount_unescaping_one(
+ const char *title,
+ const char *string,
+ bool may_fail,
+ const char *expected_source,
+ const char *expected_target) {
+ /* A test for libmount really */
+ int r;
+
+ log_info("/* %s %s */", __func__, title);
+
+ _cleanup_(mnt_free_tablep) struct libmnt_table *table = NULL;
+ _cleanup_(mnt_free_iterp) struct libmnt_iter *iter = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+
+ f = fmemopen((char*) string, strlen(string), "r");
+ assert_se(f);
+
+ assert_se(libmount_parse(title, f, &table, &iter) >= 0);
+
+ struct libmnt_fs *fs;
+ const char *source, *target;
+ _cleanup_free_ char *x = NULL, *cs = NULL, *s = NULL, *ct = NULL, *t = NULL;
+
+ /* We allow this call and the checks below to fail in some cases. See the case definitions below. */
+
+ r = mnt_table_next_fs(table, iter, &fs);
+ if (r != 0 && may_fail) {
+ log_error_errno(r, "mnt_table_next_fs failed: %m");
+ return;
+ }
+ assert_se(r == 0);
+
+ assert_se(x = cescape(string));
+
+ assert_se(source = mnt_fs_get_source(fs));
+ assert_se(target = mnt_fs_get_target(fs));
+
+ assert_se(cs = cescape(source));
+ assert_se(ct = cescape(target));
+
+ assert_se(cunescape(source, UNESCAPE_RELAX, &s) >= 0);
+ assert_se(cunescape(target, UNESCAPE_RELAX, &t) >= 0);
+
+ log_info("from '%s'", x);
+ log_info("source: '%s'", source);
+ log_info("source: '%s'", cs);
+ log_info("source: '%s'", s);
+ log_info("expected: '%s'", strna(expected_source));
+ log_info("target: '%s'", target);
+ log_info("target: '%s'", ct);
+ log_info("target: '%s'", t);
+ log_info("expected: '%s'", strna(expected_target));
+
+ assert_se(may_fail || streq(source, expected_source));
+ assert_se(may_fail || streq(target, expected_target));
+
+ assert_se(mnt_table_next_fs(table, iter, &fs) == 1);
+}
+
+TEST(libmount_unescaping) {
+ test_libmount_unescaping_one(
+ "escaped space + utf8",
+ "729 38 0:59 / /tmp/„zupa\\040zębowa” rw,relatime shared:395 - tmpfs die\\040Brühe rw,seclabel",
+ false,
+ "die Brühe",
+ "/tmp/„zupa zębowa”"
+ );
+
+ test_libmount_unescaping_one(
+ "escaped newline",
+ "729 38 0:59 / /tmp/x\\012y rw,relatime shared:395 - tmpfs newline rw,seclabel",
+ false,
+ "newline",
+ "/tmp/x\ny"
+ );
+
+ /* The result of "mount -t tmpfs '' /tmp/emptysource".
+ * This will fail with libmount <= v2.33.
+ * See https://github.com/karelzak/util-linux/commit/18a52a5094.
+ */
+ test_libmount_unescaping_one(
+ "empty source",
+ "760 38 0:60 / /tmp/emptysource rw,relatime shared:410 - tmpfs rw,seclabel",
+ true,
+ "",
+ "/tmp/emptysource"
+ );
+
+ /* The kernel leaves \r as is.
+ * Also see https://github.com/karelzak/util-linux/issues/780.
+ */
+ test_libmount_unescaping_one(
+ "foo\\rbar",
+ "790 38 0:61 / /tmp/foo\rbar rw,relatime shared:425 - tmpfs tmpfs rw,seclabel",
+ true,
+ "tmpfs",
+ "/tmp/foo\rbar"
+ );
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-limits-util.c b/src/test/test-limits-util.c
new file mode 100644
index 0000000..3b6c8c0
--- /dev/null
+++ b/src/test/test-limits-util.c
@@ -0,0 +1,89 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "format-util.h"
+#include "limits-util.h"
+#include "tests.h"
+
+TEST(physical_memory) {
+ uint64_t p;
+
+ p = physical_memory();
+ assert_se(p > 0);
+ assert_se(p < UINT64_MAX);
+ assert_se(p % page_size() == 0);
+
+ log_info("Memory: %s (%" PRIu64 ")", FORMAT_BYTES(p), p);
+}
+
+TEST(physical_memory_scale) {
+ uint64_t p;
+
+ p = physical_memory();
+
+ assert_se(physical_memory_scale(0, 100) == 0);
+ assert_se(physical_memory_scale(100, 100) == p);
+
+ log_info("Memory original: %" PRIu64, physical_memory());
+ log_info("Memory scaled by 50%%: %" PRIu64, physical_memory_scale(50, 100));
+ log_info("Memory divided by 2: %" PRIu64, physical_memory() / 2);
+ log_info("Page size: %zu", page_size());
+
+ /* There might be an uneven number of pages, hence permit these calculations to be half a page off... */
+ assert_se(page_size()/2 + physical_memory_scale(50, 100) - p/2 <= page_size());
+ assert_se(physical_memory_scale(200, 100) == p*2);
+
+ assert_se(physical_memory_scale(0, 1) == 0);
+ assert_se(physical_memory_scale(1, 1) == p);
+ assert_se(physical_memory_scale(2, 1) == p*2);
+
+ assert_se(physical_memory_scale(0, 2) == 0);
+
+ assert_se(page_size()/2 + physical_memory_scale(1, 2) - p/2 <= page_size());
+ assert_se(physical_memory_scale(2, 2) == p);
+ assert_se(physical_memory_scale(4, 2) == p*2);
+
+ assert_se(physical_memory_scale(0, UINT32_MAX) == 0);
+ assert_se(physical_memory_scale(UINT32_MAX, UINT32_MAX) == p);
+
+ /* overflow */
+ assert_se(physical_memory_scale(UINT64_MAX/4, UINT64_MAX) == UINT64_MAX);
+}
+
+TEST(system_tasks_max) {
+ uint64_t t;
+
+ t = system_tasks_max();
+ assert_se(t > 0);
+ assert_se(t < UINT64_MAX);
+
+ log_info("Max tasks: %" PRIu64, t);
+}
+
+TEST(system_tasks_max_scale) {
+ uint64_t t;
+
+ t = system_tasks_max();
+
+ assert_se(system_tasks_max_scale(0, 100) == 0);
+ assert_se(system_tasks_max_scale(100, 100) == t);
+
+ assert_se(system_tasks_max_scale(0, 1) == 0);
+ assert_se(system_tasks_max_scale(1, 1) == t);
+ assert_se(system_tasks_max_scale(2, 1) == 2*t);
+
+ assert_se(system_tasks_max_scale(0, 2) == 0);
+ assert_se(system_tasks_max_scale(1, 2) == t/2);
+ assert_se(system_tasks_max_scale(2, 2) == t);
+ assert_se(system_tasks_max_scale(3, 2) == (3*t)/2);
+ assert_se(system_tasks_max_scale(4, 2) == t*2);
+
+ assert_se(system_tasks_max_scale(0, UINT32_MAX) == 0);
+ assert_se(system_tasks_max_scale((UINT32_MAX-1)/2, UINT32_MAX-1) == t/2);
+ assert_se(system_tasks_max_scale(UINT32_MAX, UINT32_MAX) == t);
+
+ /* overflow */
+
+ assert_se(system_tasks_max_scale(UINT64_MAX/4, UINT64_MAX) == UINT64_MAX);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-list.c b/src/test/test-list.c
new file mode 100644
index 0000000..87f7c7b
--- /dev/null
+++ b/src/test/test-list.c
@@ -0,0 +1,286 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "list.h"
+#include "tests.h"
+
+int main(int argc, const char *argv[]) {
+ test_setup_logging(LOG_DEBUG);
+
+ size_t i;
+ typedef struct list_item {
+ LIST_FIELDS(struct list_item, item_list);
+ } list_item;
+ LIST_HEAD(list_item, head);
+ LIST_HEAD(list_item, head2);
+ list_item items[4];
+
+ LIST_HEAD_INIT(head);
+ LIST_HEAD_INIT(head2);
+ assert_se(head == NULL);
+ assert_se(head2 == NULL);
+
+ for (i = 0; i < ELEMENTSOF(items); i++) {
+ LIST_INIT(item_list, &items[i]);
+ assert_se(LIST_JUST_US(item_list, &items[i]));
+ assert_se(LIST_PREPEND(item_list, head, &items[i]) == &items[i]);
+ }
+
+ i = 0;
+ LIST_FOREACH_OTHERS(item_list, cursor, &items[2]) {
+ i++;
+ assert_se(cursor != &items[2]);
+ }
+ assert_se(i == ELEMENTSOF(items)-1);
+
+ i = 0;
+ LIST_FOREACH_OTHERS(item_list, cursor, &items[0]) {
+ i++;
+ assert_se(cursor != &items[0]);
+ }
+ assert_se(i == ELEMENTSOF(items)-1);
+
+ i = 0;
+ LIST_FOREACH_OTHERS(item_list, cursor, &items[3]) {
+ i++;
+ assert_se(cursor != &items[3]);
+ }
+ assert_se(i == ELEMENTSOF(items)-1);
+
+ assert_se(!LIST_JUST_US(item_list, head));
+
+ assert_se(items[0].item_list_next == NULL);
+ assert_se(items[1].item_list_next == &items[0]);
+ assert_se(items[2].item_list_next == &items[1]);
+ assert_se(items[3].item_list_next == &items[2]);
+
+ assert_se(items[0].item_list_prev == &items[1]);
+ assert_se(items[1].item_list_prev == &items[2]);
+ assert_se(items[2].item_list_prev == &items[3]);
+ assert_se(items[3].item_list_prev == NULL);
+
+ list_item *cursor = LIST_FIND_HEAD(item_list, &items[0]);
+ assert_se(cursor == &items[3]);
+
+ cursor = LIST_FIND_TAIL(item_list, &items[3]);
+ assert_se(cursor == &items[0]);
+
+ assert_se(LIST_REMOVE(item_list, head, &items[1]) == &items[1]);
+ assert_se(LIST_JUST_US(item_list, &items[1]));
+
+ assert_se(items[0].item_list_next == NULL);
+ assert_se(items[2].item_list_next == &items[0]);
+ assert_se(items[3].item_list_next == &items[2]);
+
+ assert_se(items[0].item_list_prev == &items[2]);
+ assert_se(items[2].item_list_prev == &items[3]);
+ assert_se(items[3].item_list_prev == NULL);
+
+ assert_se(LIST_INSERT_AFTER(item_list, head, &items[3], &items[1]) == &items[1]);
+ assert_se(items[0].item_list_next == NULL);
+ assert_se(items[2].item_list_next == &items[0]);
+ assert_se(items[1].item_list_next == &items[2]);
+ assert_se(items[3].item_list_next == &items[1]);
+
+ assert_se(items[0].item_list_prev == &items[2]);
+ assert_se(items[2].item_list_prev == &items[1]);
+ assert_se(items[1].item_list_prev == &items[3]);
+ assert_se(items[3].item_list_prev == NULL);
+
+ assert_se(LIST_REMOVE(item_list, head, &items[1]) == &items[1]);
+ assert_se(LIST_JUST_US(item_list, &items[1]));
+
+ assert_se(items[0].item_list_next == NULL);
+ assert_se(items[2].item_list_next == &items[0]);
+ assert_se(items[3].item_list_next == &items[2]);
+
+ assert_se(items[0].item_list_prev == &items[2]);
+ assert_se(items[2].item_list_prev == &items[3]);
+ assert_se(items[3].item_list_prev == NULL);
+
+ assert_se(LIST_INSERT_BEFORE(item_list, head, &items[2], &items[1]) == &items[1]);
+ assert_se(items[0].item_list_next == NULL);
+ assert_se(items[2].item_list_next == &items[0]);
+ assert_se(items[1].item_list_next == &items[2]);
+ assert_se(items[3].item_list_next == &items[1]);
+
+ assert_se(items[0].item_list_prev == &items[2]);
+ assert_se(items[2].item_list_prev == &items[1]);
+ assert_se(items[1].item_list_prev == &items[3]);
+ assert_se(items[3].item_list_prev == NULL);
+
+ assert_se(LIST_REMOVE(item_list, head, &items[0]) == &items[0]);
+ assert_se(LIST_JUST_US(item_list, &items[0]));
+
+ assert_se(items[2].item_list_next == NULL);
+ assert_se(items[1].item_list_next == &items[2]);
+ assert_se(items[3].item_list_next == &items[1]);
+
+ assert_se(items[2].item_list_prev == &items[1]);
+ assert_se(items[1].item_list_prev == &items[3]);
+ assert_se(items[3].item_list_prev == NULL);
+
+ assert_se(LIST_INSERT_BEFORE(item_list, head, &items[3], &items[0]) == &items[0]);
+ assert_se(items[2].item_list_next == NULL);
+ assert_se(items[1].item_list_next == &items[2]);
+ assert_se(items[3].item_list_next == &items[1]);
+ assert_se(items[0].item_list_next == &items[3]);
+
+ assert_se(items[2].item_list_prev == &items[1]);
+ assert_se(items[1].item_list_prev == &items[3]);
+ assert_se(items[3].item_list_prev == &items[0]);
+ assert_se(items[0].item_list_prev == NULL);
+ assert_se(head == &items[0]);
+
+ assert_se(LIST_REMOVE(item_list, head, &items[0]) == &items[0]);
+ assert_se(LIST_JUST_US(item_list, &items[0]));
+
+ assert_se(items[2].item_list_next == NULL);
+ assert_se(items[1].item_list_next == &items[2]);
+ assert_se(items[3].item_list_next == &items[1]);
+
+ assert_se(items[2].item_list_prev == &items[1]);
+ assert_se(items[1].item_list_prev == &items[3]);
+ assert_se(items[3].item_list_prev == NULL);
+
+ assert_se(LIST_INSERT_BEFORE(item_list, head, NULL, &items[0]) == &items[0]);
+ assert_se(items[0].item_list_next == NULL);
+ assert_se(items[2].item_list_next == &items[0]);
+ assert_se(items[1].item_list_next == &items[2]);
+ assert_se(items[3].item_list_next == &items[1]);
+
+ assert_se(items[0].item_list_prev == &items[2]);
+ assert_se(items[2].item_list_prev == &items[1]);
+ assert_se(items[1].item_list_prev == &items[3]);
+ assert_se(items[3].item_list_prev == NULL);
+
+ assert_se(LIST_REMOVE(item_list, head, &items[0]) == &items[0]);
+ assert_se(LIST_JUST_US(item_list, &items[0]));
+
+ assert_se(items[2].item_list_next == NULL);
+ assert_se(items[1].item_list_next == &items[2]);
+ assert_se(items[3].item_list_next == &items[1]);
+
+ assert_se(items[2].item_list_prev == &items[1]);
+ assert_se(items[1].item_list_prev == &items[3]);
+ assert_se(items[3].item_list_prev == NULL);
+
+ assert_se(LIST_REMOVE(item_list, head, &items[1]) == &items[1]);
+ assert_se(LIST_JUST_US(item_list, &items[1]));
+
+ assert_se(items[2].item_list_next == NULL);
+ assert_se(items[3].item_list_next == &items[2]);
+
+ assert_se(items[2].item_list_prev == &items[3]);
+ assert_se(items[3].item_list_prev == NULL);
+
+ assert_se(LIST_REMOVE(item_list, head, &items[2]) == &items[2]);
+ assert_se(LIST_JUST_US(item_list, &items[2]));
+ assert_se(LIST_JUST_US(item_list, head));
+
+ assert_se(LIST_REMOVE(item_list, head, &items[3]) == &items[3]);
+ assert_se(LIST_JUST_US(item_list, &items[3]));
+
+ assert_se(head == NULL);
+
+ for (i = 0; i < ELEMENTSOF(items); i++) {
+ assert_se(LIST_JUST_US(item_list, &items[i]));
+ assert_se(LIST_APPEND(item_list, head, &items[i]) == &items[i]);
+ }
+
+ assert_se(!LIST_JUST_US(item_list, head));
+
+ assert_se(items[0].item_list_next == &items[1]);
+ assert_se(items[1].item_list_next == &items[2]);
+ assert_se(items[2].item_list_next == &items[3]);
+ assert_se(items[3].item_list_next == NULL);
+
+ assert_se(items[0].item_list_prev == NULL);
+ assert_se(items[1].item_list_prev == &items[0]);
+ assert_se(items[2].item_list_prev == &items[1]);
+ assert_se(items[3].item_list_prev == &items[2]);
+
+ for (i = 0; i < ELEMENTSOF(items); i++)
+ assert_se(LIST_REMOVE(item_list, head, &items[i]) == &items[i]);
+
+ assert_se(head == NULL);
+
+ for (i = 0; i < ELEMENTSOF(items) / 2; i++) {
+ LIST_INIT(item_list, &items[i]);
+ assert_se(LIST_JUST_US(item_list, &items[i]));
+ assert_se(LIST_PREPEND(item_list, head, &items[i]) == &items[i]);
+ }
+
+ for (i = ELEMENTSOF(items) / 2; i < ELEMENTSOF(items); i++) {
+ LIST_INIT(item_list, &items[i]);
+ assert_se(LIST_JUST_US(item_list, &items[i]));
+ assert_se(LIST_PREPEND(item_list, head2, &items[i]) == &items[i]);
+ }
+
+ assert_se(items[0].item_list_next == NULL);
+ assert_se(items[1].item_list_next == &items[0]);
+ assert_se(items[2].item_list_next == NULL);
+ assert_se(items[3].item_list_next == &items[2]);
+
+ assert_se(items[0].item_list_prev == &items[1]);
+ assert_se(items[1].item_list_prev == NULL);
+ assert_se(items[2].item_list_prev == &items[3]);
+ assert_se(items[3].item_list_prev == NULL);
+
+ assert_se(LIST_JOIN(item_list, head2, head) == head2);
+ assert_se(head == NULL);
+
+ assert_se(items[0].item_list_next == NULL);
+ assert_se(items[1].item_list_next == &items[0]);
+ assert_se(items[2].item_list_next == &items[1]);
+ assert_se(items[3].item_list_next == &items[2]);
+
+ assert_se(items[0].item_list_prev == &items[1]);
+ assert_se(items[1].item_list_prev == &items[2]);
+ assert_se(items[2].item_list_prev == &items[3]);
+ assert_se(items[3].item_list_prev == NULL);
+
+ assert_se(LIST_JOIN(item_list, head, head2) == head);
+ assert_se(head2 == NULL);
+ assert_se(head);
+
+ for (i = 0; i < ELEMENTSOF(items); i++)
+ assert_se(LIST_REMOVE(item_list, head, &items[i]) == &items[i]);
+
+ assert_se(head == NULL);
+
+ assert_se(LIST_PREPEND(item_list, head, items + 0) == items + 0);
+ assert_se(LIST_PREPEND(item_list, head, items + 1) == items + 1);
+ assert_se(LIST_PREPEND(item_list, head, items + 2) == items + 2);
+
+ assert_se(LIST_POP(item_list, head) == items + 2);
+ assert_se(LIST_POP(item_list, head) == items + 1);
+ assert_se(LIST_POP(item_list, head) == items + 0);
+ assert_se(LIST_POP(item_list, head) == NULL);
+
+ /* No-op on an empty list */
+
+ LIST_CLEAR(item_list, head, free);
+
+ /* A non-empty list is cleared */
+
+ assert_se(LIST_PREPEND(item_list, head, new0(list_item, 1)));
+ assert_se(LIST_PREPEND(item_list, head, new0(list_item, 1)));
+
+ LIST_CLEAR(item_list, head, free);
+
+ assert_se(head == NULL);
+
+ /* A list can be cleared partially */
+
+ assert_se(LIST_PREPEND(item_list, head, new0(list_item, 1)));
+ assert_se(LIST_PREPEND(item_list, head, new0(list_item, 1)));
+ assert_se(LIST_PREPEND(item_list, head, items + 0) == items + 0);
+
+ LIST_CLEAR(item_list, head->item_list_next, free);
+
+ assert_se(head == items + 0);
+ assert_se(head->item_list_next == NULL);
+
+ return 0;
+}
diff --git a/src/test/test-load-fragment.c b/src/test/test-load-fragment.c
new file mode 100644
index 0000000..8d2cec0
--- /dev/null
+++ b/src/test/test-load-fragment.c
@@ -0,0 +1,1105 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "sd-id128.h"
+
+#include "all-units.h"
+#include "alloc-util.h"
+#include "capability-util.h"
+#include "conf-parser.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "format-util.h"
+#include "fs-util.h"
+#include "hashmap.h"
+#include "hostname-util.h"
+#include "install-printf.h"
+#include "install.h"
+#include "load-fragment.h"
+#include "macro.h"
+#include "memory-util.h"
+#include "open-file.h"
+#include "pcre2-util.h"
+#include "rm-rf.h"
+#include "specifier.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+#include "user-util.h"
+
+/* Nontrivial value serves as a placeholder to check that parsing function (didn't) change it */
+#define CGROUP_LIMIT_DUMMY 3
+
+static char *runtime_dir = NULL;
+
+STATIC_DESTRUCTOR_REGISTER(runtime_dir, rm_rf_physical_and_freep);
+
+/* For testing type compatibility. */
+_unused_ ConfigPerfItemLookup unused_lookup = load_fragment_gperf_lookup;
+
+TEST_RET(unit_file_get_set) {
+ int r;
+ _cleanup_hashmap_free_ Hashmap *h = NULL;
+ UnitFileList *p;
+
+ h = hashmap_new(&unit_file_list_hash_ops_free);
+ assert_se(h);
+
+ r = unit_file_get_list(RUNTIME_SCOPE_SYSTEM, NULL, h, NULL, NULL);
+ if (IN_SET(r, -EPERM, -EACCES))
+ return log_tests_skipped_errno(r, "unit_file_get_list");
+
+ log_full_errno(r == 0 ? LOG_INFO : LOG_ERR, r,
+ "unit_file_get_list: %m");
+ if (r < 0)
+ return EXIT_FAILURE;
+
+ HASHMAP_FOREACH(p, h)
+ printf("%s = %s\n", p->path, unit_file_state_to_string(p->state));
+
+ return 0;
+}
+
+static void check_execcommand(ExecCommand *c,
+ const char* path,
+ const char* argv0,
+ const char* argv1,
+ const char* argv2,
+ bool ignore) {
+ size_t n;
+
+ assert_se(c);
+ log_info("expect: \"%s\" [\"%s\" \"%s\" \"%s\"]",
+ path, argv0 ?: path, strnull(argv1), strnull(argv2));
+ n = strv_length(c->argv);
+ log_info("actual: \"%s\" [\"%s\" \"%s\" \"%s\"]",
+ c->path, c->argv[0], n > 0 ? c->argv[1] : "(null)", n > 1 ? c->argv[2] : "(null)");
+ assert_se(streq(c->path, path));
+ assert_se(streq(c->argv[0], argv0 ?: path));
+ if (n > 0)
+ assert_se(streq_ptr(c->argv[1], argv1));
+ if (n > 1)
+ assert_se(streq_ptr(c->argv[2], argv2));
+ assert_se(!!(c->flags & EXEC_COMMAND_IGNORE_FAILURE) == ignore);
+}
+
+TEST(config_parse_exec) {
+ /* int config_parse_exec(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) */
+ int r;
+
+ ExecCommand *c = NULL, *c1;
+ const char *ccc;
+ _cleanup_(manager_freep) Manager *m = NULL;
+ _cleanup_(unit_freep) Unit *u = NULL;
+
+ r = manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_MINIMAL, &m);
+ if (manager_errno_skip_test(r)) {
+ log_notice_errno(r, "Skipping test: manager_new: %m");
+ return;
+ }
+
+ assert_se(r >= 0);
+ assert_se(manager_startup(m, NULL, NULL, NULL) >= 0);
+
+ assert_se(u = unit_new(m, sizeof(Service)));
+
+ log_info("/* basic test */");
+ r = config_parse_exec(NULL, "fake", 1, "section", 1,
+ "LValue", 0, "/RValue r1",
+ &c, u);
+ assert_se(r >= 0);
+ check_execcommand(c, "/RValue", "/RValue", "r1", NULL, false);
+
+ r = config_parse_exec(NULL, "fake", 2, "section", 1,
+ "LValue", 0, "/RValue///slashes r1///",
+ &c, u);
+
+ log_info("/* test slashes */");
+ assert_se(r >= 0);
+ c1 = c->command_next;
+ check_execcommand(c1, "/RValue/slashes", "/RValue///slashes", "r1///", NULL, false);
+
+ log_info("/* trailing slash */");
+ r = config_parse_exec(NULL, "fake", 4, "section", 1,
+ "LValue", 0, "/RValue/ argv0 r1",
+ &c, u);
+ assert_se(r == -ENOEXEC);
+ assert_se(c1->command_next == NULL);
+
+ log_info("/* honour_argv0 */");
+ r = config_parse_exec(NULL, "fake", 3, "section", 1,
+ "LValue", 0, "@/RValue///slashes2 ///argv0 r1",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1, "/RValue/slashes2", "///argv0", "r1", NULL, false);
+
+ log_info("/* honour_argv0, no args */");
+ r = config_parse_exec(NULL, "fake", 3, "section", 1,
+ "LValue", 0, "@/RValue",
+ &c, u);
+ assert_se(r == -ENOEXEC);
+ assert_se(c1->command_next == NULL);
+
+ log_info("/* no command, whitespace only, reset */");
+ r = config_parse_exec(NULL, "fake", 3, "section", 1,
+ "LValue", 0, "",
+ &c, u);
+ assert_se(r == 0);
+ assert_se(c == NULL);
+
+ log_info("/* ignore && honour_argv0 */");
+ r = config_parse_exec(NULL, "fake", 4, "section", 1,
+ "LValue", 0, "-@/RValue///slashes3 argv0a r1",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c;
+ check_execcommand(c1, "/RValue/slashes3", "argv0a", "r1", NULL, true);
+
+ log_info("/* ignore && honour_argv0 */");
+ r = config_parse_exec(NULL, "fake", 4, "section", 1,
+ "LValue", 0, "@-/RValue///slashes4 argv0b r1",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1, "/RValue/slashes4", "argv0b", "r1", NULL, true);
+
+ log_info("/* ignore && ignore */");
+ r = config_parse_exec(NULL, "fake", 4, "section", 1,
+ "LValue", 0, "--/RValue argv0 r1",
+ &c, u);
+ assert_se(r == 0);
+ assert_se(c1->command_next == NULL);
+
+ log_info("/* ignore && ignore (2) */");
+ r = config_parse_exec(NULL, "fake", 4, "section", 1,
+ "LValue", 0, "-@-/RValue argv0 r1",
+ &c, u);
+ assert_se(r == 0);
+ assert_se(c1->command_next == NULL);
+
+ log_info("/* semicolon */");
+ r = config_parse_exec(NULL, "fake", 5, "section", 1,
+ "LValue", 0,
+ "-@/RValue argv0 r1 ; "
+ "/goo/goo boo",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1, "/RValue", "argv0", "r1", NULL, true);
+
+ c1 = c1->command_next;
+ check_execcommand(c1, "/goo/goo", NULL, "boo", NULL, false);
+
+ log_info("/* two semicolons in a row */");
+ r = config_parse_exec(NULL, "fake", 5, "section", 1,
+ "LValue", 0,
+ "-@/RValue argv0 r1 ; ; "
+ "/goo/goo boo",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1, "/RValue", "argv0", "r1", NULL, true);
+ c1 = c1->command_next;
+ check_execcommand(c1, "/goo/goo", "/goo/goo", "boo", NULL, false);
+
+ log_info("/* trailing semicolon */");
+ r = config_parse_exec(NULL, "fake", 5, "section", 1,
+ "LValue", 0,
+ "-@/RValue argv0 r1 ; ",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1, "/RValue", "argv0", "r1", NULL, true);
+
+ assert_se(c1->command_next == NULL);
+
+ log_info("/* trailing semicolon, no whitespace */");
+ r = config_parse_exec(NULL, "fake", 5, "section", 1,
+ "LValue", 0,
+ "-@/RValue argv0 r1 ;",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1, "/RValue", "argv0", "r1", NULL, true);
+
+ assert_se(c1->command_next == NULL);
+
+ log_info("/* trailing semicolon in single quotes */");
+ r = config_parse_exec(NULL, "fake", 5, "section", 1,
+ "LValue", 0,
+ "-@/RValue argv0 r1 ';'",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1, "/RValue", "argv0", "r1", ";", true);
+
+ log_info("/* escaped semicolon */");
+ r = config_parse_exec(NULL, "fake", 5, "section", 1,
+ "LValue", 0,
+ "/bin/find \\;",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1, "/bin/find", NULL, ";", NULL, false);
+
+ log_info("/* escaped semicolon with following arg */");
+ r = config_parse_exec(NULL, "fake", 5, "section", 1,
+ "LValue", 0,
+ "/sbin/find \\; /x",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1,
+ "/sbin/find", NULL, ";", "/x", false);
+
+ log_info("/* escaped semicolon as part of an expression */");
+ r = config_parse_exec(NULL, "fake", 5, "section", 1,
+ "LValue", 0,
+ "/sbin/find \\;x",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1,
+ "/sbin/find", NULL, "\\;x", NULL, false);
+
+ log_info("/* encoded semicolon */");
+ r = config_parse_exec(NULL, "fake", 5, "section", 1,
+ "LValue", 0,
+ "/bin/find \\073",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1, "/bin/find", NULL, ";", NULL, false);
+
+ log_info("/* quoted semicolon */");
+ r = config_parse_exec(NULL, "fake", 5, "section", 1,
+ "LValue", 0,
+ "/bin/find \";\"",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1, "/bin/find", NULL, ";", NULL, false);
+
+ log_info("/* quoted semicolon with following arg */");
+ r = config_parse_exec(NULL, "fake", 5, "section", 1,
+ "LValue", 0,
+ "/sbin/find \";\" /x",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1,
+ "/sbin/find", NULL, ";", "/x", false);
+
+ log_info("/* spaces in the filename */");
+ r = config_parse_exec(NULL, "fake", 5, "section", 1,
+ "LValue", 0,
+ "\"/PATH WITH SPACES/daemon\" -1 -2",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1,
+ "/PATH WITH SPACES/daemon", NULL, "-1", "-2", false);
+
+ log_info("/* spaces in the filename, no args */");
+ r = config_parse_exec(NULL, "fake", 5, "section", 1,
+ "LValue", 0,
+ "\"/PATH WITH SPACES/daemon -1 -2\"",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1,
+ "/PATH WITH SPACES/daemon -1 -2", NULL, NULL, NULL, false);
+
+ log_info("/* spaces in the filename, everything quoted */");
+ r = config_parse_exec(NULL, "fake", 5, "section", 1,
+ "LValue", 0,
+ "\"/PATH WITH SPACES/daemon\" \"-1\" '-2'",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1,
+ "/PATH WITH SPACES/daemon", NULL, "-1", "-2", false);
+
+ log_info("/* escaped spaces in the filename */");
+ r = config_parse_exec(NULL, "fake", 5, "section", 1,
+ "LValue", 0,
+ "\"/PATH\\sWITH\\sSPACES/daemon\" '-1 -2'",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1,
+ "/PATH WITH SPACES/daemon", NULL, "-1 -2", NULL, false);
+
+ log_info("/* escaped spaces in the filename (2) */");
+ r = config_parse_exec(NULL, "fake", 5, "section", 1,
+ "LValue", 0,
+ "\"/PATH\\x20WITH\\x20SPACES/daemon\" \"-1 -2\"",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1,
+ "/PATH WITH SPACES/daemon", NULL, "-1 -2", NULL, false);
+
+ for (ccc = "abfnrtv\\\'\"x"; *ccc; ccc++) {
+ /* \\x is an incomplete hexadecimal sequence, invalid because of the slash */
+ char path[] = "/path\\X";
+ path[sizeof(path) - 2] = *ccc;
+
+ log_info("/* invalid character: \\%c */", *ccc);
+ r = config_parse_exec(NULL, "fake", 4, "section", 1,
+ "LValue", 0, path,
+ &c, u);
+ assert_se(r == -ENOEXEC);
+ assert_se(c1->command_next == NULL);
+ }
+
+ log_info("/* valid character: \\s */");
+ r = config_parse_exec(NULL, "fake", 4, "section", 1,
+ "LValue", 0, "/path\\s",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1, "/path ", NULL, NULL, NULL, false);
+
+ log_info("/* quoted backslashes */");
+ r = config_parse_exec(NULL, "fake", 5, "section", 1,
+ "LValue", 0,
+ "/bin/grep '\\w+\\K'",
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1, "/bin/grep", NULL, "\\w+\\K", NULL, false);
+
+ log_info("/* trailing backslash: \\ */");
+ /* backslash is invalid */
+ r = config_parse_exec(NULL, "fake", 4, "section", 1,
+ "LValue", 0, "/path\\",
+ &c, u);
+ assert_se(r == -ENOEXEC);
+ assert_se(c1->command_next == NULL);
+
+ log_info("/* missing ending ' */");
+ r = config_parse_exec(NULL, "fake", 4, "section", 1,
+ "LValue", 0, "/path 'foo",
+ &c, u);
+ assert_se(r == -ENOEXEC);
+ assert_se(c1->command_next == NULL);
+
+ log_info("/* missing ending ' with trailing backslash */");
+ r = config_parse_exec(NULL, "fake", 4, "section", 1,
+ "LValue", 0, "/path 'foo\\",
+ &c, u);
+ assert_se(r == -ENOEXEC);
+ assert_se(c1->command_next == NULL);
+
+ log_info("/* invalid space between modifiers */");
+ r = config_parse_exec(NULL, "fake", 4, "section", 1,
+ "LValue", 0, "- /path",
+ &c, u);
+ assert_se(r == 0);
+ assert_se(c1->command_next == NULL);
+
+ log_info("/* only modifiers, no path */");
+ r = config_parse_exec(NULL, "fake", 4, "section", 1,
+ "LValue", 0, "-",
+ &c, u);
+ assert_se(r == 0);
+ assert_se(c1->command_next == NULL);
+
+ log_info("/* long arg */"); /* See issue #22957. */
+
+ char x[LONG_LINE_MAX-100], *y;
+ y = mempcpy(x, "/bin/echo ", STRLEN("/bin/echo "));
+ memset(y, 'x', sizeof(x) - STRLEN("/bin/echo ") - 1);
+ x[sizeof(x) - 1] = '\0';
+
+ r = config_parse_exec(NULL, "fake", 5, "section", 1,
+ "LValue", 0, x,
+ &c, u);
+ assert_se(r >= 0);
+ c1 = c1->command_next;
+ check_execcommand(c1,
+ "/bin/echo", NULL, y, NULL, false);
+
+ log_info("/* empty argument, reset */");
+ r = config_parse_exec(NULL, "fake", 4, "section", 1,
+ "LValue", 0, "",
+ &c, u);
+ assert_se(r == 0);
+ assert_se(c == NULL);
+
+ exec_command_free_list(c);
+}
+
+TEST(config_parse_log_extra_fields) {
+ /* int config_parse_log_extra_fields(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) */
+
+ int r;
+
+ _cleanup_(manager_freep) Manager *m = NULL;
+ _cleanup_(unit_freep) Unit *u = NULL;
+ ExecContext c = {};
+
+ r = manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_MINIMAL, &m);
+ if (manager_errno_skip_test(r)) {
+ log_notice_errno(r, "Skipping test: manager_new: %m");
+ return;
+ }
+
+ assert_se(r >= 0);
+ assert_se(manager_startup(m, NULL, NULL, NULL) >= 0);
+
+ assert_se(u = unit_new(m, sizeof(Service)));
+
+ log_info("/* %s – basic test */", __func__);
+ r = config_parse_log_extra_fields(NULL, "fake", 1, "section", 1,
+ "LValue", 0, "FOO=BAR \"QOOF=quux ' ' \"",
+ &c, u);
+ assert_se(r >= 0);
+ assert_se(c.n_log_extra_fields == 2);
+ assert_se(strneq(c.log_extra_fields[0].iov_base, "FOO=BAR", c.log_extra_fields[0].iov_len));
+ assert_se(strneq(c.log_extra_fields[1].iov_base, "QOOF=quux ' ' ", c.log_extra_fields[1].iov_len));
+
+ log_info("/* %s – add some */", __func__);
+ r = config_parse_log_extra_fields(NULL, "fake", 1, "section", 1,
+ "LValue", 0, "FOO2=BAR2 QOOF2=quux ' '",
+ &c, u);
+ assert_se(r >= 0);
+ assert_se(c.n_log_extra_fields == 4);
+ assert_se(strneq(c.log_extra_fields[0].iov_base, "FOO=BAR", c.log_extra_fields[0].iov_len));
+ assert_se(strneq(c.log_extra_fields[1].iov_base, "QOOF=quux ' ' ", c.log_extra_fields[1].iov_len));
+ assert_se(strneq(c.log_extra_fields[2].iov_base, "FOO2=BAR2", c.log_extra_fields[2].iov_len));
+ assert_se(strneq(c.log_extra_fields[3].iov_base, "QOOF2=quux", c.log_extra_fields[3].iov_len));
+
+ exec_context_dump(&c, stdout, " --> ");
+
+ log_info("/* %s – reset */", __func__);
+ r = config_parse_log_extra_fields(NULL, "fake", 1, "section", 1,
+ "LValue", 0, "",
+ &c, u);
+ assert_se(r >= 0);
+ assert_se(c.n_log_extra_fields == 0);
+
+ exec_context_free_log_extra_fields(&c);
+
+ log_info("/* %s – bye */", __func__);
+}
+
+TEST(install_printf, .sd_booted = true) {
+ char name[] = "name.service",
+ path[] = "/run/systemd/system/name.service";
+ InstallInfo i = { .name = name, .path = path, };
+ InstallInfo i2 = { .name= name, .path = path, };
+ char name3[] = "name@inst.service",
+ path3[] = "/run/systemd/system/name.service";
+ InstallInfo i3 = { .name = name3, .path = path3, };
+ InstallInfo i4 = { .name = name3, .path = path3, };
+
+ _cleanup_free_ char *mid = NULL, *bid = NULL, *host = NULL, *gid = NULL, *group = NULL, *uid = NULL, *user = NULL;
+
+ if (sd_id128_get_machine(NULL) >= 0)
+ assert_se(specifier_machine_id('m', NULL, NULL, NULL, &mid) >= 0 && mid);
+ if (sd_booted() > 0)
+ assert_se(specifier_boot_id('b', NULL, NULL, NULL, &bid) >= 0 && bid);
+ assert_se(host = gethostname_malloc());
+ assert_se(group = gid_to_name(getgid()));
+ assert_se(asprintf(&gid, UID_FMT, getgid()) >= 0);
+ assert_se(user = uid_to_name(getuid()));
+ assert_se(asprintf(&uid, UID_FMT, getuid()) >= 0);
+
+#define expect(scope, src, pattern, result) \
+ do { \
+ _cleanup_free_ char *t = NULL, \
+ *d1 = ASSERT_PTR(strdup(i.name)), \
+ *d2 = ASSERT_PTR(strdup(i.path)); \
+ int r = install_name_printf(scope, &src, pattern, &t); \
+ assert_se(result ? r >= 0 : r < 0); \
+ memzero(i.name, strlen(i.name)); \
+ memzero(i.path, strlen(i.path)); \
+ if (result) { \
+ printf("%s\n", t); \
+ assert_se(streq(t, result)); \
+ } else \
+ assert_se(!t); \
+ strcpy(i.name, d1); \
+ strcpy(i.path, d2); \
+ } while (false)
+
+ expect(RUNTIME_SCOPE_SYSTEM, i, "%n", "name.service");
+ expect(RUNTIME_SCOPE_SYSTEM, i, "%N", "name");
+ expect(RUNTIME_SCOPE_SYSTEM, i, "%p", "name");
+ expect(RUNTIME_SCOPE_SYSTEM, i, "%i", "");
+ expect(RUNTIME_SCOPE_SYSTEM, i, "%j", "name");
+ expect(RUNTIME_SCOPE_SYSTEM, i, "%g", "root");
+ expect(RUNTIME_SCOPE_SYSTEM, i, "%G", "0");
+ expect(RUNTIME_SCOPE_SYSTEM, i, "%u", "root");
+ expect(RUNTIME_SCOPE_SYSTEM, i, "%U", "0");
+
+ expect(RUNTIME_SCOPE_SYSTEM, i, "%m", mid);
+ expect(RUNTIME_SCOPE_SYSTEM, i, "%b", bid);
+ expect(RUNTIME_SCOPE_SYSTEM, i, "%H", host);
+
+ expect(RUNTIME_SCOPE_SYSTEM, i2, "%g", "root");
+ expect(RUNTIME_SCOPE_SYSTEM, i2, "%G", "0");
+ expect(RUNTIME_SCOPE_SYSTEM, i2, "%u", "root");
+ expect(RUNTIME_SCOPE_SYSTEM, i2, "%U", "0");
+
+ expect(RUNTIME_SCOPE_USER, i2, "%g", group);
+ expect(RUNTIME_SCOPE_USER, i2, "%G", gid);
+ expect(RUNTIME_SCOPE_USER, i2, "%u", user);
+ expect(RUNTIME_SCOPE_USER, i2, "%U", uid);
+
+ /* gcc-12.0.1-0.9.fc36.x86_64 insist that streq(…, NULL) is called,
+ * even though the call is inside of a conditional where the pointer is checked. :( */
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wnonnull"
+ expect(RUNTIME_SCOPE_GLOBAL, i2, "%g", NULL);
+ expect(RUNTIME_SCOPE_GLOBAL, i2, "%G", NULL);
+ expect(RUNTIME_SCOPE_GLOBAL, i2, "%u", NULL);
+ expect(RUNTIME_SCOPE_GLOBAL, i2, "%U", NULL);
+#pragma GCC diagnostic pop
+
+ expect(RUNTIME_SCOPE_SYSTEM, i3, "%n", "name@inst.service");
+ expect(RUNTIME_SCOPE_SYSTEM, i3, "%N", "name@inst");
+ expect(RUNTIME_SCOPE_SYSTEM, i3, "%p", "name");
+ expect(RUNTIME_SCOPE_USER, i3, "%g", group);
+ expect(RUNTIME_SCOPE_USER, i3, "%G", gid);
+ expect(RUNTIME_SCOPE_USER, i3, "%u", user);
+ expect(RUNTIME_SCOPE_USER, i3, "%U", uid);
+
+ expect(RUNTIME_SCOPE_SYSTEM, i3, "%m", mid);
+ expect(RUNTIME_SCOPE_SYSTEM, i3, "%b", bid);
+ expect(RUNTIME_SCOPE_SYSTEM, i3, "%H", host);
+
+ expect(RUNTIME_SCOPE_USER, i4, "%g", group);
+ expect(RUNTIME_SCOPE_USER, i4, "%G", gid);
+ expect(RUNTIME_SCOPE_USER, i4, "%u", user);
+ expect(RUNTIME_SCOPE_USER, i4, "%U", uid);
+}
+
+static uint64_t make_cap(int cap) {
+ return ((uint64_t) 1ULL << (uint64_t) cap);
+}
+
+TEST(config_parse_capability_set) {
+ /* int config_parse_capability_set(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) */
+ int r;
+ uint64_t capability_bounding_set = 0;
+
+ r = config_parse_capability_set(NULL, "fake", 1, "section", 1,
+ "CapabilityBoundingSet", 0, "CAP_NET_RAW",
+ &capability_bounding_set, NULL);
+ assert_se(r >= 0);
+ assert_se(capability_bounding_set == make_cap(CAP_NET_RAW));
+
+ r = config_parse_capability_set(NULL, "fake", 1, "section", 1,
+ "CapabilityBoundingSet", 0, "CAP_NET_ADMIN",
+ &capability_bounding_set, NULL);
+ assert_se(r >= 0);
+ assert_se(capability_bounding_set == (make_cap(CAP_NET_RAW) | make_cap(CAP_NET_ADMIN)));
+
+ r = config_parse_capability_set(NULL, "fake", 1, "section", 1,
+ "CapabilityBoundingSet", 0, "~CAP_NET_ADMIN",
+ &capability_bounding_set, NULL);
+ assert_se(r >= 0);
+ assert_se(capability_bounding_set == make_cap(CAP_NET_RAW));
+
+ r = config_parse_capability_set(NULL, "fake", 1, "section", 1,
+ "CapabilityBoundingSet", 0, "",
+ &capability_bounding_set, NULL);
+ assert_se(r >= 0);
+ assert_se(capability_bounding_set == UINT64_C(0));
+
+ r = config_parse_capability_set(NULL, "fake", 1, "section", 1,
+ "CapabilityBoundingSet", 0, "~",
+ &capability_bounding_set, NULL);
+ assert_se(r >= 0);
+ assert_se(cap_test_all(capability_bounding_set));
+
+ capability_bounding_set = 0;
+ r = config_parse_capability_set(NULL, "fake", 1, "section", 1,
+ "CapabilityBoundingSet", 0, " 'CAP_NET_RAW' WAT_CAP??? CAP_NET_ADMIN CAP'_trailing_garbage",
+ &capability_bounding_set, NULL);
+ assert_se(r >= 0);
+ assert_se(capability_bounding_set == (make_cap(CAP_NET_RAW) | make_cap(CAP_NET_ADMIN)));
+}
+
+TEST(config_parse_rlimit) {
+ struct rlimit * rl[_RLIMIT_MAX] = {};
+
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitNOFILE", RLIMIT_NOFILE, "55", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_NOFILE]);
+ assert_se(rl[RLIMIT_NOFILE]->rlim_cur == 55);
+ assert_se(rl[RLIMIT_NOFILE]->rlim_cur == rl[RLIMIT_NOFILE]->rlim_max);
+
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitNOFILE", RLIMIT_NOFILE, "55:66", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_NOFILE]);
+ assert_se(rl[RLIMIT_NOFILE]->rlim_cur == 55);
+ assert_se(rl[RLIMIT_NOFILE]->rlim_max == 66);
+
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitNOFILE", RLIMIT_NOFILE, "infinity", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_NOFILE]);
+ assert_se(rl[RLIMIT_NOFILE]->rlim_cur == RLIM_INFINITY);
+ assert_se(rl[RLIMIT_NOFILE]->rlim_cur == rl[RLIMIT_NOFILE]->rlim_max);
+
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitNOFILE", RLIMIT_NOFILE, "infinity:infinity", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_NOFILE]);
+ assert_se(rl[RLIMIT_NOFILE]->rlim_cur == RLIM_INFINITY);
+ assert_se(rl[RLIMIT_NOFILE]->rlim_cur == rl[RLIMIT_NOFILE]->rlim_max);
+
+ rl[RLIMIT_NOFILE]->rlim_cur = 10;
+ rl[RLIMIT_NOFILE]->rlim_max = 20;
+
+ /* Invalid values don't change rl */
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitNOFILE", RLIMIT_NOFILE, "10:20:30", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_NOFILE]);
+ assert_se(rl[RLIMIT_NOFILE]->rlim_cur == 10);
+ assert_se(rl[RLIMIT_NOFILE]->rlim_max == 20);
+
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitNOFILE", RLIMIT_NOFILE, "wat:wat", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_NOFILE]);
+ assert_se(rl[RLIMIT_NOFILE]->rlim_cur == 10);
+ assert_se(rl[RLIMIT_NOFILE]->rlim_max == 20);
+
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitNOFILE", RLIMIT_NOFILE, "66:wat", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_NOFILE]);
+ assert_se(rl[RLIMIT_NOFILE]->rlim_cur == 10);
+ assert_se(rl[RLIMIT_NOFILE]->rlim_max == 20);
+
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitNOFILE", RLIMIT_NOFILE, "200:100", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_NOFILE]);
+ assert_se(rl[RLIMIT_NOFILE]->rlim_cur == 10);
+ assert_se(rl[RLIMIT_NOFILE]->rlim_max == 20);
+
+ rl[RLIMIT_NOFILE] = mfree(rl[RLIMIT_NOFILE]);
+
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitCPU", RLIMIT_CPU, "56", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_CPU]);
+ assert_se(rl[RLIMIT_CPU]->rlim_cur == 56);
+ assert_se(rl[RLIMIT_CPU]->rlim_cur == rl[RLIMIT_CPU]->rlim_max);
+
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitCPU", RLIMIT_CPU, "57s", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_CPU]);
+ assert_se(rl[RLIMIT_CPU]->rlim_cur == 57);
+ assert_se(rl[RLIMIT_CPU]->rlim_cur == rl[RLIMIT_CPU]->rlim_max);
+
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitCPU", RLIMIT_CPU, "40s:1m", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_CPU]);
+ assert_se(rl[RLIMIT_CPU]->rlim_cur == 40);
+ assert_se(rl[RLIMIT_CPU]->rlim_max == 60);
+
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitCPU", RLIMIT_CPU, "infinity", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_CPU]);
+ assert_se(rl[RLIMIT_CPU]->rlim_cur == RLIM_INFINITY);
+ assert_se(rl[RLIMIT_CPU]->rlim_cur == rl[RLIMIT_CPU]->rlim_max);
+
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitCPU", RLIMIT_CPU, "1234ms", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_CPU]);
+ assert_se(rl[RLIMIT_CPU]->rlim_cur == 2);
+ assert_se(rl[RLIMIT_CPU]->rlim_cur == rl[RLIMIT_CPU]->rlim_max);
+
+ rl[RLIMIT_CPU] = mfree(rl[RLIMIT_CPU]);
+
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitRTTIME", RLIMIT_RTTIME, "58", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_RTTIME]);
+ assert_se(rl[RLIMIT_RTTIME]->rlim_cur == 58);
+ assert_se(rl[RLIMIT_RTTIME]->rlim_cur == rl[RLIMIT_RTTIME]->rlim_max);
+
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitRTTIME", RLIMIT_RTTIME, "58:60", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_RTTIME]);
+ assert_se(rl[RLIMIT_RTTIME]->rlim_cur == 58);
+ assert_se(rl[RLIMIT_RTTIME]->rlim_max == 60);
+
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitRTTIME", RLIMIT_RTTIME, "59s", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_RTTIME]);
+ assert_se(rl[RLIMIT_RTTIME]->rlim_cur == 59 * USEC_PER_SEC);
+ assert_se(rl[RLIMIT_RTTIME]->rlim_cur == rl[RLIMIT_RTTIME]->rlim_max);
+
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitRTTIME", RLIMIT_RTTIME, "59s:123s", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_RTTIME]);
+ assert_se(rl[RLIMIT_RTTIME]->rlim_cur == 59 * USEC_PER_SEC);
+ assert_se(rl[RLIMIT_RTTIME]->rlim_max == 123 * USEC_PER_SEC);
+
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitRTTIME", RLIMIT_RTTIME, "infinity", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_RTTIME]);
+ assert_se(rl[RLIMIT_RTTIME]->rlim_cur == RLIM_INFINITY);
+ assert_se(rl[RLIMIT_RTTIME]->rlim_cur == rl[RLIMIT_RTTIME]->rlim_max);
+
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitRTTIME", RLIMIT_RTTIME, "infinity:infinity", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_RTTIME]);
+ assert_se(rl[RLIMIT_RTTIME]->rlim_cur == RLIM_INFINITY);
+ assert_se(rl[RLIMIT_RTTIME]->rlim_cur == rl[RLIMIT_RTTIME]->rlim_max);
+
+ assert_se(config_parse_rlimit(NULL, "fake", 1, "section", 1, "LimitRTTIME", RLIMIT_RTTIME, "2345ms", rl, NULL) >= 0);
+ assert_se(rl[RLIMIT_RTTIME]);
+ assert_se(rl[RLIMIT_RTTIME]->rlim_cur == 2345 * USEC_PER_MSEC);
+ assert_se(rl[RLIMIT_RTTIME]->rlim_cur == rl[RLIMIT_RTTIME]->rlim_max);
+
+ rl[RLIMIT_RTTIME] = mfree(rl[RLIMIT_RTTIME]);
+}
+
+TEST(config_parse_pass_environ) {
+ /* int config_parse_pass_environ(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) */
+ int r;
+ _cleanup_strv_free_ char **passenv = NULL;
+
+ r = config_parse_pass_environ(NULL, "fake", 1, "section", 1,
+ "PassEnvironment", 0, "A B",
+ &passenv, NULL);
+ assert_se(r >= 0);
+ assert_se(strv_length(passenv) == 2);
+ assert_se(streq(passenv[0], "A"));
+ assert_se(streq(passenv[1], "B"));
+
+ r = config_parse_pass_environ(NULL, "fake", 1, "section", 1,
+ "PassEnvironment", 0, "",
+ &passenv, NULL);
+ assert_se(r >= 0);
+ assert_se(strv_isempty(passenv));
+
+ r = config_parse_pass_environ(NULL, "fake", 1, "section", 1,
+ "PassEnvironment", 0, "'invalid name' 'normal_name' A=1 'special_name$$' \\",
+ &passenv, NULL);
+ assert_se(r >= 0);
+ assert_se(strv_length(passenv) == 1);
+ assert_se(streq(passenv[0], "normal_name"));
+}
+
+TEST(config_parse_unit_env_file) {
+ /* int config_parse_unit_env_file(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) */
+
+ _cleanup_(manager_freep) Manager *m = NULL;
+ Unit *u;
+ _cleanup_strv_free_ char **files = NULL;
+ int r;
+
+ r = manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_MINIMAL, &m);
+ if (manager_errno_skip_test(r)) {
+ log_notice_errno(r, "Skipping test: manager_new: %m");
+ return;
+ }
+
+ assert_se(r >= 0);
+ assert_se(manager_startup(m, NULL, NULL, NULL) >= 0);
+
+ assert_se(u = unit_new(m, sizeof(Service)));
+ assert_se(unit_add_name(u, "foobar.service") == 0);
+
+ r = config_parse_unit_env_file(u->id, "fake", 1, "section", 1,
+ "EnvironmentFile", 0, "not-absolute",
+ &files, u);
+ assert_se(r == 0);
+ assert_se(strv_isempty(files));
+
+ r = config_parse_unit_env_file(u->id, "fake", 1, "section", 1,
+ "EnvironmentFile", 0, "/absolute1",
+ &files, u);
+ assert_se(r == 0);
+ assert_se(strv_length(files) == 1);
+
+ r = config_parse_unit_env_file(u->id, "fake", 1, "section", 1,
+ "EnvironmentFile", 0, "/absolute2",
+ &files, u);
+ assert_se(r == 0);
+ assert_se(strv_length(files) == 2);
+ assert_se(streq(files[0], "/absolute1"));
+ assert_se(streq(files[1], "/absolute2"));
+
+ r = config_parse_unit_env_file(u->id, "fake", 1, "section", 1,
+ "EnvironmentFile", 0, "",
+ &files, u);
+ assert_se(r == 0);
+ assert_se(strv_isempty(files));
+
+ r = config_parse_unit_env_file(u->id, "fake", 1, "section", 1,
+ "EnvironmentFile", 0, "/path/%n.conf",
+ &files, u);
+ assert_se(r == 0);
+ assert_se(strv_length(files) == 1);
+ assert_se(streq(files[0], "/path/foobar.service.conf"));
+}
+
+TEST(unit_dump_config_items) {
+ unit_dump_config_items(stdout);
+}
+
+TEST(config_parse_memory_limit) {
+ /* int config_parse_memory_limit(
+ const char *unit,
+ const char *filename,
+ unsigned line,
+ const char *section,
+ unsigned section_line,
+ const char *lvalue,
+ int ltype,
+ const char *rvalue,
+ void *data,
+ void *userdata) */
+ CGroupContext c;
+ struct limit_test {
+ const char *limit;
+ const char *value;
+ uint64_t *result;
+ uint64_t expected;
+ } limit_tests[]= {
+ { "MemoryMin", "", &c.memory_min, CGROUP_LIMIT_MIN },
+ { "MemoryMin", "0", &c.memory_min, CGROUP_LIMIT_MIN },
+ { "MemoryMin", "10", &c.memory_min, 10 },
+ { "MemoryMin", "infinity", &c.memory_min, CGROUP_LIMIT_MAX },
+ { "MemoryLow", "", &c.memory_low, CGROUP_LIMIT_MIN },
+ { "MemoryLow", "0", &c.memory_low, CGROUP_LIMIT_MIN },
+ { "MemoryLow", "10", &c.memory_low, 10 },
+ { "MemoryLow", "infinity", &c.memory_low, CGROUP_LIMIT_MAX },
+ { "MemoryHigh", "", &c.memory_high, CGROUP_LIMIT_MAX },
+ { "MemoryHigh", "0", &c.memory_high, CGROUP_LIMIT_DUMMY },
+ { "MemoryHigh", "10", &c.memory_high, 10 },
+ { "MemoryHigh", "infinity", &c.memory_high, CGROUP_LIMIT_MAX },
+ { "MemoryMax", "", &c.memory_max, CGROUP_LIMIT_MAX },
+ { "MemoryMax", "0", &c.memory_max, CGROUP_LIMIT_DUMMY },
+ { "MemoryMax", "10", &c.memory_max, 10 },
+ { "MemoryMax", "infinity", &c.memory_max, CGROUP_LIMIT_MAX },
+ };
+ size_t i;
+ int r;
+
+ for (i = 0; i < ELEMENTSOF(limit_tests); i++) {
+ c.memory_min = CGROUP_LIMIT_DUMMY;
+ c.memory_low = CGROUP_LIMIT_DUMMY;
+ c.memory_high = CGROUP_LIMIT_DUMMY;
+ c.memory_max = CGROUP_LIMIT_DUMMY;
+ r = config_parse_memory_limit(NULL, "fake", 1, "section", 1,
+ limit_tests[i].limit, 1,
+ limit_tests[i].value, &c, NULL);
+ log_info("%s=%s\t%"PRIu64"==%"PRIu64,
+ limit_tests[i].limit, limit_tests[i].value,
+ *limit_tests[i].result, limit_tests[i].expected);
+ assert_se(r >= 0);
+ assert_se(*limit_tests[i].result == limit_tests[i].expected);
+ }
+
+}
+
+TEST(contains_instance_specifier_superset) {
+ assert_se(contains_instance_specifier_superset("foobar@a%i"));
+ assert_se(contains_instance_specifier_superset("foobar@%ia"));
+ assert_se(contains_instance_specifier_superset("foobar@%n"));
+ assert_se(contains_instance_specifier_superset("foobar@%n.service"));
+ assert_se(contains_instance_specifier_superset("foobar@%N"));
+ assert_se(contains_instance_specifier_superset("foobar@%N.service"));
+ assert_se(contains_instance_specifier_superset("foobar@baz.%N.service"));
+ assert_se(contains_instance_specifier_superset("@%N.service"));
+ assert_se(contains_instance_specifier_superset("@%N"));
+ assert_se(contains_instance_specifier_superset("@%a%N"));
+
+ assert_se(!contains_instance_specifier_superset("foobar@%i.service"));
+ assert_se(!contains_instance_specifier_superset("foobar%ia.service"));
+ assert_se(!contains_instance_specifier_superset("foobar@%%n.service"));
+ assert_se(!contains_instance_specifier_superset("foobar@baz.service"));
+ assert_se(!contains_instance_specifier_superset("%N.service"));
+ assert_se(!contains_instance_specifier_superset("%N"));
+ assert_se(!contains_instance_specifier_superset("@%aN"));
+ assert_se(!contains_instance_specifier_superset("@%a%b"));
+}
+
+TEST(unit_is_recursive_template_dependency) {
+ _cleanup_(manager_freep) Manager *m = NULL;
+ Unit *u;
+ int r;
+
+ r = manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_MINIMAL, &m);
+ if (manager_errno_skip_test(r)) {
+ log_notice_errno(r, "Skipping test: manager_new: %m");
+ return;
+ }
+
+ assert_se(r >= 0);
+ assert_se(manager_startup(m, NULL, NULL, NULL) >= 0);
+
+ assert_se(u = unit_new(m, sizeof(Service)));
+ assert_se(unit_add_name(u, "foobar@1.service") == 0);
+ u->fragment_path = strdup("/foobar@.service");
+
+ assert_se(hashmap_put_strdup(&m->unit_id_map, "foobar@foobar@123.service", "/foobar@.service"));
+ assert_se(hashmap_put_strdup(&m->unit_id_map, "foobar@foobar@456.service", "/custom.service"));
+
+ /* Test that %n, %N and any extension of %i specifiers in the instance are detected as recursive. */
+ assert_se(unit_is_likely_recursive_template_dependency(u, "foobar@foobar@123.service", "foobar@%N.service") == 1);
+ assert_se(unit_is_likely_recursive_template_dependency(u, "foobar@foobar@123.service", "foobar@%n.service") == 1);
+ assert_se(unit_is_likely_recursive_template_dependency(u, "foobar@foobar@123.service", "foobar@a%i.service") == 1);
+ assert_se(unit_is_likely_recursive_template_dependency(u, "foobar@foobar@123.service", "foobar@%ia.service") == 1);
+ assert_se(unit_is_likely_recursive_template_dependency(u, "foobar@foobar@123.service", "foobar@%x%n.service") == 1);
+ /* Test that %i on its own is not detected as recursive. */
+ assert_se(unit_is_likely_recursive_template_dependency(u, "foobar@foobar@123.service", "foobar@%i.service") == 0);
+ /* Test that a specifier other than %i, %n and %N is not detected as recursive. */
+ assert_se(unit_is_likely_recursive_template_dependency(u, "foobar@foobar@123.service", "foobar@%xn.service") == 0);
+ /* Test that an expanded specifier is not detected as recursive. */
+ assert_se(unit_is_likely_recursive_template_dependency(u, "foobar@foobar@123.service", "foobar@foobar@123.service") == 0);
+ /* Test that a dependency with a custom fragment path is not detected as recursive. */
+ assert_se(unit_is_likely_recursive_template_dependency(u, "foobar@foobar@456.service", "foobar@%n.service") == 0);
+ /* Test that a dependency without a fragment path is not detected as recursive. */
+ assert_se(unit_is_likely_recursive_template_dependency(u, "foobar@foobar@789.service", "foobar@%n.service") == 0);
+ /* Test that a dependency with a different prefix is not detected as recursive. */
+ assert_se(unit_is_likely_recursive_template_dependency(u, "quux@foobar@123.service", "quux@%n.service") == 0);
+ /* Test that a dependency of a different type is not detected as recursive. */
+ assert_se(unit_is_likely_recursive_template_dependency(u, "foobar@foobar@123.mount", "foobar@%n.mount") == 0);
+}
+
+#define TEST_PATTERN(_regex, _allowed_patterns_count, _denied_patterns_count) \
+ { \
+ .regex = _regex, \
+ .allowed_patterns_count = _allowed_patterns_count, \
+ .denied_patterns_count = _denied_patterns_count \
+ }
+
+TEST(config_parse_log_filter_patterns) {
+ ExecContext c = {};
+
+ static const struct {
+ const char *regex;
+ size_t allowed_patterns_count;
+ size_t denied_patterns_count;
+ } regex_tests[] = {
+ TEST_PATTERN("", 0, 0),
+ TEST_PATTERN(".*", 1, 0),
+ TEST_PATTERN("~.*", 1, 1),
+ TEST_PATTERN("", 0, 0),
+ TEST_PATTERN("~.*", 0, 1),
+ TEST_PATTERN("[.*", 0, 1), /* Invalid pattern. */
+ TEST_PATTERN(".*gg.*", 1, 1),
+ TEST_PATTERN("~.*", 1, 1), /* Already in the patterns list. */
+ TEST_PATTERN("[.*", 1, 1), /* Invalid pattern. */
+ TEST_PATTERN("\\x7ehello", 2, 1),
+ TEST_PATTERN("", 0, 0),
+ TEST_PATTERN("~foobar", 0, 1),
+ };
+
+ if (ERRNO_IS_NOT_SUPPORTED(dlopen_pcre2()))
+ return (void) log_tests_skipped("PCRE2 support is not available");
+
+ for (size_t i = 0; i < ELEMENTSOF(regex_tests); i++) {
+ assert_se(config_parse_log_filter_patterns(NULL, "fake", 1, "section", 1, "LogFilterPatterns", 1,
+ regex_tests[i].regex, &c, NULL) >= 0);
+
+ assert_se(set_size(c.log_filter_allowed_patterns) == regex_tests[i].allowed_patterns_count);
+ assert_se(set_size(c.log_filter_denied_patterns) == regex_tests[i].denied_patterns_count);
+
+ /* Ensure `~` is properly removed */
+ const char *p;
+ SET_FOREACH(p, c.log_filter_allowed_patterns)
+ assert_se(p && p[0] != '~');
+ SET_FOREACH(p, c.log_filter_denied_patterns)
+ assert_se(p && p[0] != '~');
+ }
+
+ exec_context_done(&c);
+}
+
+TEST(config_parse_open_file) {
+ _cleanup_(manager_freep) Manager *m = NULL;
+ _cleanup_(unit_freep) Unit *u = NULL;
+ _cleanup_(open_file_freep) OpenFile *of = NULL;
+ int r;
+
+ r = manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_MINIMAL, &m);
+ if (manager_errno_skip_test(r)) {
+ log_notice_errno(r, "Skipping test: manager_new: %m");
+ return;
+ }
+
+ assert_se(r >= 0);
+ assert_se(manager_startup(m, NULL, NULL, NULL) >= 0);
+
+ assert_se(u = unit_new(m, sizeof(Service)));
+ assert_se(unit_add_name(u, "foobar.service") == 0);
+
+ r = config_parse_open_file(NULL, "fake", 1, "section", 1,
+ "OpenFile", 0, "/proc/1/ns/mnt:host-mount-namespace:read-only",
+ &of, u);
+ assert_se(r >= 0);
+ assert_se(of);
+ assert_se(streq(of->path, "/proc/1/ns/mnt"));
+ assert_se(streq(of->fdname, "host-mount-namespace"));
+ assert_se(of->flags == OPENFILE_READ_ONLY);
+
+ of = open_file_free(of);
+ r = config_parse_open_file(NULL, "fake", 1, "section", 1,
+ "OpenFile", 0, "/proc/1/ns/mnt::read-only",
+ &of, u);
+ assert_se(r >= 0);
+ assert_se(of);
+ assert_se(streq(of->path, "/proc/1/ns/mnt"));
+ assert_se(streq(of->fdname, "mnt"));
+ assert_se(of->flags == OPENFILE_READ_ONLY);
+
+ r = config_parse_open_file(NULL, "fake", 1, "section", 1,
+ "OpenFile", 0, "",
+ &of, u);
+ assert_se(r >= 0);
+ assert_se(!of);
+}
+
+static int intro(void) {
+ if (enter_cgroup_subroot(NULL) == -ENOMEDIUM)
+ return log_tests_skipped("cgroupfs not available");
+
+ assert_se(runtime_dir = setup_fake_runtime_dir());
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_INFO, intro);
diff --git a/src/test/test-local-addresses.c b/src/test/test-local-addresses.c
new file mode 100644
index 0000000..5a02465
--- /dev/null
+++ b/src/test/test-local-addresses.c
@@ -0,0 +1,73 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+
+#include "af-list.h"
+#include "alloc-util.h"
+#include "in-addr-util.h"
+#include "local-addresses.h"
+#include "tests.h"
+
+static void print_local_addresses(struct local_address *a, unsigned n) {
+ for (unsigned i = 0; i < n; i++) {
+ _cleanup_free_ char *b = NULL;
+
+ assert_se(in_addr_to_string(a[i].family, &a[i].address, &b) >= 0);
+ log_debug("%s if%i scope=%i metric=%u address=%s", af_to_name(a[i].family), a[i].ifindex, a[i].scope, a[i].metric, b);
+ }
+}
+
+TEST(local_addresses) {
+ struct local_address *a = NULL;
+ int n;
+
+ n = local_addresses(NULL, 0, AF_INET, &a);
+ assert_se(n >= 0);
+ log_debug("/* Local Addresses(ifindex:0, AF_INET) */");
+ print_local_addresses(a, (unsigned) n);
+ a = mfree(a);
+
+ n = local_addresses(NULL, 0, AF_INET6, &a);
+ assert_se(n >= 0);
+ log_debug("/* Local Addresses(ifindex:0, AF_INET6) */");
+ print_local_addresses(a, (unsigned) n);
+ a = mfree(a);
+
+ n = local_addresses(NULL, 0, AF_UNSPEC, &a);
+ assert_se(n >= 0);
+ log_debug("/* Local Addresses(ifindex:0, AF_UNSPEC) */");
+ print_local_addresses(a, (unsigned) n);
+ a = mfree(a);
+
+ n = local_addresses(NULL, 1, AF_INET, &a);
+ assert_se(n >= 0);
+ log_debug("/* Local Addresses(ifindex:1, AF_INET) */");
+ print_local_addresses(a, (unsigned) n);
+ a = mfree(a);
+
+ n = local_addresses(NULL, 1, AF_INET6, &a);
+ assert_se(n >= 0);
+ log_debug("/* Local Addresses(ifindex:1, AF_INET6) */");
+ print_local_addresses(a, (unsigned) n);
+ a = mfree(a);
+
+ n = local_addresses(NULL, 1, AF_UNSPEC, &a);
+ assert_se(n >= 0);
+ log_debug("/* Local Addresses(ifindex:1, AF_UNSPEC) */");
+ print_local_addresses(a, (unsigned) n);
+ a = mfree(a);
+
+ n = local_gateways(NULL, 0, AF_UNSPEC, &a);
+ assert_se(n >= 0);
+ log_debug("/* Local Gateways */");
+ print_local_addresses(a, (unsigned) n);
+ a = mfree(a);
+
+ n = local_outbounds(NULL, 0, AF_UNSPEC, &a);
+ assert_se(n >= 0);
+ log_debug("/* Local Outbounds */");
+ print_local_addresses(a, (unsigned) n);
+ free(a);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-locale-util.c b/src/test/test-locale-util.c
new file mode 100644
index 0000000..39f71c6
--- /dev/null
+++ b/src/test/test-locale-util.c
@@ -0,0 +1,132 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "glyph-util.h"
+#include "kbd-util.h"
+#include "locale-util.h"
+#include "macro.h"
+#include "strv.h"
+#include "tests.h"
+
+TEST(get_locales) {
+ _cleanup_strv_free_ char **locales = NULL;
+ int r;
+
+ r = get_locales(&locales);
+ assert_se(r >= 0);
+ assert_se(locales);
+
+ STRV_FOREACH(p, locales) {
+ puts(*p);
+ assert_se(locale_is_valid(*p));
+ }
+}
+
+TEST(locale_is_valid) {
+ assert_se(locale_is_valid("en_EN.utf8"));
+ assert_se(locale_is_valid("fr_FR.utf8"));
+ assert_se(locale_is_valid("fr_FR@euro"));
+ assert_se(locale_is_valid("fi_FI"));
+ assert_se(locale_is_valid("POSIX"));
+ assert_se(locale_is_valid("C"));
+
+ assert_se(!locale_is_valid(""));
+ assert_se(!locale_is_valid("/usr/bin/foo"));
+ assert_se(!locale_is_valid("\x01gar\x02 bage\x03"));
+}
+
+TEST(locale_is_installed) {
+ /* Always available */
+ assert_se(locale_is_installed("POSIX") > 0);
+ assert_se(locale_is_installed("C") > 0);
+
+ /* Might, or might not be installed. */
+ assert_se(locale_is_installed("en_EN.utf8") >= 0);
+ assert_se(locale_is_installed("fr_FR.utf8") >= 0);
+ assert_se(locale_is_installed("fr_FR@euro") >= 0);
+ assert_se(locale_is_installed("fi_FI") >= 0);
+
+ /* Definitely not valid */
+ assert_se(locale_is_installed("") == 0);
+ assert_se(locale_is_installed("/usr/bin/foo") == 0);
+ assert_se(locale_is_installed("\x01gar\x02 bage\x03") == 0);
+
+ /* Definitely not installed */
+ assert_se(locale_is_installed("zz_ZZ") == 0);
+}
+
+TEST(keymaps) {
+ _cleanup_strv_free_ char **kmaps = NULL;
+ int r;
+
+ assert_se(!keymap_is_valid(""));
+ assert_se(!keymap_is_valid("/usr/bin/foo"));
+ assert_se(!keymap_is_valid("\x01gar\x02 bage\x03"));
+
+ r = get_keymaps(&kmaps);
+ if (r == -ENOENT)
+ return; /* skip test if no keymaps are installed */
+
+ assert_se(r >= 0);
+ assert_se(kmaps);
+
+ STRV_FOREACH(p, kmaps) {
+ puts(*p);
+ assert_se(keymap_is_valid(*p));
+ }
+
+ assert_se(keymap_is_valid("uk"));
+ assert_se(keymap_is_valid("de-nodeadkeys"));
+ assert_se(keymap_is_valid("ANSI-dvorak"));
+ assert_se(keymap_is_valid("unicode"));
+}
+
+#define dump_glyph(x) log_info(STRINGIFY(x) ": %s", special_glyph(x))
+TEST(dump_special_glyphs) {
+ assert_cc(SPECIAL_GLYPH_WORLD + 1 == _SPECIAL_GLYPH_MAX);
+
+ log_info("is_locale_utf8: %s", yes_no(is_locale_utf8()));
+
+ dump_glyph(SPECIAL_GLYPH_TREE_VERTICAL);
+ dump_glyph(SPECIAL_GLYPH_TREE_BRANCH);
+ dump_glyph(SPECIAL_GLYPH_TREE_RIGHT);
+ dump_glyph(SPECIAL_GLYPH_TREE_SPACE);
+ dump_glyph(SPECIAL_GLYPH_TREE_TOP);
+ dump_glyph(SPECIAL_GLYPH_VERTICAL_DOTTED);
+ dump_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET);
+ dump_glyph(SPECIAL_GLYPH_BLACK_CIRCLE);
+ dump_glyph(SPECIAL_GLYPH_WHITE_CIRCLE);
+ dump_glyph(SPECIAL_GLYPH_MULTIPLICATION_SIGN);
+ dump_glyph(SPECIAL_GLYPH_CIRCLE_ARROW);
+ dump_glyph(SPECIAL_GLYPH_BULLET);
+ dump_glyph(SPECIAL_GLYPH_MU);
+ dump_glyph(SPECIAL_GLYPH_CHECK_MARK);
+ dump_glyph(SPECIAL_GLYPH_CROSS_MARK);
+ dump_glyph(SPECIAL_GLYPH_LIGHT_SHADE);
+ dump_glyph(SPECIAL_GLYPH_DARK_SHADE);
+ dump_glyph(SPECIAL_GLYPH_FULL_BLOCK);
+ dump_glyph(SPECIAL_GLYPH_SIGMA);
+ dump_glyph(SPECIAL_GLYPH_ARROW_UP);
+ dump_glyph(SPECIAL_GLYPH_ARROW_DOWN);
+ dump_glyph(SPECIAL_GLYPH_ARROW_LEFT);
+ dump_glyph(SPECIAL_GLYPH_ARROW_RIGHT);
+ dump_glyph(SPECIAL_GLYPH_ELLIPSIS);
+ dump_glyph(SPECIAL_GLYPH_EXTERNAL_LINK);
+ dump_glyph(SPECIAL_GLYPH_ECSTATIC_SMILEY);
+ dump_glyph(SPECIAL_GLYPH_HAPPY_SMILEY);
+ dump_glyph(SPECIAL_GLYPH_SLIGHTLY_HAPPY_SMILEY);
+ dump_glyph(SPECIAL_GLYPH_NEUTRAL_SMILEY);
+ dump_glyph(SPECIAL_GLYPH_SLIGHTLY_UNHAPPY_SMILEY);
+ dump_glyph(SPECIAL_GLYPH_UNHAPPY_SMILEY);
+ dump_glyph(SPECIAL_GLYPH_DEPRESSED_SMILEY);
+ dump_glyph(SPECIAL_GLYPH_LOCK_AND_KEY);
+ dump_glyph(SPECIAL_GLYPH_TOUCH);
+ dump_glyph(SPECIAL_GLYPH_RECYCLING);
+ dump_glyph(SPECIAL_GLYPH_DOWNLOAD);
+ dump_glyph(SPECIAL_GLYPH_SPARKLES);
+ dump_glyph(SPECIAL_GLYPH_LOW_BATTERY);
+ dump_glyph(SPECIAL_GLYPH_WARNING_SIGN);
+ dump_glyph(SPECIAL_GLYPH_COMPUTER_DISK);
+ dump_glyph(SPECIAL_GLYPH_WORLD);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-lock-util.c b/src/test/test-lock-util.c
new file mode 100644
index 0000000..5edd087
--- /dev/null
+++ b/src/test/test-lock-util.c
@@ -0,0 +1,65 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/file.h>
+#include <unistd.h>
+
+#include "fd-util.h"
+#include "lock-util.h"
+#include "rm-rf.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+TEST(make_lock_file) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ _cleanup_close_ int tfd = -EBADF;
+ _cleanup_(release_lock_file) LockFile lock1 = LOCK_FILE_INIT, lock2 = LOCK_FILE_INIT;
+
+ assert_se((tfd = mkdtemp_open(NULL, 0, &t)) >= 0);
+
+ assert_se(make_lock_file_at(tfd, "lock", LOCK_EX, &lock1) >= 0);
+ assert_se(faccessat(tfd, "lock", F_OK, 0) >= 0);
+ assert_se(make_lock_file_at(tfd, "lock", LOCK_EX|LOCK_NB, &lock2) == -EBUSY);
+ release_lock_file(&lock1);
+ assert_se(RET_NERRNO(faccessat(tfd, "lock", F_OK, 0)) == -ENOENT);
+ assert_se(make_lock_file_at(tfd, "lock", LOCK_EX, &lock2) >= 0);
+ release_lock_file(&lock2);
+ assert_se(make_lock_file_at(tfd, "lock", LOCK_SH, &lock1) >= 0);
+ assert_se(faccessat(tfd, "lock", F_OK, 0) >= 0);
+ assert_se(make_lock_file_at(tfd, "lock", LOCK_SH, &lock2) >= 0);
+ release_lock_file(&lock1);
+ assert_se(faccessat(tfd, "lock", F_OK, 0) >= 0);
+ release_lock_file(&lock2);
+
+ assert_se(fchdir(tfd) >= 0);
+ assert_se(make_lock_file_at(tfd, "lock", LOCK_EX, &lock1) >= 0);
+ assert_se(make_lock_file("lock", LOCK_EX|LOCK_NB, &lock2) == -EBUSY);
+}
+
+static void test_lock_generic_with_timeout_for_type(LockType type) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ _cleanup_close_ int tfd = -EBADF, tfd2 = -EBADF;
+
+ tfd = mkdtemp_open(NULL, 0, &t);
+ assert_se(tfd >= 0);
+
+ tfd2 = fd_reopen(tfd, O_CLOEXEC|O_DIRECTORY);
+ assert_se(tfd2 >= 0);
+
+ assert_se(lock_generic(tfd, LOCK_BSD, LOCK_EX) >= 0);
+ assert_se(lock_generic(tfd2, LOCK_BSD, LOCK_EX|LOCK_NB) == -EWOULDBLOCK);
+
+ usec_t start = now(CLOCK_MONOTONIC);
+ assert_se(lock_generic_with_timeout(tfd2, LOCK_BSD, LOCK_EX, 200 * USEC_PER_MSEC) == -ETIMEDOUT);
+ assert_se(usec_sub_unsigned(now(CLOCK_MONOTONIC), start) >= 200 * USEC_PER_MSEC);
+
+ assert_se(lock_generic(tfd, LOCK_BSD, LOCK_UN) >= 0);
+ assert_se(lock_generic_with_timeout(tfd2, LOCK_BSD, LOCK_EX, 200 * USEC_PER_MSEC) == 0);
+ assert_se(lock_generic(tfd, LOCK_BSD, LOCK_EX|LOCK_NB) == -EWOULDBLOCK);
+}
+
+TEST(lock_generic_with_timeout) {
+ test_lock_generic_with_timeout_for_type(LOCK_BSD);
+ test_lock_generic_with_timeout_for_type(LOCK_UNPOSIX);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-log.c b/src/test/test-log.c
new file mode 100644
index 0000000..b5ba67b
--- /dev/null
+++ b/src/test/test-log.c
@@ -0,0 +1,226 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stddef.h>
+#include <unistd.h>
+
+#include "format-util.h"
+#include "io-util.h"
+#include "iovec-util.h"
+#include "iovec-wrapper.h"
+#include "log.h"
+#include "process-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+
+assert_cc(IS_SYNTHETIC_ERRNO(SYNTHETIC_ERRNO(EINVAL)));
+assert_cc(!IS_SYNTHETIC_ERRNO(EINVAL));
+assert_cc(IS_SYNTHETIC_ERRNO(SYNTHETIC_ERRNO(0)));
+assert_cc(!IS_SYNTHETIC_ERRNO(0));
+
+#define X10(x) x x x x x x x x x x
+#define X100(x) X10(X10(x))
+#define X1000(x) X100(X10(x))
+
+static void test_file(void) {
+ log_info("__FILE__: %s", __FILE__);
+ log_info("RELATIVE_SOURCE_PATH: %s", RELATIVE_SOURCE_PATH);
+ log_info("PROJECT_FILE: %s", PROJECT_FILE);
+
+ assert_se(startswith(__FILE__, RELATIVE_SOURCE_PATH "/"));
+}
+
+static void test_log_struct(void) {
+ log_struct(LOG_INFO,
+ "MESSAGE=Waldo PID="PID_FMT" (no errno)", getpid_cached(),
+ "SERVICE=piepapo");
+
+ /* The same as above, just using LOG_MESSAGE(), which is generally recommended */
+ log_struct(LOG_INFO,
+ LOG_MESSAGE("Waldo PID="PID_FMT" (no errno)", getpid_cached()),
+ "SERVICE=piepapo");
+
+ log_struct_errno(LOG_INFO, EILSEQ,
+ LOG_MESSAGE("Waldo PID="PID_FMT": %m (normal)", getpid_cached()),
+ "SERVICE=piepapo");
+
+ log_struct_errno(LOG_INFO, SYNTHETIC_ERRNO(EILSEQ),
+ LOG_MESSAGE("Waldo PID="PID_FMT": %m (synthetic)", getpid_cached()),
+ "SERVICE=piepapo");
+
+ log_struct(LOG_INFO,
+ LOG_MESSAGE("Foobar PID="PID_FMT, getpid_cached()),
+ "FORMAT_STR_TEST=1=%i A=%c 2=%hi 3=%li 4=%lli 1=%p foo=%s 2.5=%g 3.5=%g 4.5=%Lg",
+ (int) 1, 'A', (short) 2, (long int) 3, (long long int) 4, (void*) 1, "foo", (float) 2.5f, (double) 3.5, (long double) 4.5,
+ "SUFFIX=GOT IT");
+}
+
+static void test_long_lines(void) {
+ log_object_internal(LOG_NOTICE,
+ EUCLEAN,
+ X1000("abcd_") ".txt",
+ 1000000,
+ X1000("fff") "unc",
+ "OBJECT=",
+ X1000("obj_") "ect",
+ "EXTRA=",
+ X1000("ext_") "tra",
+ "asdfasdf %s asdfasdfa", "foobar");
+}
+
+static void test_log_syntax(void) {
+ assert_se(log_syntax("unit", LOG_ERR, "filename", 10, EINVAL, "EINVAL: %s: %m", "hogehoge") == -EINVAL);
+ assert_se(log_syntax("unit", LOG_ERR, "filename", 10, -ENOENT, "ENOENT: %s: %m", "hogehoge") == -ENOENT);
+ assert_se(log_syntax("unit", LOG_ERR, "filename", 10, SYNTHETIC_ERRNO(ENOTTY), "ENOTTY: %s: %m", "hogehoge") == -ENOTTY);
+}
+
+static void test_log_context(void) {
+ {
+ char **strv = STRV_MAKE("FIRST=abc", "SECOND=qrs");
+
+ LOG_CONTEXT_PUSH("THIRD=pfs");
+ LOG_CONTEXT_PUSH("FOURTH=def");
+ LOG_CONTEXT_PUSH_STRV(strv);
+ LOG_CONTEXT_PUSH_STRV(strv);
+
+ /* Test that the log context was set up correctly. The strv we pushed twice should only
+ * result in one log context which is reused. */
+ assert_se(log_context_num_contexts() == 3);
+ assert_se(log_context_num_fields() == 4);
+
+ /* Test that everything still works with modifications to the log context. */
+ test_log_struct();
+ test_long_lines();
+ test_log_syntax();
+
+ {
+ LOG_CONTEXT_PUSH("FIFTH=123");
+ LOG_CONTEXT_PUSH_STRV(strv);
+
+ /* Check that our nested fields got added correctly. */
+ assert_se(log_context_num_contexts() == 4);
+ assert_se(log_context_num_fields() == 5);
+
+ /* Test that everything still works in a nested block. */
+ test_log_struct();
+ test_long_lines();
+ test_log_syntax();
+ }
+
+ /* Check that only the fields from the nested block got removed. */
+ assert_se(log_context_num_contexts() == 3);
+ assert_se(log_context_num_fields() == 4);
+ }
+
+ assert_se(log_context_num_contexts() == 0);
+ assert_se(log_context_num_fields() == 0);
+
+ {
+ _cleanup_(log_context_unrefp) LogContext *ctx = NULL;
+
+ char **strv = STRV_MAKE("SIXTH=ijn", "SEVENTH=PRP");
+ assert_se(ctx = log_context_new_strv(strv, /*owned=*/ false));
+
+ assert_se(log_context_num_contexts() == 1);
+ assert_se(log_context_num_fields() == 2);
+
+ /* Test that everything still works with a manually configured log context. */
+ test_log_struct();
+ test_long_lines();
+ test_log_syntax();
+ }
+
+ {
+ char **strv = NULL;
+
+ assert_se(strv = strv_new("ABC", "DEF"));
+ LOG_CONTEXT_CONSUME_STRV(strv);
+
+ assert_se(log_context_num_contexts() == 1);
+ assert_se(log_context_num_fields() == 2);
+ }
+
+ {
+ /* Test that everything still works with a mixed strv and iov. */
+ struct iovec iov[] = {
+ IOVEC_MAKE_STRING("ABC=def"),
+ IOVEC_MAKE_STRING("GHI=jkl"),
+ };
+ _cleanup_free_ struct iovec_wrapper *iovw = iovw_new();
+ assert_se(iovw);
+ assert_se(iovw_consume(iovw, strdup("MNO=pqr"), STRLEN("MNO=pqr") + 1) == 0);
+
+ LOG_CONTEXT_PUSH_IOV(iov, ELEMENTSOF(iov));
+ LOG_CONTEXT_PUSH_IOV(iov, ELEMENTSOF(iov));
+ LOG_CONTEXT_CONSUME_IOV(iovw->iovec, iovw->count);
+ LOG_CONTEXT_PUSH("STU=vwx");
+
+ assert_se(log_context_num_contexts() == 3);
+ assert_se(log_context_num_fields() == 4);
+
+ test_log_struct();
+ test_long_lines();
+ test_log_syntax();
+ }
+
+ {
+ LOG_CONTEXT_PUSH_KEY_VALUE("ABC=", "QED");
+ LOG_CONTEXT_PUSH_KEY_VALUE("ABC=", "QED");
+ assert_se(log_context_num_contexts() == 1);
+ assert_se(log_context_num_fields() == 1);
+
+ test_log_struct();
+ test_long_lines();
+ test_log_syntax();
+ }
+
+ assert_se(log_context_num_contexts() == 0);
+ assert_se(log_context_num_fields() == 0);
+}
+
+static void test_log_prefix(void) {
+ {
+ LOG_SET_PREFIX("ABC");
+
+ test_log_struct();
+ test_long_lines();
+ test_log_syntax();
+
+ {
+ LOG_SET_PREFIX("QED");
+
+ test_log_struct();
+ test_long_lines();
+ test_log_syntax();
+ }
+
+ test_log_struct();
+ test_long_lines();
+ test_log_syntax();
+ }
+
+ test_log_struct();
+ test_long_lines();
+ test_log_syntax();
+}
+
+int main(int argc, char* argv[]) {
+ test_setup_logging(LOG_DEBUG);
+
+ test_file();
+
+ assert_se(log_info_errno(SYNTHETIC_ERRNO(EUCLEAN), "foo") == -EUCLEAN);
+
+ for (int target = 0; target < _LOG_TARGET_MAX; target++) {
+ log_set_target(target);
+ log_open();
+
+ test_log_struct();
+ test_long_lines();
+ test_log_syntax();
+ test_log_context();
+ test_log_prefix();
+ }
+
+ return 0;
+}
diff --git a/src/test/test-logarithm.c b/src/test/test-logarithm.c
new file mode 100644
index 0000000..b35fea9
--- /dev/null
+++ b/src/test/test-logarithm.c
@@ -0,0 +1,95 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "logarithm.h"
+#include "tests.h"
+
+TEST(LOG2ULL) {
+ assert_se(LOG2ULL(0) == 0);
+ assert_se(LOG2ULL(1) == 0);
+ assert_se(LOG2ULL(8) == 3);
+ assert_se(LOG2ULL(9) == 3);
+ assert_se(LOG2ULL(15) == 3);
+ assert_se(LOG2ULL(16) == 4);
+ assert_se(LOG2ULL(1024*1024) == 20);
+ assert_se(LOG2ULL(1024*1024+5) == 20);
+}
+
+TEST(CONST_LOG2ULL) {
+ assert_se(CONST_LOG2ULL(0) == 0);
+ assert_se(CONST_LOG2ULL(1) == 0);
+ assert_se(CONST_LOG2ULL(8) == 3);
+ assert_se(CONST_LOG2ULL(9) == 3);
+ assert_se(CONST_LOG2ULL(15) == 3);
+ assert_se(CONST_LOG2ULL(16) == 4);
+ assert_se(CONST_LOG2ULL(1024*1024) == 20);
+ assert_se(CONST_LOG2ULL(1024*1024+5) == 20);
+}
+
+TEST(NONCONST_LOG2ULL) {
+ assert_se(NONCONST_LOG2ULL(0) == 0);
+ assert_se(NONCONST_LOG2ULL(1) == 0);
+ assert_se(NONCONST_LOG2ULL(8) == 3);
+ assert_se(NONCONST_LOG2ULL(9) == 3);
+ assert_se(NONCONST_LOG2ULL(15) == 3);
+ assert_se(NONCONST_LOG2ULL(16) == 4);
+ assert_se(NONCONST_LOG2ULL(1024*1024) == 20);
+ assert_se(NONCONST_LOG2ULL(1024*1024+5) == 20);
+}
+
+TEST(log2u64) {
+ assert_se(log2u64(0) == 0);
+ assert_se(log2u64(1) == 0);
+ assert_se(log2u64(8) == 3);
+ assert_se(log2u64(9) == 3);
+ assert_se(log2u64(15) == 3);
+ assert_se(log2u64(16) == 4);
+ assert_se(log2u64(1024*1024) == 20);
+ assert_se(log2u64(1024*1024+5) == 20);
+}
+
+TEST(log2u) {
+ assert_se(log2u(0) == 0);
+ assert_se(log2u(1) == 0);
+ assert_se(log2u(2) == 1);
+ assert_se(log2u(3) == 1);
+ assert_se(log2u(4) == 2);
+ assert_se(log2u(32) == 5);
+ assert_se(log2u(33) == 5);
+ assert_se(log2u(63) == 5);
+ assert_se(log2u(INT_MAX) == sizeof(int)*8-2);
+}
+
+TEST(log2i) {
+ assert_se(log2i(0) == 0);
+ assert_se(log2i(1) == 0);
+ assert_se(log2i(2) == 1);
+ assert_se(log2i(3) == 1);
+ assert_se(log2i(4) == 2);
+ assert_se(log2i(32) == 5);
+ assert_se(log2i(33) == 5);
+ assert_se(log2i(63) == 5);
+ assert_se(log2i(INT_MAX) == sizeof(int)*8-2);
+}
+
+TEST(popcount) {
+ uint16_t u16a = 0x0000;
+ uint16_t u16b = 0xFFFF;
+ uint32_t u32a = 0x00000010;
+ uint32_t u32b = 0xFFFFFFFF;
+ uint64_t u64a = 0x0000000000000010;
+ uint64_t u64b = 0x0100000000100010;
+
+ assert_se(popcount(u16a) == 0);
+ assert_se(popcount(u16b) == 16);
+ assert_se(popcount(u32a) == 1);
+ assert_se(popcount(u32b) == 32);
+ assert_se(popcount(u64a) == 1);
+ assert_se(popcount(u64b) == 3);
+
+ /* This would fail:
+ * error: ‘_Generic’ selector of type ‘int’ is not compatible with any association
+ * assert_se(popcount(0x10) == 1);
+ */
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-loop-block.c b/src/test/test-loop-block.c
new file mode 100644
index 0000000..1bd00d1
--- /dev/null
+++ b/src/test/test-loop-block.c
@@ -0,0 +1,350 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <linux/loop.h>
+#include <pthread.h>
+#include <sys/file.h>
+#include <sys/ioctl.h>
+#include <sys/mount.h>
+
+#include "alloc-util.h"
+#include "capability-util.h"
+#include "dissect-image.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "gpt.h"
+#include "main-func.h"
+#include "missing_loop.h"
+#include "mkfs-util.h"
+#include "mount-util.h"
+#include "namespace-util.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+#include "user-util.h"
+#include "virt.h"
+
+static unsigned arg_n_threads = 5;
+static unsigned arg_n_iterations = 3;
+static usec_t arg_timeout = 0;
+
+#if HAVE_BLKID
+static usec_t end = 0;
+
+static void verify_dissected_image(DissectedImage *dissected) {
+ assert_se(dissected->partitions[PARTITION_ESP].found);
+ assert_se(dissected->partitions[PARTITION_ESP].node);
+ assert_se(dissected->partitions[PARTITION_XBOOTLDR].found);
+ assert_se(dissected->partitions[PARTITION_XBOOTLDR].node);
+ assert_se(dissected->partitions[PARTITION_ROOT].found);
+ assert_se(dissected->partitions[PARTITION_ROOT].node);
+ assert_se(dissected->partitions[PARTITION_HOME].found);
+ assert_se(dissected->partitions[PARTITION_HOME].node);
+}
+
+static void verify_dissected_image_harder(DissectedImage *dissected) {
+ verify_dissected_image(dissected);
+
+ assert_se(streq(dissected->partitions[PARTITION_ESP].fstype, "vfat"));
+ assert_se(streq(dissected->partitions[PARTITION_XBOOTLDR].fstype, "vfat"));
+ assert_se(streq(dissected->partitions[PARTITION_ROOT].fstype, "ext4"));
+ assert_se(streq(dissected->partitions[PARTITION_HOME].fstype, "ext4"));
+}
+
+static void* thread_func(void *ptr) {
+ int fd = PTR_TO_FD(ptr);
+ int r;
+
+ for (unsigned i = 0; i < arg_n_iterations; i++) {
+ _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL;
+ _cleanup_(umount_and_rmdir_and_freep) char *mounted = NULL;
+ _cleanup_(dissected_image_unrefp) DissectedImage *dissected = NULL;
+
+ if (now(CLOCK_MONOTONIC) >= end) {
+ log_notice("Time's up, exiting thread's loop");
+ break;
+ }
+
+ log_notice("> Thread iteration #%u.", i);
+
+ assert_se(mkdtemp_malloc(NULL, &mounted) >= 0);
+
+ r = loop_device_make(fd, O_RDONLY, 0, UINT64_MAX, 0, LO_FLAGS_PARTSCAN, LOCK_SH, &loop);
+ if (r < 0)
+ log_error_errno(r, "Failed to allocate loopback device: %m");
+ assert_se(r >= 0);
+ assert_se(loop->dev);
+ assert_se(loop->backing_file);
+
+ log_notice("Acquired loop device %s, will mount on %s", loop->node, mounted);
+
+ r = dissect_loop_device(loop, NULL, NULL, NULL, DISSECT_IMAGE_READ_ONLY|DISSECT_IMAGE_ADD_PARTITION_DEVICES|DISSECT_IMAGE_PIN_PARTITION_DEVICES, &dissected);
+ if (r < 0)
+ log_error_errno(r, "Failed dissect loopback device %s: %m", loop->node);
+ assert_se(r >= 0);
+
+ log_info("Dissected loop device %s", loop->node);
+
+ for (PartitionDesignator d = 0; d < _PARTITION_DESIGNATOR_MAX; d++) {
+ if (!dissected->partitions[d].found)
+ continue;
+
+ log_notice("Found node %s fstype %s designator %s",
+ dissected->partitions[d].node,
+ dissected->partitions[d].fstype,
+ partition_designator_to_string(d));
+ }
+
+ verify_dissected_image(dissected);
+
+ r = dissected_image_mount(
+ dissected,
+ mounted,
+ /* uid_shift= */ UID_INVALID,
+ /* uid_range= */ UID_INVALID,
+ /* userns_fd= */ -EBADF,
+ DISSECT_IMAGE_READ_ONLY);
+ log_notice_errno(r, "Mounted %s → %s: %m", loop->node, mounted);
+ assert_se(r >= 0);
+
+ /* Now the block device is mounted, we don't need no manual lock anymore, the devices are now
+ * pinned by the mounts. */
+ assert_se(loop_device_flock(loop, LOCK_UN) >= 0);
+
+ log_notice("Unmounting %s", mounted);
+ mounted = umount_and_rmdir_and_free(mounted);
+
+ log_notice("Unmounted.");
+
+ dissected = dissected_image_unref(dissected);
+
+ log_notice("Detaching loop device %s", loop->node);
+ loop = loop_device_unref(loop);
+ log_notice("Detached loop device.");
+ }
+
+ log_notice("Leaving thread");
+
+ return NULL;
+}
+#endif
+
+static bool have_root_gpt_type(void) {
+#ifdef SD_GPT_ROOT_NATIVE
+ return true;
+#else
+ return false;
+#endif
+}
+
+static int run(int argc, char *argv[]) {
+#if HAVE_BLKID
+ _cleanup_(dissected_image_unrefp) DissectedImage *dissected = NULL;
+ _cleanup_(umount_and_rmdir_and_freep) char *mounted = NULL;
+ pthread_t threads[arg_n_threads];
+ sd_id128_t id;
+#endif
+ _cleanup_free_ char *p = NULL, *cmd = NULL;
+ _cleanup_pclose_ FILE *sfdisk = NULL;
+ _cleanup_(loop_device_unrefp) LoopDevice *loop = NULL;
+ _cleanup_close_ int fd = -EBADF;
+ int r;
+
+ test_setup_logging(LOG_DEBUG);
+ log_show_tid(true);
+ log_show_time(true);
+ log_show_color(true);
+
+ if (argc >= 2) {
+ r = safe_atou(argv[1], &arg_n_threads);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse first argument (number of threads): %s", argv[1]);
+ if (arg_n_threads <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Number of threads must be at least 1, refusing.");
+ }
+
+ if (argc >= 3) {
+ r = safe_atou(argv[2], &arg_n_iterations);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse second argument (number of iterations): %s", argv[2]);
+ if (arg_n_iterations <= 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Number of iterations must be at least 1, refusing.");
+ }
+
+ if (argc >= 4) {
+ r = parse_sec(argv[3], &arg_timeout);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse third argument (timeout): %s", argv[3]);
+ }
+
+ if (argc >= 5)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too many arguments (expected 3 at max).");
+
+ if (!have_root_gpt_type())
+ return log_tests_skipped("No root partition GPT defined for this architecture");
+
+ r = find_executable("sfdisk", NULL);
+ if (r < 0)
+ return log_tests_skipped_errno(r, "Could not find sfdisk command");
+
+ assert_se(tempfn_random_child("/var/tmp", "sfdisk", &p) >= 0);
+ fd = open(p, O_CREAT|O_EXCL|O_RDWR|O_CLOEXEC|O_NOFOLLOW, 0666);
+ assert_se(fd >= 0);
+ assert_se(ftruncate(fd, 256*1024*1024) >= 0);
+
+ assert_se(cmd = strjoin("sfdisk ", p));
+ assert_se(sfdisk = popen(cmd, "we"));
+
+ /* A reasonably complex partition table that fits on a 64K disk */
+ fputs("label: gpt\n"
+ "size=32M, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B\n"
+ "size=32M, type=BC13C2FF-59E6-4262-A352-B275FD6F7172\n"
+ "size=32M, type=0657FD6D-A4AB-43C4-84E5-0933C84B4F4F\n"
+ "size=32M, type=", sfdisk);
+
+#ifdef SD_GPT_ROOT_NATIVE
+ fprintf(sfdisk, SD_ID128_UUID_FORMAT_STR, SD_ID128_FORMAT_VAL(SD_GPT_ROOT_NATIVE));
+#else
+ fprintf(sfdisk, SD_ID128_UUID_FORMAT_STR, SD_ID128_FORMAT_VAL(SD_GPT_ROOT_X86_64));
+#endif
+
+ fputs("\n"
+ "size=32M, type=933AC7E1-2EB4-4F13-B844-0E14E2AEF915\n", sfdisk);
+
+ assert_se(pclose(sfdisk) == 0);
+ sfdisk = NULL;
+
+#if HAVE_BLKID
+ assert_se(dissect_image_file(p, NULL, NULL, NULL, 0, &dissected) >= 0);
+ verify_dissected_image(dissected);
+ dissected = dissected_image_unref(dissected);
+#endif
+
+ if (geteuid() != 0 || have_effective_cap(CAP_SYS_ADMIN) <= 0) {
+ log_tests_skipped("not running privileged");
+ return 0;
+ }
+
+ if (detect_container() > 0) {
+ log_tests_skipped("Test not supported in a container, requires udev/uevent notifications");
+ return 0;
+ }
+
+ assert_se(loop_device_make(fd, O_RDWR, 0, UINT64_MAX, 0, LO_FLAGS_PARTSCAN, LOCK_EX, &loop) >= 0);
+
+#if HAVE_BLKID
+ assert_se(dissect_loop_device(loop, NULL, NULL, NULL, DISSECT_IMAGE_ADD_PARTITION_DEVICES|DISSECT_IMAGE_PIN_PARTITION_DEVICES, &dissected) >= 0);
+ verify_dissected_image(dissected);
+
+ FOREACH_STRING(fs, "vfat", "ext4") {
+ r = mkfs_exists(fs);
+ assert_se(r >= 0);
+ if (!r) {
+ log_tests_skipped("mkfs.{vfat|ext4} not installed");
+ return 0;
+ }
+ }
+ assert_se(r >= 0);
+
+ assert_se(sd_id128_randomize(&id) >= 0);
+ assert_se(make_filesystem(dissected->partitions[PARTITION_ESP].node, "vfat", "EFI", NULL, id, true, false, 0, NULL) >= 0);
+
+ assert_se(sd_id128_randomize(&id) >= 0);
+ assert_se(make_filesystem(dissected->partitions[PARTITION_XBOOTLDR].node, "vfat", "xbootldr", NULL, id, true, false, 0, NULL) >= 0);
+
+ assert_se(sd_id128_randomize(&id) >= 0);
+ assert_se(make_filesystem(dissected->partitions[PARTITION_ROOT].node, "ext4", "root", NULL, id, true, false, 0, NULL) >= 0);
+
+ assert_se(sd_id128_randomize(&id) >= 0);
+ assert_se(make_filesystem(dissected->partitions[PARTITION_HOME].node, "ext4", "home", NULL, id, true, false, 0, NULL) >= 0);
+
+ dissected = dissected_image_unref(dissected);
+
+ /* We created the file systems now via the per-partition block devices. But the dissection code might
+ * probe them via the whole block device. These block devices have separate buffer caches though,
+ * hence what was written via the partition device might not appear on the whole block device
+ * yet. Let's hence explicitly flush the whole block device, so that the read-back definitely
+ * works. */
+ assert_se(ioctl(loop->fd, BLKFLSBUF, 0) >= 0);
+
+ /* Try to read once, without pinning or adding partitions, i.e. by only accessing the whole block
+ * device. */
+ assert_se(dissect_loop_device(loop, NULL, NULL, NULL, 0, &dissected) >= 0);
+ verify_dissected_image_harder(dissected);
+ dissected = dissected_image_unref(dissected);
+
+ /* Now go via the loopback device after all, but this time add/pin, because now we want to mount it. */
+ assert_se(dissect_loop_device(loop, NULL, NULL, NULL, DISSECT_IMAGE_ADD_PARTITION_DEVICES|DISSECT_IMAGE_PIN_PARTITION_DEVICES, &dissected) >= 0);
+ verify_dissected_image_harder(dissected);
+
+ assert_se(mkdtemp_malloc(NULL, &mounted) >= 0);
+
+ /* We are particularly correct here, and now downgrade LOCK → LOCK_SH. That's because we are done
+ * with formatting the file systems, so we don't need the exclusive lock anymore. From now on a
+ * shared one is fine. This way udev can now probe the device if it wants, but still won't call
+ * BLKRRPART on it, and that's good, because that would destroy our partition table while we are at
+ * it. */
+ assert_se(loop_device_flock(loop, LOCK_SH) >= 0);
+
+ /* This is a test for the loopback block device setup code and it's use by the image dissection
+ * logic: since the kernel APIs are hard use and prone to races, let's test this in a heavy duty
+ * test: we open a bunch of threads and repeatedly allocate and deallocate loopback block devices in
+ * them in parallel, with an image file with a number of partitions. */
+ assert_se(detach_mount_namespace() >= 0);
+
+ /* This first (writable) mount will initialize the mount point dirs, so that the subsequent read-only ones can work */
+ assert_se(dissected_image_mount(
+ dissected,
+ mounted,
+ /* uid_shift= */ UID_INVALID,
+ /* uid_range= */ UID_INVALID,
+ /* usernfs_fd= */ -EBADF,
+ 0) >= 0);
+
+ /* Now we mounted everything, the partitions are pinned. Now it's fine to release the lock
+ * fully. This means udev could now issue BLKRRPART again, but that's OK given this will fail because
+ * we now mounted the device. */
+ assert_se(loop_device_flock(loop, LOCK_UN) >= 0);
+
+ assert_se(umount_recursive(mounted, 0) >= 0);
+ loop = loop_device_unref(loop);
+
+ log_notice("Threads are being started now");
+
+ /* zero timeout means pick default: let's make sure we run for 10s on slow systems at max */
+ if (arg_timeout == 0)
+ arg_timeout = slow_tests_enabled() ? 5 * USEC_PER_SEC : 1 * USEC_PER_SEC;
+
+ end = usec_add(now(CLOCK_MONOTONIC), arg_timeout);
+
+ if (arg_n_threads > 1)
+ for (unsigned i = 0; i < arg_n_threads; i++)
+ assert_se(pthread_create(threads + i, NULL, thread_func, FD_TO_PTR(fd)) == 0);
+
+ log_notice("All threads started now.");
+
+ if (arg_n_threads == 1)
+ assert_se(thread_func(FD_TO_PTR(fd)) == NULL);
+ else
+ for (unsigned i = 0; i < arg_n_threads; i++) {
+ log_notice("Joining thread #%u.", i);
+
+ void *k;
+ assert_se(pthread_join(threads[i], &k) == 0);
+ assert_se(!k);
+
+ log_notice("Joined thread #%u.", i);
+ }
+
+ log_notice("Threads are all terminated now.");
+#else
+ log_notice("Cutting test short, since we do not have libblkid.");
+#endif
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);
diff --git a/src/test/test-loopback.c b/src/test/test-loopback.c
new file mode 100644
index 0000000..48869ae
--- /dev/null
+++ b/src/test/test-loopback.c
@@ -0,0 +1,46 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sched.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "errno-util.h"
+#include "log.h"
+#include "loopback-setup.h"
+#include "tests.h"
+
+TEST_RET(loopback_setup) {
+ int r;
+
+ if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) {
+ if (ERRNO_IS_PRIVILEGE(errno) || ERRNO_IS_NOT_SUPPORTED(errno))
+ return log_tests_skipped("lacking privileges or namespaces not supported");
+ return log_error_errno(errno, "Failed to create user+network namespace: %m");
+ }
+
+ r = loopback_setup();
+ if (r < 0)
+ return log_error_errno(r, "loopback: %m");
+
+ log_info("> ipv6 main");
+ /* <0 → fork error, ==0 → success, >0 → error in child */
+ assert_se(system("ip -6 route show table main") >= 0);
+
+ log_info("> ipv6 local");
+ assert_se(system("ip -6 route show table local") >=0);
+
+ log_info("> ipv4 main");
+ assert_se(system("ip -4 route show table main") >= 0);
+
+ log_info("> ipv4 local");
+ assert_se(system("ip -4 route show table local") >= 0);
+
+ return EXIT_SUCCESS;
+}
+
+static int intro(void) {
+ log_show_color(true);
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_INFO, intro);
diff --git a/src/test/test-macro.c b/src/test/test-macro.c
new file mode 100644
index 0000000..b91a1f9
--- /dev/null
+++ b/src/test/test-macro.c
@@ -0,0 +1,1040 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stddef.h>
+
+#include "log.h"
+#include "macro.h"
+#include "tests.h"
+
+TEST(saturate_add) {
+ assert_se(saturate_add(1, 2, UINT8_MAX) == 3);
+ assert_se(saturate_add(1, UINT8_MAX-2, UINT8_MAX) == UINT8_MAX-1);
+ assert_se(saturate_add(1, UINT8_MAX-1, UINT8_MAX) == UINT8_MAX);
+ assert_se(saturate_add(1, UINT8_MAX, UINT8_MAX) == UINT8_MAX);
+ assert_se(saturate_add(2, UINT8_MAX, UINT8_MAX) == UINT8_MAX);
+ assert_se(saturate_add(60, 60, 50) == 50);
+}
+
+TEST(ALIGN_POWER2) {
+ unsigned long i, p2;
+
+ assert_se(ALIGN_POWER2(0) == 0);
+ assert_se(ALIGN_POWER2(1) == 1);
+ assert_se(ALIGN_POWER2(2) == 2);
+ assert_se(ALIGN_POWER2(3) == 4);
+ assert_se(ALIGN_POWER2(4) == 4);
+ assert_se(ALIGN_POWER2(5) == 8);
+ assert_se(ALIGN_POWER2(6) == 8);
+ assert_se(ALIGN_POWER2(7) == 8);
+ assert_se(ALIGN_POWER2(9) == 16);
+ assert_se(ALIGN_POWER2(10) == 16);
+ assert_se(ALIGN_POWER2(11) == 16);
+ assert_se(ALIGN_POWER2(12) == 16);
+ assert_se(ALIGN_POWER2(13) == 16);
+ assert_se(ALIGN_POWER2(14) == 16);
+ assert_se(ALIGN_POWER2(15) == 16);
+ assert_se(ALIGN_POWER2(16) == 16);
+ assert_se(ALIGN_POWER2(17) == 32);
+
+ assert_se(ALIGN_POWER2(ULONG_MAX) == 0);
+ assert_se(ALIGN_POWER2(ULONG_MAX - 1) == 0);
+ assert_se(ALIGN_POWER2(ULONG_MAX - 1024) == 0);
+ assert_se(ALIGN_POWER2(ULONG_MAX / 2) == ULONG_MAX / 2 + 1);
+ assert_se(ALIGN_POWER2(ULONG_MAX + 1) == 0);
+
+ for (i = 1; i < 131071; ++i) {
+ for (p2 = 1; p2 < i; p2 <<= 1)
+ /* empty */ ;
+
+ assert_se(ALIGN_POWER2(i) == p2);
+ }
+
+ for (i = ULONG_MAX - 1024; i < ULONG_MAX; ++i) {
+ for (p2 = 1; p2 && p2 < i; p2 <<= 1)
+ /* empty */ ;
+
+ assert_se(ALIGN_POWER2(i) == p2);
+ }
+}
+
+TEST(MAX) {
+ static const struct {
+ int a;
+ int b[CONST_MAX(10, 100)];
+ } val1 = {
+ .a = CONST_MAX(10, 100),
+ };
+ int d = 0;
+ unsigned long x = 12345;
+ unsigned long y = 54321;
+ const char str[] = "a_string_constant";
+ const unsigned long long arr[] = {9999ULL, 10ULL, 0ULL, 3000ULL, 2000ULL, 1000ULL, 100ULL, 9999999ULL};
+ void *p = (void *)str;
+ void *q = (void *)&str[16];
+
+ assert_cc(sizeof(val1.b) == sizeof(int) * 100);
+
+ /* CONST_MAX returns (void) instead of a value if the passed arguments
+ * are not of the same type or not constant expressions. */
+ assert_cc(__builtin_types_compatible_p(typeof(CONST_MAX(1, 10)), int));
+ assert_cc(__builtin_types_compatible_p(typeof(CONST_MAX(1, 1U)), void));
+
+ assert_se(val1.a == 100);
+ assert_se(MAX(++d, 0) == 1);
+ assert_se(d == 1);
+
+ assert_cc(MAXSIZE(char[3], uint16_t) == 3);
+ assert_cc(MAXSIZE(char[3], uint32_t) == 4);
+ assert_cc(MAXSIZE(char, long) == sizeof(long));
+
+ assert_se(MAX(-5, 5) == 5);
+ assert_se(MAX(5, 5) == 5);
+ assert_se(MAX(MAX(1, MAX(2, MAX(3, 4))), 5) == 5);
+ assert_se(MAX(MAX(1, MAX(2, MAX(3, 2))), 1) == 3);
+ assert_se(MAX(MIN(1, MIN(2, MIN(3, 4))), 5) == 5);
+ assert_se(MAX(MAX(1, MIN(2, MIN(3, 2))), 1) == 2);
+ assert_se(LESS_BY(8, 4) == 4);
+ assert_se(LESS_BY(8, 8) == 0);
+ assert_se(LESS_BY(4, 8) == 0);
+ assert_se(LESS_BY(16, LESS_BY(8, 4)) == 12);
+ assert_se(LESS_BY(4, LESS_BY(8, 4)) == 0);
+ assert_se(CMP(3, 5) == -1);
+ assert_se(CMP(5, 3) == 1);
+ assert_se(CMP(5, 5) == 0);
+ assert_se(CMP(x, y) == -1);
+ assert_se(CMP(y, x) == 1);
+ assert_se(CMP(x, x) == 0);
+ assert_se(CMP(y, y) == 0);
+ assert_se(CMP(UINT64_MAX, (uint64_t) 0) == 1);
+ assert_se(CMP((uint64_t) 0, UINT64_MAX) == -1);
+ assert_se(CMP(UINT64_MAX, UINT64_MAX) == 0);
+ assert_se(CMP(INT64_MIN, INT64_MAX) == -1);
+ assert_se(CMP(INT64_MAX, INT64_MIN) == 1);
+ assert_se(CMP(INT64_MAX, INT64_MAX) == 0);
+ assert_se(CMP(INT64_MIN, INT64_MIN) == 0);
+ assert_se(CMP(INT64_MAX, (int64_t) 0) == 1);
+ assert_se(CMP((int64_t) 0, INT64_MIN) == 1);
+ assert_se(CMP(INT64_MIN, (int64_t) 0) == -1);
+ assert_se(CMP((int64_t) 0, INT64_MAX) == -1);
+ assert_se(CMP(&str[2], &str[7]) == -1);
+ assert_se(CMP(&str[2], &str[2]) == 0);
+ assert_se(CMP(&str[7], (const char *)str) == 1);
+ assert_se(CMP(str[2], str[7]) == 1);
+ assert_se(CMP(str[7], *str) == 1);
+ assert_se(CMP((const unsigned long long *)arr, &arr[3]) == -1);
+ assert_se(CMP(*arr, arr[3]) == 1);
+ assert_se(CMP(p, q) == -1);
+ assert_se(CMP(q, p) == 1);
+ assert_se(CMP(p, p) == 0);
+ assert_se(CMP(q, q) == 0);
+ assert_se(CLAMP(-5, 0, 1) == 0);
+ assert_se(CLAMP(5, 0, 1) == 1);
+ assert_se(CLAMP(5, -10, 1) == 1);
+ assert_se(CLAMP(5, -10, 10) == 5);
+ assert_se(CLAMP(CLAMP(0, -10, 10), CLAMP(-5, 10, 20), CLAMP(100, -5, 20)) == 10);
+}
+
+#pragma GCC diagnostic push
+#ifdef __clang__
+# pragma GCC diagnostic ignored "-Waddress-of-packed-member"
+#endif
+
+TEST(container_of) {
+ struct mytype {
+ uint8_t pad1[3];
+ uint64_t v1;
+ uint8_t pad2[2];
+ uint32_t v2;
+ } myval = { };
+
+ assert_cc(sizeof(myval) >= 17);
+ assert_se(container_of(&myval.v1, struct mytype, v1) == &myval);
+ assert_se(container_of(&myval.v2, struct mytype, v2) == &myval);
+ assert_se(container_of(&container_of(&myval.v2,
+ struct mytype,
+ v2)->v1,
+ struct mytype,
+ v1) == &myval);
+}
+
+#pragma GCC diagnostic pop
+
+TEST(DIV_ROUND_UP) {
+ int div;
+
+ /* basic tests */
+ assert_se(DIV_ROUND_UP(0, 8) == 0);
+ assert_se(DIV_ROUND_UP(1, 8) == 1);
+ assert_se(DIV_ROUND_UP(8, 8) == 1);
+ assert_se(DIV_ROUND_UP(12, 8) == 2);
+ assert_se(DIV_ROUND_UP(16, 8) == 2);
+
+ /* test multiple evaluation */
+ div = 0;
+ assert_se(DIV_ROUND_UP(div++, 8) == 0 && div == 1);
+ assert_se(DIV_ROUND_UP(++div, 8) == 1 && div == 2);
+ assert_se(DIV_ROUND_UP(8, div++) == 4 && div == 3);
+ assert_se(DIV_ROUND_UP(8, ++div) == 2 && div == 4);
+
+ /* overflow test with exact division */
+ assert_se(sizeof(0U) == 4);
+ assert_se(0xfffffffaU % 10U == 0U);
+ assert_se(0xfffffffaU / 10U == 429496729U);
+ assert_se(DIV_ROUND_UP(0xfffffffaU, 10U) == 429496729U);
+ assert_se((0xfffffffaU + 10U - 1U) / 10U == 0U);
+ assert_se(0xfffffffaU / 10U + !!(0xfffffffaU % 10U) == 429496729U);
+
+ /* overflow test with rounded division */
+ assert_se(0xfffffffdU % 10U == 3U);
+ assert_se(0xfffffffdU / 10U == 429496729U);
+ assert_se(DIV_ROUND_UP(0xfffffffdU, 10U) == 429496730U);
+ assert_se((0xfffffffdU + 10U - 1U) / 10U == 0U);
+ assert_se(0xfffffffdU / 10U + !!(0xfffffffdU % 10U) == 429496730U);
+}
+
+TEST(PTR_TO_INT) {
+ /* Primary reason to have this test is to validate that pointers are large enough to hold entire int range */
+ assert_se(PTR_TO_INT(INT_TO_PTR(0)) == 0);
+ assert_se(PTR_TO_INT(INT_TO_PTR(1)) == 1);
+ assert_se(PTR_TO_INT(INT_TO_PTR(-1)) == -1);
+ assert_se(PTR_TO_INT(INT_TO_PTR(INT_MAX)) == INT_MAX);
+ assert_se(PTR_TO_INT(INT_TO_PTR(INT_MIN)) == INT_MIN);
+}
+
+TEST(IN_SET) {
+ assert_se(IN_SET(1, 1, 2));
+ assert_se(IN_SET(1, 1, 2, 3, 4));
+ assert_se(IN_SET(2, 1, 2, 3, 4));
+ assert_se(IN_SET(3, 1, 2, 3, 4));
+ assert_se(IN_SET(4, 1, 2, 3, 4));
+ assert_se(!IN_SET(0, 1, 2));
+ assert_se(!IN_SET(0, 1, 2, 3, 4));
+
+ struct {
+ unsigned x:3;
+ } t = { 1 };
+
+ assert_se(IN_SET(t.x, 1, 2));
+ assert_se(IN_SET(t.x, 1, 2, 3, 4));
+ assert_se(IN_SET(t.x, 2, 3, 4, 1));
+ assert_se(!IN_SET(t.x, 0, 2));
+ assert_se(!IN_SET(t.x, 2, 3, 4));
+}
+
+TEST(FOREACH_POINTER) {
+ int a, b, c, *i;
+ size_t k = 0;
+
+ FOREACH_POINTER(i, &a, &b, &c) {
+ switch (k) {
+
+ case 0:
+ assert_se(i == &a);
+ break;
+
+ case 1:
+ assert_se(i == &b);
+ break;
+
+ case 2:
+ assert_se(i == &c);
+ break;
+
+ default:
+ assert_not_reached();
+ break;
+ }
+
+ k++;
+ }
+
+ assert_se(k == 3);
+
+ FOREACH_POINTER(i, &b) {
+ assert_se(k == 3);
+ assert_se(i == &b);
+ k = 4;
+ }
+
+ assert_se(k == 4);
+
+ FOREACH_POINTER(i, NULL, &c, NULL, &b, NULL, &a, NULL) {
+ switch (k) {
+
+ case 4:
+ assert_se(i == NULL);
+ break;
+
+ case 5:
+ assert_se(i == &c);
+ break;
+
+ case 6:
+ assert_se(i == NULL);
+ break;
+
+ case 7:
+ assert_se(i == &b);
+ break;
+
+ case 8:
+ assert_se(i == NULL);
+ break;
+
+ case 9:
+ assert_se(i == &a);
+ break;
+
+ case 10:
+ assert_se(i == NULL);
+ break;
+
+ default:
+ assert_not_reached();
+ break;
+ }
+
+ k++;
+ }
+
+ assert_se(k == 11);
+}
+
+TEST(FOREACH_VA_ARGS) {
+ size_t i;
+
+ i = 0;
+ uint8_t u8, u8_1 = 1, u8_2 = 2, u8_3 = 3;
+ VA_ARGS_FOREACH(u8, u8_2, 8, 0xff, u8_1, u8_3, 0, 1) {
+ switch(i++) {
+ case 0: assert_se(u8 == u8_2); break;
+ case 1: assert_se(u8 == 8); break;
+ case 2: assert_se(u8 == 0xff); break;
+ case 3: assert_se(u8 == u8_1); break;
+ case 4: assert_se(u8 == u8_3); break;
+ case 5: assert_se(u8 == 0); break;
+ case 6: assert_se(u8 == 1); break;
+ default: assert_se(false);
+ }
+ }
+ assert_se(i == 7);
+ i = 0;
+ VA_ARGS_FOREACH(u8, 0) {
+ assert_se(u8 == 0);
+ assert_se(i++ == 0);
+ }
+ assert_se(i == 1);
+ i = 0;
+ VA_ARGS_FOREACH(u8, 0xff) {
+ assert_se(u8 == 0xff);
+ assert_se(i++ == 0);
+ }
+ assert_se(i == 1);
+ VA_ARGS_FOREACH(u8)
+ assert_se(false);
+
+ i = 0;
+ uint32_t u32, u32_1 = 0xffff0000, u32_2 = 10, u32_3 = 0xffff;
+ VA_ARGS_FOREACH(u32, 1, 100, u32_2, 1000, u32_3, u32_1, 1, 0) {
+ switch(i++) {
+ case 0: assert_se(u32 == 1); break;
+ case 1: assert_se(u32 == 100); break;
+ case 2: assert_se(u32 == u32_2); break;
+ case 3: assert_se(u32 == 1000); break;
+ case 4: assert_se(u32 == u32_3); break;
+ case 5: assert_se(u32 == u32_1); break;
+ case 6: assert_se(u32 == 1); break;
+ case 7: assert_se(u32 == 0); break;
+ default: assert_se(false);
+ }
+ }
+ assert_se(i == 8);
+ i = 0;
+ VA_ARGS_FOREACH(u32, 0) {
+ assert_se(u32 == 0);
+ assert_se(i++ == 0);
+ }
+ assert_se(i == 1);
+ i = 0;
+ VA_ARGS_FOREACH(u32, 1000) {
+ assert_se(u32 == 1000);
+ assert_se(i++ == 0);
+ }
+ assert_se(i == 1);
+ VA_ARGS_FOREACH(u32)
+ assert_se(false);
+
+ i = 0;
+ uint64_t u64, u64_1 = 0xffffffffffffffff, u64_2 = 50, u64_3 = 0xffff;
+ VA_ARGS_FOREACH(u64, 44, 0, u64_3, 100, u64_2, u64_1, 50000) {
+ switch(i++) {
+ case 0: assert_se(u64 == 44); break;
+ case 1: assert_se(u64 == 0); break;
+ case 2: assert_se(u64 == u64_3); break;
+ case 3: assert_se(u64 == 100); break;
+ case 4: assert_se(u64 == u64_2); break;
+ case 5: assert_se(u64 == u64_1); break;
+ case 6: assert_se(u64 == 50000); break;
+ default: assert_se(false);
+ }
+ }
+ assert_se(i == 7);
+ i = 0;
+ VA_ARGS_FOREACH(u64, 0) {
+ assert_se(u64 == 0);
+ assert_se(i++ == 0);
+ }
+ assert_se(i == 1);
+ i = 0;
+ VA_ARGS_FOREACH(u64, 0xff00ff00000000) {
+ assert_se(u64 == 0xff00ff00000000);
+ assert_se(i++ == 0);
+ }
+ assert_se(i == 1);
+ VA_ARGS_FOREACH(u64)
+ assert_se(false);
+
+ struct test {
+ int a;
+ char b;
+ };
+
+ i = 0;
+ struct test s,
+ s_1 = { .a = 0, .b = 'c', },
+ s_2 = { .a = 100000, .b = 'z', },
+ s_3 = { .a = 0xff, .b = 'q', },
+ s_4 = { .a = 1, .b = 'x', };
+ VA_ARGS_FOREACH(s, s_1, (struct test){ .a = 10, .b = 'd', }, s_2, (struct test){}, s_3, s_4) {
+ switch(i++) {
+ case 0: assert_se(s.a == 0 ); assert_se(s.b == 'c'); break;
+ case 1: assert_se(s.a == 10 ); assert_se(s.b == 'd'); break;
+ case 2: assert_se(s.a == 100000); assert_se(s.b == 'z'); break;
+ case 3: assert_se(s.a == 0 ); assert_se(s.b == 0 ); break;
+ case 4: assert_se(s.a == 0xff ); assert_se(s.b == 'q'); break;
+ case 5: assert_se(s.a == 1 ); assert_se(s.b == 'x'); break;
+ default: assert_se(false);
+ }
+ }
+ assert_se(i == 6);
+ i = 0;
+ VA_ARGS_FOREACH(s, (struct test){ .a = 1, .b = 'A', }) {
+ assert_se(s.a == 1);
+ assert_se(s.b == 'A');
+ assert_se(i++ == 0);
+ }
+ assert_se(i == 1);
+ VA_ARGS_FOREACH(s)
+ assert_se(false);
+
+ i = 0;
+ struct test *p, *p_1 = &s_1, *p_2 = &s_2, *p_3 = &s_3, *p_4 = &s_4;
+ VA_ARGS_FOREACH(p, p_1, NULL, p_2, p_3, NULL, p_4, NULL) {
+ switch(i++) {
+ case 0: assert_se(p == p_1); break;
+ case 1: assert_se(p == NULL); break;
+ case 2: assert_se(p == p_2); break;
+ case 3: assert_se(p == p_3); break;
+ case 4: assert_se(p == NULL); break;
+ case 5: assert_se(p == p_4); break;
+ case 6: assert_se(p == NULL); break;
+ default: assert_se(false);
+ }
+ }
+ assert_se(i == 7);
+ i = 0;
+ VA_ARGS_FOREACH(p, p_3) {
+ assert_se(p == p_3);
+ assert_se(i++ == 0);
+ }
+ assert_se(i == 1);
+ VA_ARGS_FOREACH(p)
+ assert_se(false);
+
+ i = 0;
+ void *v, *v_1 = p_1, *v_2 = p_2, *v_3 = p_3;
+ uint32_t *u32p = &u32;
+ VA_ARGS_FOREACH(v, v_1, NULL, u32p, v_3, p_2, p_4, v_2, NULL) {
+ switch(i++) {
+ case 0: assert_se(v == v_1); break;
+ case 1: assert_se(v == NULL); break;
+ case 2: assert_se(v == u32p); break;
+ case 3: assert_se(v == v_3); break;
+ case 4: assert_se(v == p_2); break;
+ case 5: assert_se(v == p_4); break;
+ case 6: assert_se(v == v_2); break;
+ case 7: assert_se(v == NULL); break;
+ default: assert_se(false);
+ }
+ }
+ assert_se(i == 8);
+ i = 0;
+ VA_ARGS_FOREACH(v, NULL) {
+ assert_se(v == NULL);
+ assert_se(i++ == 0);
+ }
+ assert_se(i == 1);
+ i = 0;
+ VA_ARGS_FOREACH(v, v_1) {
+ assert_se(v == v_1);
+ assert_se(i++ == 0);
+ }
+ assert_se(i == 1);
+ VA_ARGS_FOREACH(v)
+ assert_se(false);
+}
+
+TEST(ALIGN_TO) {
+ assert_se(ALIGN_TO(0, 1) == 0);
+ assert_se(ALIGN_TO(1, 1) == 1);
+ assert_se(ALIGN_TO(2, 1) == 2);
+ assert_se(ALIGN_TO(3, 1) == 3);
+ assert_se(ALIGN_TO(4, 1) == 4);
+ assert_se(ALIGN_TO(SIZE_MAX-1, 1) == SIZE_MAX-1);
+ assert_se(ALIGN_TO(SIZE_MAX, 1) == SIZE_MAX);
+
+ assert_se(ALIGN_TO(0, 2) == 0);
+ assert_se(ALIGN_TO(1, 2) == 2);
+ assert_se(ALIGN_TO(2, 2) == 2);
+ assert_se(ALIGN_TO(3, 2) == 4);
+ assert_se(ALIGN_TO(4, 2) == 4);
+ assert_se(ALIGN_TO(SIZE_MAX-3, 2) == SIZE_MAX-3);
+ assert_se(ALIGN_TO(SIZE_MAX-2, 2) == SIZE_MAX-1);
+ assert_se(ALIGN_TO(SIZE_MAX-1, 2) == SIZE_MAX-1);
+ assert_se(ALIGN_TO(SIZE_MAX, 2) == SIZE_MAX); /* overflow */
+
+ assert_se(ALIGN_TO(0, 4) == 0);
+ assert_se(ALIGN_TO(1, 4) == 4);
+ assert_se(ALIGN_TO(2, 4) == 4);
+ assert_se(ALIGN_TO(3, 4) == 4);
+ assert_se(ALIGN_TO(4, 4) == 4);
+ assert_se(ALIGN_TO(SIZE_MAX-3, 4) == SIZE_MAX-3);
+ assert_se(ALIGN_TO(SIZE_MAX-2, 4) == SIZE_MAX); /* overflow */
+ assert_se(ALIGN_TO(SIZE_MAX-1, 4) == SIZE_MAX); /* overflow */
+ assert_se(ALIGN_TO(SIZE_MAX, 4) == SIZE_MAX); /* overflow */
+
+ assert_se(ALIGN_TO_U64(0, 1) == 0);
+ assert_se(ALIGN_TO_U64(1, 1) == 1);
+ assert_se(ALIGN_TO_U64(2, 1) == 2);
+ assert_se(ALIGN_TO_U64(3, 1) == 3);
+ assert_se(ALIGN_TO_U64(4, 1) == 4);
+ assert_se(ALIGN_TO_U64(UINT64_MAX-1, 1) == UINT64_MAX-1);
+ assert_se(ALIGN_TO_U64(UINT64_MAX, 1) == UINT64_MAX);
+
+ assert_se(ALIGN_TO_U64(0, 2) == 0);
+ assert_se(ALIGN_TO_U64(1, 2) == 2);
+ assert_se(ALIGN_TO_U64(2, 2) == 2);
+ assert_se(ALIGN_TO_U64(3, 2) == 4);
+ assert_se(ALIGN_TO_U64(4, 2) == 4);
+ assert_se(ALIGN_TO_U64(UINT64_MAX-3, 2) == UINT64_MAX-3);
+ assert_se(ALIGN_TO_U64(UINT64_MAX-2, 2) == UINT64_MAX-1);
+ assert_se(ALIGN_TO_U64(UINT64_MAX-1, 2) == UINT64_MAX-1);
+ assert_se(ALIGN_TO_U64(UINT64_MAX, 2) == UINT64_MAX); /* overflow */
+
+ assert_se(ALIGN_TO_U64(0, 4) == 0);
+ assert_se(ALIGN_TO_U64(1, 4) == 4);
+ assert_se(ALIGN_TO_U64(2, 4) == 4);
+ assert_se(ALIGN_TO_U64(3, 4) == 4);
+ assert_se(ALIGN_TO_U64(4, 4) == 4);
+ assert_se(ALIGN_TO_U64(UINT64_MAX-3, 4) == UINT64_MAX-3);
+ assert_se(ALIGN_TO_U64(UINT64_MAX-2, 4) == UINT64_MAX); /* overflow */
+ assert_se(ALIGN_TO_U64(UINT64_MAX-1, 4) == UINT64_MAX); /* overflow */
+ assert_se(ALIGN_TO_U64(UINT64_MAX, 4) == UINT64_MAX); /* overflow */
+
+ assert_cc(CONST_ALIGN_TO(96, 512) == 512);
+ assert_cc(CONST_ALIGN_TO(511, 512) == 512);
+ assert_cc(CONST_ALIGN_TO(512, 512) == 512);
+ assert_cc(CONST_ALIGN_TO(513, 512) == 1024);
+ assert_cc(CONST_ALIGN_TO(sizeof(int), 64) == 64);
+
+ assert_cc(__builtin_types_compatible_p(typeof(CONST_ALIGN_TO(4, 3)), void));
+ assert_cc(__builtin_types_compatible_p(typeof(CONST_ALIGN_TO(SIZE_MAX, 512)), void));
+}
+
+TEST(align_down) {
+ assert_se(ALIGN_DOWN(0, 1) == 0);
+ assert_se(ALIGN_DOWN(1, 1) == 1);
+ assert_se(ALIGN_DOWN(2, 1) == 2);
+ assert_se(ALIGN_DOWN(3, 1) == 3);
+ assert_se(ALIGN_DOWN(4, 1) == 4);
+ assert_se(ALIGN_DOWN(SIZE_MAX-1, 1) == SIZE_MAX-1);
+ assert_se(ALIGN_DOWN(SIZE_MAX, 1) == SIZE_MAX);
+
+ assert_se(ALIGN_DOWN(0, 2) == 0);
+ assert_se(ALIGN_DOWN(1, 2) == 0);
+ assert_se(ALIGN_DOWN(2, 2) == 2);
+ assert_se(ALIGN_DOWN(3, 2) == 2);
+ assert_se(ALIGN_DOWN(4, 2) == 4);
+ assert_se(ALIGN_DOWN(SIZE_MAX-1, 2) == SIZE_MAX-1);
+ assert_se(ALIGN_DOWN(SIZE_MAX, 2) == SIZE_MAX-1);
+
+ assert_se(ALIGN_DOWN(0, 4) == 0);
+ assert_se(ALIGN_DOWN(1, 4) == 0);
+ assert_se(ALIGN_DOWN(2, 4) == 0);
+ assert_se(ALIGN_DOWN(3, 4) == 0);
+ assert_se(ALIGN_DOWN(4, 4) == 4);
+ assert_se(ALIGN_DOWN(SIZE_MAX-1, 4) == SIZE_MAX-3);
+ assert_se(ALIGN_DOWN(SIZE_MAX, 4) == SIZE_MAX-3);
+
+ assert_se(ALIGN_DOWN_U64(0, 1) == 0);
+ assert_se(ALIGN_DOWN_U64(1, 1) == 1);
+ assert_se(ALIGN_DOWN_U64(2, 1) == 2);
+ assert_se(ALIGN_DOWN_U64(3, 1) == 3);
+ assert_se(ALIGN_DOWN_U64(4, 1) == 4);
+ assert_se(ALIGN_DOWN_U64(UINT64_MAX-1, 1) == UINT64_MAX-1);
+ assert_se(ALIGN_DOWN_U64(UINT64_MAX, 1) == UINT64_MAX);
+
+ assert_se(ALIGN_DOWN_U64(0, 2) == 0);
+ assert_se(ALIGN_DOWN_U64(1, 2) == 0);
+ assert_se(ALIGN_DOWN_U64(2, 2) == 2);
+ assert_se(ALIGN_DOWN_U64(3, 2) == 2);
+ assert_se(ALIGN_DOWN_U64(4, 2) == 4);
+ assert_se(ALIGN_DOWN_U64(UINT64_MAX-1, 2) == UINT64_MAX-1);
+ assert_se(ALIGN_DOWN_U64(UINT64_MAX, 2) == UINT64_MAX-1);
+
+ assert_se(ALIGN_DOWN_U64(0, 4) == 0);
+ assert_se(ALIGN_DOWN_U64(1, 4) == 0);
+ assert_se(ALIGN_DOWN_U64(2, 4) == 0);
+ assert_se(ALIGN_DOWN_U64(3, 4) == 0);
+ assert_se(ALIGN_DOWN_U64(4, 4) == 4);
+ assert_se(ALIGN_DOWN_U64(UINT64_MAX-1, 4) == UINT64_MAX-3);
+ assert_se(ALIGN_DOWN_U64(UINT64_MAX, 4) == UINT64_MAX-3);
+}
+
+TEST(align_offset) {
+ assert_se(ALIGN_OFFSET(0, 1) == 0);
+ assert_se(ALIGN_OFFSET(1, 1) == 0);
+ assert_se(ALIGN_OFFSET(2, 1) == 0);
+ assert_se(ALIGN_OFFSET(3, 1) == 0);
+ assert_se(ALIGN_OFFSET(4, 1) == 0);
+ assert_se(ALIGN_OFFSET(SIZE_MAX-1, 1) == 0);
+ assert_se(ALIGN_OFFSET(SIZE_MAX, 1) == 0);
+
+ assert_se(ALIGN_OFFSET(0, 2) == 0);
+ assert_se(ALIGN_OFFSET(1, 2) == 1);
+ assert_se(ALIGN_OFFSET(2, 2) == 0);
+ assert_se(ALIGN_OFFSET(3, 2) == 1);
+ assert_se(ALIGN_OFFSET(4, 2) == 0);
+ assert_se(ALIGN_OFFSET(SIZE_MAX-1, 2) == 0);
+ assert_se(ALIGN_OFFSET(SIZE_MAX, 2) == 1);
+
+ assert_se(ALIGN_OFFSET(0, 4) == 0);
+ assert_se(ALIGN_OFFSET(1, 4) == 1);
+ assert_se(ALIGN_OFFSET(2, 4) == 2);
+ assert_se(ALIGN_OFFSET(3, 4) == 3);
+ assert_se(ALIGN_OFFSET(4, 4) == 0);
+ assert_se(ALIGN_OFFSET(SIZE_MAX-1, 4) == 2);
+ assert_se(ALIGN_OFFSET(SIZE_MAX, 4) == 3);
+
+ assert_se(ALIGN_OFFSET_U64(0, 1) == 0);
+ assert_se(ALIGN_OFFSET_U64(1, 1) == 0);
+ assert_se(ALIGN_OFFSET_U64(2, 1) == 0);
+ assert_se(ALIGN_OFFSET_U64(3, 1) == 0);
+ assert_se(ALIGN_OFFSET_U64(4, 1) == 0);
+ assert_se(ALIGN_OFFSET_U64(UINT64_MAX-1, 1) == 0);
+ assert_se(ALIGN_OFFSET_U64(UINT64_MAX, 1) == 0);
+
+ assert_se(ALIGN_OFFSET_U64(0, 2) == 0);
+ assert_se(ALIGN_OFFSET_U64(1, 2) == 1);
+ assert_se(ALIGN_OFFSET_U64(2, 2) == 0);
+ assert_se(ALIGN_OFFSET_U64(3, 2) == 1);
+ assert_se(ALIGN_OFFSET_U64(4, 2) == 0);
+ assert_se(ALIGN_OFFSET_U64(UINT64_MAX-1, 2) == 0);
+ assert_se(ALIGN_OFFSET_U64(UINT64_MAX, 2) == 1);
+
+ assert_se(ALIGN_OFFSET_U64(0, 4) == 0);
+ assert_se(ALIGN_OFFSET_U64(1, 4) == 1);
+ assert_se(ALIGN_OFFSET_U64(2, 4) == 2);
+ assert_se(ALIGN_OFFSET_U64(3, 4) == 3);
+ assert_se(ALIGN_OFFSET_U64(4, 4) == 0);
+ assert_se(ALIGN_OFFSET_U64(UINT64_MAX-1, 4) == 2);
+ assert_se(ALIGN_OFFSET_U64(UINT64_MAX, 4) == 3);
+}
+
+TEST(flags) {
+ enum {
+ F1 = 1 << 0,
+ F2 = 1 << 1,
+ F3 = 1 << 2,
+ F_ALL = F1 | F2 | F3
+ };
+ unsigned n, f;
+
+ assert_se(FLAGS_SET(0, 0));
+ assert_se(FLAGS_SET(F1, F1));
+ assert_se(FLAGS_SET(F1 | F2, F1));
+ assert_se(FLAGS_SET(F1 | F3, F1 | F3));
+ assert_se(FLAGS_SET(F1 | F2 | F3, F_ALL));
+ assert_se(!FLAGS_SET(0, F1));
+ assert_se(!FLAGS_SET(F2, F1));
+ assert_se(!FLAGS_SET(F1 | F2, F3));
+ assert_se(!FLAGS_SET(F1 | F2, F1 | F3));
+ assert_se(!FLAGS_SET(F1 | F2 | F3, ~F_ALL));
+
+ /* Check for no double eval. */
+ n = F2;
+ f = F1;
+ assert_se(!FLAGS_SET(--n, ++f));
+ assert_se(n == F1);
+ assert_se(f == F2);
+
+ SET_FLAG(n, F3, true);
+ assert_se(n == (F1 | F3));
+ SET_FLAG(n, F2, false);
+ assert_se(n == (F1 | F3));
+ SET_FLAG(n, F3, false);
+ assert_se(n == F1);
+ SET_FLAG(n, F1, true);
+ assert_se(n == F1);
+ SET_FLAG(n, F1 | F3, true);
+ assert_se(n == (F1 | F3));
+ SET_FLAG(n, F_ALL, false);
+ assert_se(n == 0);
+
+ assert_se(UPDATE_FLAG(0, 0, true) == 0);
+ assert_se(UPDATE_FLAG(0, F1, true) == F1);
+ assert_se(UPDATE_FLAG(0, F1 | F2, true) == (F1 | F2));
+ assert_se(UPDATE_FLAG(F1, 0, true) == F1);
+ assert_se(UPDATE_FLAG(F1, F1, true) == F1);
+ assert_se(UPDATE_FLAG(F1, F3, true) == (F1 | F3));
+ assert_se(UPDATE_FLAG(F1, F1 | F3, true) == (F1 | F3));
+ assert_se(UPDATE_FLAG(F1, F_ALL, true) == F_ALL);
+ assert_se(UPDATE_FLAG(0, 0, false) == 0);
+ assert_se(UPDATE_FLAG(0, F1, false) == 0);
+ assert_se(UPDATE_FLAG(0, F1 | F2, false) == 0);
+ assert_se(UPDATE_FLAG(F1, 0, false) == F1);
+ assert_se(UPDATE_FLAG(F1, F1, false) == 0);
+ assert_se(UPDATE_FLAG(F1, F3, false) == F1);
+ assert_se(UPDATE_FLAG(F1, F1 | F3, false) == 0);
+ assert_se(UPDATE_FLAG(F1, F2 | F3, false) == F1);
+ assert_se(UPDATE_FLAG(F1, F_ALL, false) == 0);
+ assert_se(UPDATE_FLAG(F_ALL, F_ALL, false) == 0);
+
+ /* Check for no double eval. */
+ n = F2;
+ f = F1;
+ assert_se(UPDATE_FLAG(--n, ++f, true) == (F1 | F2));
+ assert_se(n == F1);
+ assert_se(f == F2);
+}
+
+TEST(DECIMAL_STR_WIDTH) {
+ assert_se(DECIMAL_STR_WIDTH(0) == 1);
+ assert_se(DECIMAL_STR_WIDTH(1) == 1);
+ assert_se(DECIMAL_STR_WIDTH(2) == 1);
+ assert_se(DECIMAL_STR_WIDTH(9) == 1);
+ assert_se(DECIMAL_STR_WIDTH(10) == 2);
+ assert_se(DECIMAL_STR_WIDTH(11) == 2);
+ assert_se(DECIMAL_STR_WIDTH(99) == 2);
+ assert_se(DECIMAL_STR_WIDTH(100) == 3);
+ assert_se(DECIMAL_STR_WIDTH(101) == 3);
+ assert_se(DECIMAL_STR_WIDTH(-1) == 2);
+ assert_se(DECIMAL_STR_WIDTH(-2) == 2);
+ assert_se(DECIMAL_STR_WIDTH(-9) == 2);
+ assert_se(DECIMAL_STR_WIDTH(-10) == 3);
+ assert_se(DECIMAL_STR_WIDTH(-11) == 3);
+ assert_se(DECIMAL_STR_WIDTH(-99) == 3);
+ assert_se(DECIMAL_STR_WIDTH(-100) == 4);
+ assert_se(DECIMAL_STR_WIDTH(-101) == 4);
+ assert_se(DECIMAL_STR_WIDTH(UINT64_MAX) == STRLEN("18446744073709551615"));
+ assert_se(DECIMAL_STR_WIDTH(INT64_MAX) == STRLEN("9223372036854775807"));
+ assert_se(DECIMAL_STR_WIDTH(INT64_MIN) == STRLEN("-9223372036854775808"));
+}
+
+TEST(DECIMAL_STR_MAX) {
+ int8_t s8_longest = INT8_MIN;
+ int16_t s16_longest = INT16_MIN;
+ int32_t s32_longest = INT32_MIN;
+ int64_t s64_longest = INT64_MIN;
+ uint8_t u8_longest = UINT8_MAX;
+ uint16_t u16_longest = UINT16_MAX;
+ uint32_t u32_longest = UINT32_MAX;
+ uint64_t u64_longest = UINT64_MAX;
+
+ /* NB: Always add +1, because DECIMAL_STR_MAX() includes space for trailing NUL byte, but
+ * DECIMAL_STR_WIDTH() does not! */
+ assert_se(DECIMAL_STR_MAX(int8_t) == DECIMAL_STR_WIDTH(s8_longest)+1);
+ assert_se(DECIMAL_STR_MAX(int16_t) == DECIMAL_STR_WIDTH(s16_longest)+1);
+ assert_se(DECIMAL_STR_MAX(int32_t) == DECIMAL_STR_WIDTH(s32_longest)+1);
+ assert_se(DECIMAL_STR_MAX(int64_t) == DECIMAL_STR_WIDTH(s64_longest)+1);
+
+ assert_se(DECIMAL_STR_MAX(uint8_t) == DECIMAL_STR_WIDTH(u8_longest)+1);
+ assert_se(DECIMAL_STR_MAX(uint16_t) == DECIMAL_STR_WIDTH(u16_longest)+1);
+ assert_se(DECIMAL_STR_MAX(uint32_t) == DECIMAL_STR_WIDTH(u32_longest)+1);
+ assert_se(DECIMAL_STR_MAX(uint64_t) == DECIMAL_STR_WIDTH(u64_longest)+1);
+}
+
+TEST(PTR_SUB1) {
+ static const uint64_t x[4] = { 2, 3, 4, 5 };
+ const uint64_t *p;
+
+ p = x + ELEMENTSOF(x)-1;
+ assert_se(*p == 5);
+
+ p = PTR_SUB1(p, x);
+ assert_se(*p == 4);
+
+ p = PTR_SUB1(p, x);
+ assert_se(*p == 3);
+
+ p = PTR_SUB1(p, x);
+ assert_se(*p == 2);
+
+ p = PTR_SUB1(p, x);
+ assert_se(!p);
+
+ p = PTR_SUB1(p, x);
+ assert_se(!p);
+}
+
+TEST(ISPOWEROF2) {
+ uint64_t u;
+ int64_t i;
+
+ /* First, test constant expressions */
+ assert_se(!ISPOWEROF2(-2));
+ assert_se(!ISPOWEROF2(-1));
+ assert_se(!ISPOWEROF2(0));
+ assert_se(ISPOWEROF2(1));
+ assert_se(ISPOWEROF2(2));
+ assert_se(!ISPOWEROF2(3));
+ assert_se(ISPOWEROF2(4));
+ assert_se(!ISPOWEROF2(5));
+ assert_se(!ISPOWEROF2(6));
+ assert_se(!ISPOWEROF2(7));
+ assert_se(ISPOWEROF2(8));
+ assert_se(!ISPOWEROF2(9));
+ assert_se(!ISPOWEROF2(1022));
+ assert_se(ISPOWEROF2(1024));
+ assert_se(!ISPOWEROF2(1025));
+ assert_se(!ISPOWEROF2(UINT64_C(0xffffffff)));
+ assert_se(ISPOWEROF2(UINT64_C(0x100000000)));
+ assert_se(!ISPOWEROF2(UINT64_C(0x100000001)));
+
+ /* Then, test dynamic expressions, and if they are side-effect free */
+ i = -2;
+ assert_se(!ISPOWEROF2(i++));
+ assert_se(i == -1);
+ assert_se(!ISPOWEROF2(i++));
+ assert_se(i == 0);
+ assert_se(!ISPOWEROF2(i++));
+ assert_se(i == 1);
+ assert_se(ISPOWEROF2(i++));
+ assert_se(i == 2);
+ assert_se(ISPOWEROF2(i++));
+ assert_se(i == 3);
+ assert_se(!ISPOWEROF2(i++));
+ assert_se(i == 4);
+ assert_se(ISPOWEROF2(i++));
+ assert_se(i == 5);
+ assert_se(!ISPOWEROF2(i));
+
+ u = 0;
+ assert_se(!ISPOWEROF2(u++));
+ assert_se(u == 1);
+ assert_se(ISPOWEROF2(u++));
+ assert_se(u == 2);
+ assert_se(ISPOWEROF2(u++));
+ assert_se(u == 3);
+ assert_se(!ISPOWEROF2(u++));
+ assert_se(u == 4);
+ assert_se(ISPOWEROF2(u++));
+ assert_se(u == 5);
+ assert_se(!ISPOWEROF2(u));
+}
+
+TEST(ALIGNED) {
+ assert_se(IS_ALIGNED16(NULL));
+ assert_se(IS_ALIGNED32(NULL));
+ assert_se(IS_ALIGNED64(NULL));
+
+ uint64_t u64;
+ uint32_t u32;
+ uint16_t u16;
+
+ assert_se(IS_ALIGNED16(&u16));
+ assert_se(IS_ALIGNED16(&u32));
+ assert_se(IS_ALIGNED16(&u64));
+ assert_se(IS_ALIGNED32(&u32));
+ assert_se(IS_ALIGNED32(&u64));
+ assert_se(IS_ALIGNED64(&u64));
+
+ _align_(32) uint8_t ua256;
+ _align_(8) uint8_t ua64;
+ _align_(4) uint8_t ua32;
+ _align_(2) uint8_t ua16;
+
+ assert_se(IS_ALIGNED16(&ua256));
+ assert_se(IS_ALIGNED32(&ua256));
+ assert_se(IS_ALIGNED64(&ua256));
+
+ assert_se(IS_ALIGNED16(&ua64));
+ assert_se(IS_ALIGNED32(&ua64));
+ assert_se(IS_ALIGNED64(&ua64));
+
+ assert_se(IS_ALIGNED16(&ua32));
+ assert_se(IS_ALIGNED32(&ua32));
+
+ assert_se(IS_ALIGNED16(&ua16));
+
+#ifdef __x86_64__
+ /* Conditionalized on x86-64, since there we know for sure that all three types are aligned to
+ * their size. Too lazy to figure it out for other archs */
+ void *p = UINT_TO_PTR(1); /* definitely not aligned */
+ assert_se(!IS_ALIGNED16(p));
+ assert_se(!IS_ALIGNED32(p));
+ assert_se(!IS_ALIGNED64(p));
+
+ assert_se(IS_ALIGNED16(ALIGN2_PTR(p)));
+ assert_se(IS_ALIGNED32(ALIGN4_PTR(p)));
+ assert_se(IS_ALIGNED64(ALIGN8_PTR(p)));
+
+ p = UINT_TO_PTR(-1); /* also definitely not aligned */
+ assert_se(!IS_ALIGNED16(p));
+ assert_se(!IS_ALIGNED32(p));
+ assert_se(!IS_ALIGNED64(p));
+#endif
+}
+
+TEST(FOREACH_ARRAY) {
+ int a[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+ int b[10] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
+ int x, n;
+
+ x = n = 0;
+ FOREACH_ARRAY(i, a, 10) {
+ x += *i;
+ n++;
+ }
+ assert_se(x == 45);
+ assert_se(n == 10);
+
+ x = n = 0;
+ FOREACH_ARRAY(i, a, 10)
+ FOREACH_ARRAY(j, b, 10) {
+ x += (*i) * (*j);
+ n++;
+ }
+ assert_se(x == 45 * 45);
+ assert_se(n == 10 * 10);
+
+ x = n = 0;
+ FOREACH_ARRAY(i, a, 5)
+ FOREACH_ARRAY(j, b, 5) {
+ x += (*i) * (*j);
+ n++;
+ }
+ assert_se(x == 10 * 35);
+ assert_se(n == 5 * 5);
+
+ x = n = 0;
+ FOREACH_ARRAY(i, a, 0)
+ FOREACH_ARRAY(j, b, 0) {
+ x += (*i) * (*j);
+ n++;
+ }
+ assert_se(x == 0);
+ assert_se(n == 0);
+
+ x = n = 0;
+ FOREACH_ARRAY(i, a, -1)
+ FOREACH_ARRAY(j, b, -1) {
+ x += (*i) * (*j);
+ n++;
+ }
+ assert_se(x == 0);
+ assert_se(n == 0);
+}
+
+#define TEST_ROUND_UP_BY_TYPE(type, max_value) \
+ ({ \
+ type x, y; \
+ x = 0, y = 1; \
+ assert_se(ROUND_UP(x, y) == 0); \
+ x = 0, y = 2; \
+ assert_se(ROUND_UP(x, y) == 0); \
+ x = 0, y = 3; \
+ assert_se(ROUND_UP(x, y) == 0); \
+ x = 0, y = 4; \
+ assert_se(ROUND_UP(x, y) == 0); \
+ x = 1, y = 1; \
+ assert_se(ROUND_UP(x, y) == 1); \
+ x = 1, y = 2; \
+ assert_se(ROUND_UP(x, y) == 2); \
+ x = 1, y = 3; \
+ assert_se(ROUND_UP(x, y) == 3); \
+ x = 1, y = 4; \
+ assert_se(ROUND_UP(x, y) == 4); \
+ x = 2, y = 1; \
+ assert_se(ROUND_UP(x, y) == 2); \
+ x = 2, y = 2; \
+ assert_se(ROUND_UP(x, y) == 2); \
+ x = 2, y = 3; \
+ assert_se(ROUND_UP(x, y) == 3); \
+ x = 2, y = 4; \
+ assert_se(ROUND_UP(x, y) == 4); \
+ x = 3, y = 1; \
+ assert_se(ROUND_UP(x, y) == 3); \
+ x = 3, y = 2; \
+ assert_se(ROUND_UP(x, y) == 4); \
+ x = 3, y = 3; \
+ assert_se(ROUND_UP(x, y) == 3); \
+ x = 3, y = 4; \
+ assert_se(ROUND_UP(x, y) == 4); \
+ x = 4, y = 1; \
+ assert_se(ROUND_UP(x, y) == 4); \
+ x = 4, y = 2; \
+ assert_se(ROUND_UP(x, y) == 4); \
+ x = 4, y = 3; \
+ assert_se(ROUND_UP(x, y) == 6); \
+ x = 4, y = 4; \
+ assert_se(ROUND_UP(x, y) == 4); \
+ x = max_value, y = 1; \
+ assert_se(ROUND_UP(x, y) == max_value); \
+ x = max_value, y = 2; \
+ assert_se(ROUND_UP(x, y) == max_value); \
+ x = max_value, y = 3; \
+ assert_se(ROUND_UP(x, y) == max_value); \
+ x = max_value, y = 4; \
+ assert_se(ROUND_UP(x, y) == max_value); \
+ x = max_value-1, y = 1; \
+ assert_se(ROUND_UP(x, y) == max_value-1); \
+ x = max_value-1, y = 2; \
+ assert_se(ROUND_UP(x, y) == max_value-1); \
+ x = max_value-1, y = 4; \
+ assert_se(ROUND_UP(x, y) == max_value); \
+ })
+
+TEST(ROUND_UP) {
+ TEST_ROUND_UP_BY_TYPE(uint8_t, UINT8_MAX);
+ TEST_ROUND_UP_BY_TYPE(uint16_t, UINT16_MAX);
+ TEST_ROUND_UP_BY_TYPE(uint32_t, UINT32_MAX);
+ TEST_ROUND_UP_BY_TYPE(uint64_t, UINT64_MAX);
+}
+
+TEST(u64_multiply_safe) {
+ assert_se(u64_multiply_safe(0, 0) == 0);
+ assert_se(u64_multiply_safe(10, 0) == 0);
+ assert_se(u64_multiply_safe(0, 10) == 0);
+ assert_se(u64_multiply_safe(10, 10) == 100);
+
+ assert_se(u64_multiply_safe(UINT64_MAX, 0) == 0);
+ assert_se(u64_multiply_safe(UINT64_MAX, 1) == UINT64_MAX);
+ assert_se(u64_multiply_safe(UINT64_MAX, 2) == 0);
+ assert_se(u64_multiply_safe(0, UINT64_MAX) == 0);
+ assert_se(u64_multiply_safe(1, UINT64_MAX) == UINT64_MAX);
+ assert_se(u64_multiply_safe(2, UINT64_MAX) == 0);
+
+ assert_se(u64_multiply_safe(UINT64_MAX / 2, 0) == 0);
+ assert_se(u64_multiply_safe(UINT64_MAX / 2, 1) == UINT64_MAX / 2);
+ assert_se(u64_multiply_safe(UINT64_MAX / 2, 2) == UINT64_MAX - 1);
+ assert_se(u64_multiply_safe(UINT64_MAX / 2, 3) == 0);
+ assert_se(u64_multiply_safe(0, UINT64_MAX / 2) == 0);
+ assert_se(u64_multiply_safe(1, UINT64_MAX / 2) == UINT64_MAX / 2);
+ assert_se(u64_multiply_safe(2, UINT64_MAX / 2) == UINT64_MAX - 1);
+ assert_se(u64_multiply_safe(3, UINT64_MAX / 2) == 0);
+
+ assert_se(u64_multiply_safe(UINT64_MAX, UINT64_MAX) == 0);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-manager.c b/src/test/test-manager.c
new file mode 100644
index 0000000..76e094b
--- /dev/null
+++ b/src/test/test-manager.c
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "manager.h"
+#include "tests.h"
+
+TEST(manager_taint_string) {
+ Manager m = {};
+
+ _cleanup_free_ char *a = manager_taint_string(&m);
+ assert_se(a);
+ log_debug("taint string: '%s'", a);
+
+ if (cg_all_unified() == 0)
+ assert_se(strstr(a, "cgroupsv1"));
+ else
+ assert_se(!strstr(a, "cgroupsv1"));
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-math-util.c b/src/test/test-math-util.c
new file mode 100644
index 0000000..9771576
--- /dev/null
+++ b/src/test/test-math-util.c
@@ -0,0 +1,110 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <float.h>
+
+#include "math-util.h"
+#include "tests.h"
+
+TEST(iszero_safe) {
+ /* zeros */
+ assert_se(iszero_safe(0.0));
+ assert_se(iszero_safe(-0.0));
+ assert_se(iszero_safe(0e0));
+ assert_se(iszero_safe(-0e0));
+ assert_se(iszero_safe(0e+0));
+ assert_se(iszero_safe(0e-0));
+ assert_se(iszero_safe(-0e-0));
+ assert_se(iszero_safe(-0e000));
+ assert_se(iszero_safe(0e000));
+
+ /* non-zero normal values */
+ assert_se(!iszero_safe(42.0));
+ assert_se(!iszero_safe(M_PI));
+ assert_se(!iszero_safe(DBL_MAX));
+ assert_se(!iszero_safe(-DBL_MAX));
+ assert_se(!iszero_safe(DBL_MIN));
+ assert_se(!iszero_safe(-DBL_MIN));
+ assert_se(!iszero_safe(1 / DBL_MAX));
+
+ /* subnormal values */
+ assert_se(!iszero_safe(DBL_MIN / 2));
+ assert_se(!iszero_safe(-DBL_MIN / 42));
+ assert_se(!iszero_safe(1 / DBL_MAX / 2));
+
+ /* too small values which cannot be in subnormal form */
+ assert_se( iszero_safe(DBL_MIN / DBL_MAX));
+ assert_se( iszero_safe(DBL_MIN / -DBL_MAX));
+ assert_se( iszero_safe(-DBL_MIN / DBL_MAX));
+ assert_se( iszero_safe(-DBL_MIN / -DBL_MAX));
+
+ /* NaN or infinity */
+ assert_se(!iszero_safe(NAN));
+ assert_se(!iszero_safe(INFINITY));
+ assert_se(!iszero_safe(-INFINITY));
+ assert_se(!iszero_safe(1 / NAN));
+
+ /* inverse of infinity */
+ assert_se( iszero_safe(1 / INFINITY));
+ assert_se( iszero_safe(1 / -INFINITY));
+ assert_se( iszero_safe(-1 / INFINITY));
+ assert_se( iszero_safe(-1 / -INFINITY));
+ assert_se( iszero_safe(42 / -INFINITY));
+ assert_se( iszero_safe(-42 / -INFINITY));
+ assert_se( iszero_safe(DBL_MIN / INFINITY));
+ assert_se( iszero_safe(DBL_MIN / -INFINITY));
+ assert_se( iszero_safe(DBL_MAX / INFINITY / 2));
+ assert_se( iszero_safe(DBL_MAX / -INFINITY * DBL_MAX));
+
+ /* infinity / infinity is NaN */
+ assert_se(!iszero_safe(INFINITY / INFINITY));
+ assert_se(!iszero_safe(INFINITY * 2 / INFINITY));
+ assert_se(!iszero_safe(INFINITY / DBL_MAX / INFINITY));
+}
+
+TEST(fp_equal) {
+ /* normal values */
+ assert_se( fp_equal(0.0, -0e0));
+ assert_se( fp_equal(3.0, 3));
+ assert_se(!fp_equal(3.000001, 3));
+ assert_se( fp_equal(M_PI, M_PI));
+ assert_se(!fp_equal(M_PI, -M_PI));
+ assert_se( fp_equal(DBL_MAX, DBL_MAX));
+ assert_se(!fp_equal(DBL_MAX, -DBL_MAX));
+ assert_se(!fp_equal(-DBL_MAX, DBL_MAX));
+ assert_se( fp_equal(-DBL_MAX, -DBL_MAX));
+ assert_se( fp_equal(DBL_MIN, DBL_MIN));
+ assert_se(!fp_equal(DBL_MIN, -DBL_MIN));
+ assert_se(!fp_equal(-DBL_MIN, DBL_MIN));
+ assert_se( fp_equal(-DBL_MIN, -DBL_MIN));
+
+ /* subnormal values */
+ assert_se( fp_equal(DBL_MIN / 10, DBL_MIN / 10));
+ assert_se(!fp_equal(DBL_MIN / 10, -DBL_MIN / 10));
+ assert_se(!fp_equal(-DBL_MIN / 10, DBL_MIN / 10));
+ assert_se( fp_equal(-DBL_MIN / 10, -DBL_MIN / 10));
+ assert_se(!fp_equal(DBL_MIN / 10, DBL_MIN / 15));
+ assert_se(!fp_equal(DBL_MIN / 10, DBL_MIN / 15));
+
+ /* subnormal difference */
+ assert_se(!fp_equal(DBL_MIN / 10, DBL_MIN + DBL_MIN / 10));
+ assert_se( fp_equal(3.0, 3.0 + DBL_MIN / 2)); /* 3.0 + DBL_MIN / 2 is truncated to 3.0 */
+
+ /* too small values */
+ assert_se( fp_equal(DBL_MIN / DBL_MAX, -DBL_MIN / DBL_MAX));
+
+ /* NaN or infinity */
+ assert_se(!fp_equal(NAN, NAN));
+ assert_se(!fp_equal(NAN, 0));
+ assert_se(!fp_equal(NAN, INFINITY));
+ assert_se(!fp_equal(INFINITY, INFINITY));
+ assert_se(!fp_equal(INFINITY, -INFINITY));
+ assert_se(!fp_equal(-INFINITY, INFINITY));
+ assert_se(!fp_equal(-INFINITY, -INFINITY));
+
+ /* inverse of infinity */
+ assert_se( fp_equal(0, 1 / INFINITY));
+ assert_se( fp_equal(42 / INFINITY, 1 / -INFINITY));
+ assert_se(!fp_equal(42 / INFINITY, INFINITY / INFINITY));
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-memfd-util.c b/src/test/test-memfd-util.c
new file mode 100644
index 0000000..f8e1b46
--- /dev/null
+++ b/src/test/test-memfd-util.c
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <unistd.h>
+
+#include "errno-util.h"
+#include "fd-util.h"
+#include "memfd-util.h"
+#include "string-util.h"
+#include "tests.h"
+
+TEST(memfd_get_sealed) {
+#define TEST_TEXT "this is some random test text we are going to write to a memfd"
+ _cleanup_close_ int fd = -EBADF;
+
+ fd = memfd_new("test-memfd-get-sealed");
+ if (fd < 0) {
+ assert_se(ERRNO_IS_NOT_SUPPORTED(fd));
+ return;
+ }
+
+ assert_se(write(fd, TEST_TEXT, strlen(TEST_TEXT)) == strlen(TEST_TEXT));
+ /* we'll leave the read offset at the end of the memfd, the fdopen_independent() descriptors should
+ * start at the beginning anyway */
+
+ assert_se(memfd_get_sealed(fd) == 0);
+ assert_se(memfd_set_sealed(fd) >= 0);
+ assert_se(memfd_get_sealed(fd) > 0);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-memory-util.c b/src/test/test-memory-util.c
new file mode 100644
index 0000000..cd4b64a
--- /dev/null
+++ b/src/test/test-memory-util.c
@@ -0,0 +1,125 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "memory-util.h"
+#include "tests.h"
+
+TEST(eqzero) {
+ const uint32_t zeros[] = {0, 0, 0};
+ const uint32_t ones[] = {1, 1};
+ const uint32_t mixed[] = {0, 1, 0, 0, 0};
+ const uint8_t longer[] = {[55] = 255};
+
+ assert_se(eqzero(zeros));
+ assert_se(!eqzero(ones));
+ assert_se(!eqzero(mixed));
+ assert_se(!eqzero(longer));
+}
+
+static void my_destructor(struct iovec *iov, size_t n) {
+ /* not really a destructor, just something we can use to check if the destruction worked */
+ memset(iov, 'y', sizeof(struct iovec) * n);
+}
+
+TEST(cleanup_array) {
+ struct iovec *iov, *saved_iov;
+ size_t n, saved_n;
+
+ n = 7;
+ iov = new(struct iovec, n);
+ assert_se(iov);
+
+ memset(iov, 'x', sizeof(struct iovec) * n);
+
+ saved_iov = iov;
+ saved_n = n;
+
+ {
+ assert_se(memeqbyte('x', saved_iov, sizeof(struct iovec) * saved_n));
+ assert_se(iov);
+ assert_se(n > 0);
+
+ CLEANUP_ARRAY(iov, n, my_destructor);
+
+ assert_se(memeqbyte('x', saved_iov, sizeof(struct iovec) * saved_n));
+ assert_se(iov);
+ assert_se(n > 0);
+ }
+
+ assert_se(memeqbyte('y', saved_iov, sizeof(struct iovec) * saved_n));
+ assert_se(!iov);
+ assert_se(n == 0);
+
+ free(saved_iov);
+}
+
+TEST(page_align) {
+ assert_se(PAGE_ALIGN(page_size() - 1) == page_size());
+ assert_se(PAGE_ALIGN(page_size() ) == page_size());
+ assert_se(PAGE_ALIGN(page_size() + 1) == page_size() * 2);
+ assert_se(PAGE_ALIGN(page_size() * 123 - 1) == page_size() * 123);
+ assert_se(PAGE_ALIGN(page_size() * 123 ) == page_size() * 123);
+ assert_se(PAGE_ALIGN(page_size() * 123 + 1) == page_size() * 124);
+ assert_se(PAGE_ALIGN(SIZE_MAX - page_size() - 1) == SIZE_MAX - page_size() + 1);
+ assert_se(PAGE_ALIGN(SIZE_MAX - page_size() ) == SIZE_MAX - page_size() + 1);
+ assert_se(PAGE_ALIGN(SIZE_MAX - page_size() + 1) == SIZE_MAX - page_size() + 1);
+ assert_se(PAGE_ALIGN(SIZE_MAX - page_size() + 2) == SIZE_MAX); /* overflow */
+ assert_se(PAGE_ALIGN(SIZE_MAX) == SIZE_MAX); /* overflow */
+
+ assert_se(PAGE_ALIGN_U64(page_size() - 1) == page_size());
+ assert_se(PAGE_ALIGN_U64(page_size() ) == page_size());
+ assert_se(PAGE_ALIGN_U64(page_size() + 1) == page_size() * 2);
+ assert_se(PAGE_ALIGN_U64(page_size() * 123 - 1) == page_size() * 123);
+ assert_se(PAGE_ALIGN_U64(page_size() * 123 ) == page_size() * 123);
+ assert_se(PAGE_ALIGN_U64(page_size() * 123 + 1) == page_size() * 124);
+ assert_se(PAGE_ALIGN_U64(UINT64_MAX - page_size() - 1) == UINT64_MAX - page_size() + 1);
+ assert_se(PAGE_ALIGN_U64(UINT64_MAX - page_size() ) == UINT64_MAX - page_size() + 1);
+ assert_se(PAGE_ALIGN_U64(UINT64_MAX - page_size() + 1) == UINT64_MAX - page_size() + 1);
+ assert_se(PAGE_ALIGN_U64(UINT64_MAX - page_size() + 2) == UINT64_MAX); /* overflow */
+ assert_se(PAGE_ALIGN_U64(UINT64_MAX) == UINT64_MAX); /* overflow */
+
+ assert_se(PAGE_ALIGN_DOWN(page_size() - 1) == 0);
+ assert_se(PAGE_ALIGN_DOWN(page_size() ) == page_size());
+ assert_se(PAGE_ALIGN_DOWN(page_size() + 1) == page_size());
+ assert_se(PAGE_ALIGN_DOWN(page_size() * 123 - 1) == page_size() * 122);
+ assert_se(PAGE_ALIGN_DOWN(page_size() * 123 ) == page_size() * 123);
+ assert_se(PAGE_ALIGN_DOWN(page_size() * 123 + 1) == page_size() * 123);
+ assert_se(PAGE_ALIGN_DOWN(SIZE_MAX - page_size() - 1) == SIZE_MAX - page_size() * 2 + 1);
+ assert_se(PAGE_ALIGN_DOWN(SIZE_MAX - page_size() ) == SIZE_MAX - page_size() * 2 + 1);
+ assert_se(PAGE_ALIGN_DOWN(SIZE_MAX - page_size() + 1) == SIZE_MAX - page_size() + 1);
+ assert_se(PAGE_ALIGN_DOWN(SIZE_MAX - page_size() + 2) == SIZE_MAX - page_size() + 1);
+
+ assert_se(PAGE_ALIGN_DOWN_U64(page_size() - 1) == 0);
+ assert_se(PAGE_ALIGN_DOWN_U64(page_size() ) == page_size());
+ assert_se(PAGE_ALIGN_DOWN_U64(page_size() + 1) == page_size());
+ assert_se(PAGE_ALIGN_DOWN_U64(page_size() * 123 - 1) == page_size() * 122);
+ assert_se(PAGE_ALIGN_DOWN_U64(page_size() * 123 ) == page_size() * 123);
+ assert_se(PAGE_ALIGN_DOWN_U64(page_size() * 123 + 1) == page_size() * 123);
+ assert_se(PAGE_ALIGN_DOWN_U64(SIZE_MAX - page_size() - 1) == SIZE_MAX - page_size() * 2 + 1);
+ assert_se(PAGE_ALIGN_DOWN_U64(SIZE_MAX - page_size() ) == SIZE_MAX - page_size() * 2 + 1);
+ assert_se(PAGE_ALIGN_DOWN_U64(SIZE_MAX - page_size() + 1) == SIZE_MAX - page_size() + 1);
+ assert_se(PAGE_ALIGN_DOWN_U64(SIZE_MAX - page_size() + 2) == SIZE_MAX - page_size() + 1);
+
+ assert_se(PAGE_OFFSET(page_size() - 1) == page_size() - 1);
+ assert_se(PAGE_OFFSET(page_size() ) == 0);
+ assert_se(PAGE_OFFSET(page_size() + 1) == 1);
+ assert_se(PAGE_OFFSET(page_size() * 123 - 1) == page_size() - 1);
+ assert_se(PAGE_OFFSET(page_size() * 123 ) == 0);
+ assert_se(PAGE_OFFSET(page_size() * 123 + 1) == 1);
+ assert_se(PAGE_OFFSET(SIZE_MAX - page_size() - 1) == page_size() - 2);
+ assert_se(PAGE_OFFSET(SIZE_MAX - page_size() ) == page_size() - 1);
+ assert_se(PAGE_OFFSET(SIZE_MAX - page_size() + 1) == 0);
+ assert_se(PAGE_OFFSET(SIZE_MAX - page_size() + 2) == 1);
+
+ assert_se(PAGE_OFFSET_U64(page_size() - 1) == page_size() - 1);
+ assert_se(PAGE_OFFSET_U64(page_size() ) == 0);
+ assert_se(PAGE_OFFSET_U64(page_size() + 1) == 1);
+ assert_se(PAGE_OFFSET_U64(page_size() * 123 - 1) == page_size() - 1);
+ assert_se(PAGE_OFFSET_U64(page_size() * 123 ) == 0);
+ assert_se(PAGE_OFFSET_U64(page_size() * 123 + 1) == 1);
+ assert_se(PAGE_OFFSET_U64(UINT64_MAX - page_size() - 1) == page_size() - 2);
+ assert_se(PAGE_OFFSET_U64(UINT64_MAX - page_size() ) == page_size() - 1);
+ assert_se(PAGE_OFFSET_U64(UINT64_MAX - page_size() + 1) == 0);
+ assert_se(PAGE_OFFSET_U64(UINT64_MAX - page_size() + 2) == 1);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-mempool.c b/src/test/test-mempool.c
new file mode 100644
index 0000000..d3bc173
--- /dev/null
+++ b/src/test/test-mempool.c
@@ -0,0 +1,92 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "mempool.h"
+#include "random-util.h"
+#include "tests.h"
+
+struct element {
+ uint64_t value;
+};
+
+DEFINE_MEMPOOL(test_mempool, struct element, 8);
+
+TEST(mempool_trim) {
+
+#define NN 4000
+ struct element *a[NN];
+ size_t n_freed = 0;
+
+ assert_se(!test_mempool.first_pool);
+ assert_se(!test_mempool.freelist);
+
+ mempool_trim(&test_mempool);
+
+ for (size_t i = 0; i < NN; i++) {
+ assert_se(a[i] = mempool_alloc_tile(&test_mempool));
+ a[i]->value = i;
+ }
+
+ mempool_trim(&test_mempool);
+
+ /* free up to one third randomly */
+ size_t x = 0;
+ for (size_t i = 0; i < NN/3; i++) {
+ x = (x + random_u64()) % ELEMENTSOF(a);
+ assert_se(!a[x] || a[x]->value == x);
+
+ if (a[x])
+ n_freed ++;
+
+ a[x] = mempool_free_tile(&test_mempool, a[x]);
+ }
+
+ mempool_trim(&test_mempool);
+
+ /* free definitely at least one third */
+ for (size_t i = 2; i < NN; i += 3) {
+ assert_se(!a[i] || a[i]->value == i);
+ if (a[i])
+ n_freed ++;
+ a[i] = mempool_free_tile(&test_mempool, a[i]);
+ }
+
+ mempool_trim(&test_mempool);
+
+ /* Allocate another set of tiles, which will fill up the free list and allocate some new tiles */
+ struct element *b[NN];
+ for (size_t i = 0; i < NN; i++) {
+ assert_se(b[i] = mempool_alloc_tile(&test_mempool));
+ b[i]->value = ~(uint64_t) i;
+ }
+
+ mempool_trim(&test_mempool);
+
+ /* free everything from the original set*/
+
+ for (size_t i = 0; i < NN; i += 1) {
+ assert_se(!a[i] || a[i]->value == i);
+ if (a[i])
+ n_freed ++;
+ a[i] = mempool_free_tile(&test_mempool, a[i]);
+ }
+
+ mempool_trim(&test_mempool);
+
+ /* and now everything from the second set too */
+
+ for (size_t i = 0; i < NN; i += 1) {
+ assert_se(!b[i] || b[i]->value == ~(uint64_t) i);
+ if (b[i])
+ n_freed ++;
+ b[i] = mempool_free_tile(&test_mempool, b[i]);
+ }
+
+ assert_se(n_freed == NN * 2);
+
+ mempool_trim(&test_mempool);
+
+ assert_se(!test_mempool.first_pool);
+ assert_se(!test_mempool.freelist);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-mempress.c b/src/test/test-mempress.c
new file mode 100644
index 0000000..26ce4ce
--- /dev/null
+++ b/src/test/test-mempress.c
@@ -0,0 +1,309 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <pthread.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include <sd-bus.h>
+#include <sd-event.h>
+
+#include "bus-locator.h"
+#include "bus-wait-for-jobs.h"
+#include "fd-util.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "random-util.h"
+#include "rm-rf.h"
+#include "signal-util.h"
+#include "socket-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+#include "unit-def.h"
+
+struct fake_pressure_context {
+ int fifo_fd;
+ int socket_fd;
+};
+
+static void *fake_pressure_thread(void *p) {
+ _cleanup_free_ struct fake_pressure_context *c = ASSERT_PTR(p);
+ _cleanup_close_ int cfd = -EBADF;
+
+ usleep_safe(150);
+
+ assert_se(write(c->fifo_fd, &(const char) { 'x' }, 1) == 1);
+
+ usleep_safe(150);
+
+ cfd = accept4(c->socket_fd, NULL, NULL, SOCK_CLOEXEC);
+ assert_se(cfd >= 0);
+ char buf[STRLEN("hello")+1] = {};
+ assert_se(read(cfd, buf, sizeof(buf)-1) == sizeof(buf)-1);
+ assert_se(streq(buf, "hello"));
+ assert_se(write(cfd, &(const char) { 'z' }, 1) == 1);
+
+ return 0;
+}
+
+static int fake_pressure_callback(sd_event_source *s, void *userdata) {
+ int *value = userdata;
+ const char *d;
+
+ assert_se(s);
+ assert_se(sd_event_source_get_description(s, &d) >= 0);
+
+ *value *= d[0];
+
+ log_notice("memory pressure event: %s", d);
+
+ if (*value == 7 * 'f' * 's')
+ assert_se(sd_event_exit(sd_event_source_get_event(s), 0) >= 0);
+
+ return 0;
+}
+
+TEST(fake_pressure) {
+ _cleanup_(sd_event_source_unrefp) sd_event_source *es = NULL, *ef = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ _cleanup_free_ char *j = NULL, *k = NULL;
+ _cleanup_(rm_rf_physical_and_freep) char *tmp = NULL;
+ _cleanup_close_ int fifo_fd = -EBADF, socket_fd = -EBADF;
+ union sockaddr_union sa;
+ pthread_t th;
+ int value = 7;
+
+ assert_se(sd_event_default(&e) >= 0);
+
+ assert_se(mkdtemp_malloc(NULL, &tmp) >= 0);
+
+ assert_se(j = path_join(tmp, "fifo"));
+ assert_se(mkfifo(j, 0600) >= 0);
+ fifo_fd = open(j, O_CLOEXEC|O_RDWR|O_NONBLOCK);
+ assert_se(fifo_fd >= 0);
+
+ assert_se(k = path_join(tmp, "sock"));
+ socket_fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0);
+ assert_se(socket_fd >= 0);
+ assert_se(sockaddr_un_set_path(&sa.un, k) >= 0);
+ assert_se(bind(socket_fd, &sa.sa, SOCKADDR_UN_LEN(sa.un)) >= 0);
+ assert_se(listen(socket_fd, 1) >= 0);
+
+ /* Ideally we'd just allocate this on the stack, but AddressSanitizer doesn't like it if threads
+ * access each other's stack */
+ struct fake_pressure_context *fp = new(struct fake_pressure_context, 1);
+ assert_se(fp);
+ *fp = (struct fake_pressure_context) {
+ .fifo_fd = fifo_fd,
+ .socket_fd = socket_fd,
+ };
+
+ assert_se(pthread_create(&th, NULL, fake_pressure_thread, TAKE_PTR(fp)) == 0);
+
+ assert_se(setenv("MEMORY_PRESSURE_WATCH", j, /* override= */ true) >= 0);
+ assert_se(unsetenv("MEMORY_PRESSURE_WRITE") >= 0);
+
+ assert_se(sd_event_add_memory_pressure(e, &es, fake_pressure_callback, &value) >= 0);
+ assert_se(sd_event_source_set_description(es, "fifo event source") >= 0);
+
+ assert_se(setenv("MEMORY_PRESSURE_WATCH", k, /* override= */ true) >= 0);
+ assert_se(setenv("MEMORY_PRESSURE_WRITE", "aGVsbG8K", /* override= */ true) >= 0);
+
+ assert_se(sd_event_add_memory_pressure(e, &ef, fake_pressure_callback, &value) >= 0);
+ assert_se(sd_event_source_set_description(ef, "socket event source") >= 0);
+
+ assert_se(sd_event_loop(e) >= 0);
+
+ assert_se(value == 7 * 'f' * 's');
+
+ assert_se(pthread_join(th, NULL) == 0);
+}
+
+struct real_pressure_context {
+ sd_event_source *pid;
+};
+
+static int real_pressure_callback(sd_event_source *s, void *userdata) {
+ struct real_pressure_context *c = ASSERT_PTR(userdata);
+ const char *d;
+
+ assert_se(s);
+ assert_se(sd_event_source_get_description(s, &d) >= 0);
+
+ log_notice("real_memory pressure event: %s", d);
+
+ sd_event_trim_memory();
+
+ assert_se(c->pid);
+ assert_se(sd_event_source_send_child_signal(c->pid, SIGKILL, NULL, 0) >= 0);
+ c->pid = NULL;
+
+ return 0;
+}
+
+#define MMAP_SIZE (10 * 1024 * 1024)
+
+_noreturn_ static void real_pressure_eat_memory(int pipe_fd) {
+ size_t ate = 0;
+
+ /* Allocates and touches 10M at a time, until runs out of memory */
+
+ char x;
+ assert_se(read(pipe_fd, &x, 1) == 1); /* Wait for the GO! */
+
+ for (;;) {
+ void *p;
+
+ p = mmap(NULL, MMAP_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
+ assert_se(p != MAP_FAILED);
+
+ log_info("Eating another %s.", FORMAT_BYTES(MMAP_SIZE));
+
+ memset(p, random_u32() & 0xFF, MMAP_SIZE);
+ ate += MMAP_SIZE;
+
+ log_info("Ate %s in total.", FORMAT_BYTES(ate));
+
+ usleep_safe(50 * USEC_PER_MSEC);
+ }
+}
+
+static int real_pressure_child_callback(sd_event_source *s, const siginfo_t *si, void *userdata) {
+ assert_se(s);
+ assert_se(si);
+
+ log_notice("child dead");
+
+ assert_se(si->si_signo == SIGCHLD);
+ assert_se(si->si_status == SIGKILL);
+ assert_se(si->si_code == CLD_KILLED);
+
+ assert_se(sd_event_exit(sd_event_source_get_event(s), 31) >= 0);
+ return 0;
+}
+
+TEST(real_pressure) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_(sd_event_source_unrefp) sd_event_source *es = NULL, *cs = NULL;
+ _cleanup_(bus_wait_for_jobs_freep) BusWaitForJobs *w = NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ _cleanup_close_pair_ int pipe_fd[2] = EBADF_PAIR;
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ _cleanup_free_ char *scope = NULL;
+ const char *object;
+ int r;
+ pid_t pid;
+
+ r = sd_bus_open_system(&bus);
+ if (r < 0) {
+ log_notice_errno(r, "Can't connect to system bus, skipping test: %m");
+ return;
+ }
+
+ assert_se(bus_wait_for_jobs_new(bus, &w) >= 0);
+
+ assert_se(bus_message_new_method_call(bus, &m, bus_systemd_mgr, "StartTransientUnit") >= 0);
+ assert_se(asprintf(&scope, "test-%" PRIu64 ".scope", random_u64()) >= 0);
+ assert_se(sd_bus_message_append(m, "ss", scope, "fail") >= 0);
+ assert_se(sd_bus_message_open_container(m, 'a', "(sv)") >= 0);
+ assert_se(sd_bus_message_append(m, "(sv)", "PIDs", "au", 1, 0) >= 0);
+ assert_se(sd_bus_message_append(m, "(sv)", "MemoryAccounting", "b", true) >= 0);
+ assert_se(sd_bus_message_close_container(m) >= 0);
+ assert_se(sd_bus_message_append(m, "a(sa(sv))", 0) >= 0);
+
+ r = sd_bus_call(bus, m, 0, &error, &reply);
+ if (r < 0) {
+ log_notice_errno(r, "Can't issue transient unit call, skipping test: %m");
+ return;
+ }
+
+ assert_se(sd_bus_message_read(reply, "o", &object) >= 0);
+
+ assert_se(bus_wait_for_jobs_one(w, object, /* quiet= */ false, /* extra_args= */ NULL) >= 0);
+
+ assert_se(sd_event_default(&e) >= 0);
+
+ assert_se(pipe2(pipe_fd, O_CLOEXEC) >= 0);
+
+ r = safe_fork("(eat-memory)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM, &pid);
+ assert_se(r >= 0);
+ if (r == 0) {
+ real_pressure_eat_memory(pipe_fd[0]);
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD, -1) >= 0);
+ assert_se(sd_event_add_child(e, &cs, pid, WEXITED, real_pressure_child_callback, NULL) >= 0);
+ assert_se(sd_event_source_set_child_process_own(cs, true) >= 0);
+
+ assert_se(unsetenv("MEMORY_PRESSURE_WATCH") >= 0);
+ assert_se(unsetenv("MEMORY_PRESSURE_WRITE") >= 0);
+
+ struct real_pressure_context context = {
+ .pid = cs,
+ };
+
+ r = sd_event_add_memory_pressure(e, &es, real_pressure_callback, &context);
+ if (r < 0) {
+ log_notice_errno(r, "Can't allocate memory pressure fd, skipping test: %m");
+ return;
+ }
+
+ assert_se(sd_event_source_set_description(es, "real pressure event source") >= 0);
+ assert_se(sd_event_source_set_memory_pressure_type(es, "some") == 0);
+ assert_se(sd_event_source_set_memory_pressure_type(es, "full") > 0);
+ assert_se(sd_event_source_set_memory_pressure_type(es, "full") == 0);
+ assert_se(sd_event_source_set_memory_pressure_type(es, "some") > 0);
+ assert_se(sd_event_source_set_memory_pressure_type(es, "some") == 0);
+ assert_se(sd_event_source_set_memory_pressure_period(es, 70 * USEC_PER_MSEC, USEC_PER_SEC) > 0);
+ assert_se(sd_event_source_set_memory_pressure_period(es, 70 * USEC_PER_MSEC, USEC_PER_SEC) == 0);
+ assert_se(sd_event_source_set_enabled(es, SD_EVENT_ONESHOT) >= 0);
+
+ _cleanup_free_ char *uo = NULL;
+ assert_se(uo = unit_dbus_path_from_name(scope));
+
+ uint64_t mcurrent = UINT64_MAX;
+ assert_se(sd_bus_get_property_trivial(bus, "org.freedesktop.systemd1", uo, "org.freedesktop.systemd1.Scope", "MemoryCurrent", &error, 't', &mcurrent) >= 0);
+
+ printf("current: %" PRIu64 "\n", mcurrent);
+ if (mcurrent == UINT64_MAX) {
+ log_notice_errno(r, "Memory accounting not available, skipping test: %m");
+ return;
+ }
+
+ m = sd_bus_message_unref(m);
+
+ assert_se(bus_message_new_method_call(bus, &m, bus_systemd_mgr, "SetUnitProperties") >= 0);
+ assert_se(sd_bus_message_append(m, "sb", scope, true) >= 0);
+ assert_se(sd_bus_message_open_container(m, 'a', "(sv)") >= 0);
+ assert_se(sd_bus_message_append(m, "(sv)", "MemoryHigh", "t", mcurrent + (15 * 1024 * 1024)) >= 0);
+ assert_se(sd_bus_message_append(m, "(sv)", "MemoryMax", "t", mcurrent + (50 * 1024 * 1024)) >= 0);
+ assert_se(sd_bus_message_close_container(m) >= 0);
+
+ assert_se(sd_bus_call(bus, m, 0, NULL, NULL) >= 0);
+
+ /* Generate some memory allocations via mempool */
+#define NN (1024)
+ Hashmap **h = new(Hashmap*, NN);
+ for (int i = 0; i < NN; i++)
+ h[i] = hashmap_new(NULL);
+ for (int i = 0; i < NN; i++)
+ hashmap_free(h[i]);
+ free(h);
+
+ /* Now start eating memory */
+ assert_se(write(pipe_fd[1], &(const char) { 'x' }, 1) == 1);
+
+ assert_se(sd_event_loop(e) >= 0);
+ int ex = 0;
+ assert_se(sd_event_get_exit_code(e, &ex) >= 0);
+ assert_se(ex == 31);
+}
+
+static int outro(void) {
+ hashmap_trim_pools();
+ return 0;
+}
+
+DEFINE_TEST_MAIN_FULL(LOG_DEBUG, NULL, outro);
diff --git a/src/test/test-memstream-util.c b/src/test/test-memstream-util.c
new file mode 100644
index 0000000..254bdca
--- /dev/null
+++ b/src/test/test-memstream-util.c
@@ -0,0 +1,60 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "memstream-util.h"
+#include "string-util.h"
+#include "tests.h"
+
+TEST(memstream_done) {
+ _cleanup_(memstream_done) MemStream m = {};
+
+ assert_se(memstream_init(&m));
+}
+
+TEST(memstream_empty) {
+ _cleanup_(memstream_done) MemStream m = {};
+ _cleanup_free_ char *buf = NULL;
+ size_t sz;
+
+ assert_se(memstream_init(&m));
+ assert_se(memstream_finalize(&m, &buf, &sz) >= 0);
+ assert_se(streq(buf, ""));
+ assert_se(sz == 0);
+}
+
+TEST(memstream) {
+ _cleanup_(memstream_done) MemStream m = {};
+ _cleanup_free_ char *buf = NULL;
+ size_t sz;
+ FILE *f;
+
+ assert_se(f = memstream_init(&m));
+ fputs("hoge", f);
+ fputs("おはよう!", f);
+ fputs(u8"😀😀😀", f);
+ assert_se(memstream_finalize(&m, &buf, &sz) >= 0);
+ assert_se(streq(buf, u8"hogeおはよう!😀😀😀"));
+ assert_se(sz == strlen(u8"hogeおはよう!😀😀😀"));
+
+ buf = mfree(buf);
+
+ assert_se(f = memstream_init(&m));
+ fputs("second", f);
+ assert_se(memstream_finalize(&m, &buf, &sz) >= 0);
+ assert_se(streq(buf, "second"));
+ assert_se(sz == strlen("second"));
+}
+
+TEST(memstream_dump) {
+ _cleanup_(memstream_done) MemStream m = {};
+ FILE *f;
+
+ assert_se(f = memstream_init(&m));
+ fputs("first", f);
+ assert_se(memstream_dump(LOG_DEBUG, &m) >= 0);
+
+ assert_se(f = memstream_init(&m));
+ fputs("second", f);
+ assert_se(memstream_dump(LOG_DEBUG, &m) >= 0);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-mkdir.c b/src/test/test-mkdir.c
new file mode 100644
index 0000000..4820b32
--- /dev/null
+++ b/src/test/test-mkdir.c
@@ -0,0 +1,141 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <unistd.h>
+
+#include "capability-util.h"
+#include "fs-util.h"
+#include "mkdir.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "rm-rf.h"
+#include "stat-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+#include "user-util.h"
+
+TEST(mkdir_p_safe) {
+ _cleanup_(rm_rf_physical_and_freep) char *tmp = NULL;
+ _cleanup_free_ char *p = NULL, *q = NULL;
+ int r;
+
+ assert_se(mkdtemp_malloc("/tmp/test-mkdir-XXXXXX", &tmp) >= 0);
+
+ assert_se(p = path_join(tmp, "run/aaa/bbb"));
+ assert_se(mkdir_p(p, 0755) >= 0);
+ assert_se(is_dir(p, false) > 0);
+ assert_se(is_dir(p, true) > 0);
+
+ p = mfree(p);
+ assert_se(p = path_join(tmp, "run/ccc/ddd"));
+ assert_se(mkdir_p_safe(tmp, p, 0755, UID_INVALID, GID_INVALID, 0) >= 0);
+ assert_se(is_dir(p, false) > 0);
+ assert_se(is_dir(p, true) > 0);
+
+ p = mfree(p);
+ assert_se(p = path_join(tmp, "var/run"));
+ assert_se(mkdir_parents_safe(tmp, p, 0755, UID_INVALID, GID_INVALID, 0) >= 0);
+ assert_se(symlink("../run", p) >= 0);
+ assert_se(is_dir(p, false) == 0);
+ assert_se(is_dir(p, true) > 0);
+
+ assert_se(mkdir_safe(p, 0755, UID_INVALID, GID_INVALID, 0) == -ENOTDIR);
+ assert_se(mkdir_safe(p, 0755, UID_INVALID, GID_INVALID, MKDIR_IGNORE_EXISTING) >= 0);
+ assert_se(mkdir_safe(p, 0755, UID_INVALID, GID_INVALID, MKDIR_FOLLOW_SYMLINK) >= 0);
+ assert_se(is_dir(p, false) == 0);
+ assert_se(is_dir(p, true) > 0);
+
+ p = mfree(p);
+ assert_se(p = path_join(tmp, "var/run/hoge/foo/baz"));
+ assert_se(mkdir_p_safe(tmp, p, 0755, UID_INVALID, GID_INVALID, 0) >= 0);
+ assert_se(is_dir(p, false) > 0);
+ assert_se(is_dir(p, true) > 0);
+
+ p = mfree(p);
+ assert_se(p = path_join(tmp, "not-exists"));
+ assert_se(q = path_join(p, "aaa"));
+ assert_se(mkdir_p_safe(p, q, 0755, UID_INVALID, GID_INVALID, 0) == -ENOENT);
+
+ p = mfree(p);
+ q = mfree(q);
+ assert_se(p = path_join(tmp, "regular-file"));
+ assert_se(q = path_join(p, "aaa"));
+ assert_se(touch(p) >= 0);
+ assert_se(mkdir_p_safe(p, q, 0755, UID_INVALID, GID_INVALID, 0) == -ENOTDIR);
+
+ p = mfree(p);
+ q = mfree(q);
+ assert_se(p = path_join(tmp, "symlink"));
+ assert_se(q = path_join(p, "hoge/foo"));
+ assert_se(symlink("aaa", p) >= 0);
+ assert_se(mkdir_p_safe(tmp, q, 0755, UID_INVALID, GID_INVALID, 0) >= 0);
+ assert_se(is_dir(q, false) > 0);
+ assert_se(is_dir(q, true) > 0);
+ q = mfree(q);
+ assert_se(q = path_join(tmp, "aaa/hoge/foo"));
+ assert_se(is_dir(q, false) > 0);
+ assert_se(is_dir(q, true) > 0);
+
+ assert_se(mkdir_p_safe(tmp, "/tmp/test-mkdir-outside", 0755, UID_INVALID, GID_INVALID, 0) == -ENOTDIR);
+
+ p = mfree(p);
+ assert_se(p = path_join(tmp, "zero-mode/should-fail-to-create-child"));
+ assert_se(mkdir_parents_safe(tmp, p, 0000, UID_INVALID, GID_INVALID, 0) >= 0);
+ r = safe_fork("(test-mkdir-no-cap)", FORK_DEATHSIG_SIGTERM | FORK_WAIT | FORK_LOG, NULL);
+ if (r == 0) {
+ (void) capability_bounding_set_drop(0, /* right_now = */ true);
+ assert_se(mkdir_p_safe(tmp, p, 0000, UID_INVALID, GID_INVALID, 0) == -EACCES);
+ _exit(EXIT_SUCCESS);
+ }
+ assert_se(r >= 0);
+}
+
+TEST(mkdir_p_root) {
+ _cleanup_(rm_rf_physical_and_freep) char *tmp = NULL;
+ _cleanup_free_ char *p = NULL;
+
+ assert_se(mkdtemp_malloc("/tmp/test-mkdir-XXXXXX", &tmp) >= 0);
+
+ assert_se(p = path_join(tmp, "run/aaa/bbb"));
+ assert_se(mkdir_p_root(tmp, "/run/aaa/bbb", UID_INVALID, GID_INVALID, 0755, NULL) >= 0);
+ assert_se(is_dir(p, false) > 0);
+ assert_se(is_dir(p, true) > 0);
+
+ p = mfree(p);
+ assert_se(p = path_join(tmp, "var/run"));
+ assert_se(mkdir_parents_safe(tmp, p, 0755, UID_INVALID, GID_INVALID, 0) >= 0);
+ assert_se(symlink("../run", p) >= 0);
+ assert_se(is_dir(p, false) == 0);
+ assert_se(is_dir(p, true) > 0);
+
+ p = mfree(p);
+ assert_se(p = path_join(tmp, "var/run/hoge/foo/baz"));
+ assert_se(mkdir_p_root(tmp, "/var/run/hoge/foo/baz", UID_INVALID, GID_INVALID, 0755, NULL) >= 0);
+ assert_se(is_dir(p, false) > 0);
+ assert_se(is_dir(p, true) > 0);
+
+ p = mfree(p);
+ assert_se(p = path_join(tmp, "not-exists"));
+ assert_se(mkdir_p_root(p, "/aaa", UID_INVALID, GID_INVALID, 0755, NULL) == -ENOENT);
+
+ p = mfree(p);
+ assert_se(p = path_join(tmp, "regular-file"));
+ assert_se(touch(p) >= 0);
+ assert_se(mkdir_p_root(p, "/aaa", UID_INVALID, GID_INVALID, 0755, NULL) == -ENOTDIR);
+
+ /* FIXME: The tests below do not work.
+ p = mfree(p);
+ assert_se(p = path_join(tmp, "symlink"));
+ assert_se(symlink("aaa", p) >= 0);
+ assert_se(mkdir_p_root(tmp, "/symlink/hoge/foo", UID_INVALID, GID_INVALID, 0755) >= 0);
+ p = mfree(p);
+ assert_se(p = path_join(tmp, "symlink/hoge/foo"));
+ assert_se(is_dir(p, false) > 0);
+ assert_se(is_dir(p, true) > 0);
+ p = mfree(p);
+ assert_se(p = path_join(tmp, "aaa/hoge/foo"));
+ assert_se(is_dir(p, false) > 0);
+ assert_se(is_dir(p, true) > 0);
+ */
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-modhex.c b/src/test/test-modhex.c
new file mode 100644
index 0000000..6725732
--- /dev/null
+++ b/src/test/test-modhex.c
@@ -0,0 +1,51 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "recovery-key.h"
+#include "alloc-util.h"
+#include "string-util.h"
+#include "tests.h"
+
+static void test_normalize_recovery_key(const char *t, const char *expected) {
+ _cleanup_free_ char *z = NULL;
+ int r;
+
+ assert_se(t);
+
+ r = normalize_recovery_key(t, &z);
+ assert_se(expected ?
+ (r >= 0 && streq(z, expected)) :
+ (r == -EINVAL && z == NULL));
+}
+
+TEST(normalize_recovery_key_all) {
+ test_normalize_recovery_key("iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj",
+ "iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj");
+
+ test_normalize_recovery_key("iefgcelhbiduvkjvcjvuncnkvlfchdidjhtuhhdeurkllkegilkjgbrthjkbgktj",
+ "iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj");
+
+ test_normalize_recovery_key("IEFGCELH-BIDUVKJV-CJVUNCNK-VLFCHDID-JHTUHHDE-URKLLKEG-ILKJGBRT-HJKBGKTJ",
+ "iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj");
+
+ test_normalize_recovery_key("IEFGCELHBIDUVKJVCJVUNCNKVLFCHDIDJHTUHHDEURKLLKEGILKJGBRTHJKBGKTJ",
+ "iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj");
+
+ test_normalize_recovery_key("Iefgcelh-Biduvkjv-Cjvuncnk-Vlfchdid-Jhtuhhde-Urkllkeg-Ilkjgbrt-Hjkbgktj",
+ "iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj");
+
+ test_normalize_recovery_key("Iefgcelhbiduvkjvcjvuncnkvlfchdidjhtuhhdeurkllkegilkjgbrthjkbgktj",
+ "iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj");
+
+ test_normalize_recovery_key("iefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgkt", NULL);
+ test_normalize_recovery_key("iefgcelhbiduvkjvcjvuncnkvlfchdidjhtuhhdeurkllkegilkjgbrthjkbgkt", NULL);
+ test_normalize_recovery_key("IEFGCELHBIDUVKJVCJVUNCNKVLFCHDIDJHTUHHDEURKLLKEGILKJGBRTHJKBGKT", NULL);
+
+ test_normalize_recovery_key("xefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj", NULL);
+ test_normalize_recovery_key("Xefgcelh-biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj", NULL);
+ test_normalize_recovery_key("iefgcelh+biduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj", NULL);
+ test_normalize_recovery_key("iefgcelhebiduvkjv-cjvuncnk-vlfchdid-jhtuhhde-urkllkeg-ilkjgbrt-hjkbgktj", NULL);
+
+ test_normalize_recovery_key("", NULL);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-mount-util.c b/src/test/test-mount-util.c
new file mode 100644
index 0000000..c3d0acb
--- /dev/null
+++ b/src/test/test-mount-util.c
@@ -0,0 +1,509 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/mount.h>
+#include <sys/statvfs.h>
+
+#include "alloc-util.h"
+#include "capability-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "libmount-util.h"
+#include "missing_magic.h"
+#include "missing_mount.h"
+#include "mkdir.h"
+#include "mount-util.h"
+#include "mountpoint-util.h"
+#include "namespace-util.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "random-util.h"
+#include "rm-rf.h"
+#include "stat-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+TEST(mount_option_mangle) {
+ char *opts = NULL;
+ unsigned long f;
+
+ assert_se(mount_option_mangle(NULL, MS_RDONLY|MS_NOSUID, &f, &opts) == 0);
+ assert_se(f == (MS_RDONLY|MS_NOSUID));
+ assert_se(opts == NULL);
+
+ assert_se(mount_option_mangle("", MS_RDONLY|MS_NOSUID, &f, &opts) == 0);
+ assert_se(f == (MS_RDONLY|MS_NOSUID));
+ assert_se(opts == NULL);
+
+ assert_se(mount_option_mangle("ro,nosuid,nodev,noexec", 0, &f, &opts) == 0);
+ assert_se(f == (MS_RDONLY|MS_NOSUID|MS_NODEV|MS_NOEXEC));
+ assert_se(opts == NULL);
+
+ assert_se(mount_option_mangle("ro,nosuid,nodev,noexec,mode=0755", 0, &f, &opts) == 0);
+ assert_se(f == (MS_RDONLY|MS_NOSUID|MS_NODEV|MS_NOEXEC));
+ assert_se(streq(opts, "mode=0755"));
+ opts = mfree(opts);
+
+ assert_se(mount_option_mangle("rw,nosuid,foo,hogehoge,nodev,mode=0755", 0, &f, &opts) == 0);
+ assert_se(f == (MS_NOSUID|MS_NODEV));
+ assert_se(streq(opts, "foo,hogehoge,mode=0755"));
+ opts = mfree(opts);
+
+ assert_se(mount_option_mangle("rw,nosuid,nodev,noexec,relatime,net_cls,net_prio", MS_RDONLY, &f, &opts) == 0);
+ assert_se(f == (MS_NOSUID|MS_NODEV|MS_NOEXEC|MS_RELATIME));
+ assert_se(streq(opts, "net_cls,net_prio"));
+ opts = mfree(opts);
+
+ assert_se(mount_option_mangle("rw,nosuid,nodev,relatime,size=1630748k,mode=0700,uid=1000,gid=1000", MS_RDONLY, &f, &opts) == 0);
+ assert_se(f == (MS_NOSUID|MS_NODEV|MS_RELATIME));
+ assert_se(streq(opts, "size=1630748k,mode=0700,uid=1000,gid=1000"));
+ opts = mfree(opts);
+
+ assert_se(mount_option_mangle("size=1630748k,rw,gid=1000,,,nodev,relatime,,mode=0700,nosuid,uid=1000", MS_RDONLY, &f, &opts) == 0);
+ assert_se(f == (MS_NOSUID|MS_NODEV|MS_RELATIME));
+ assert_se(streq(opts, "size=1630748k,gid=1000,mode=0700,uid=1000"));
+ opts = mfree(opts);
+
+ assert_se(mount_option_mangle("rw,exec,size=8143984k,nr_inodes=2035996,mode=0755", MS_RDONLY|MS_NOSUID|MS_NOEXEC|MS_NODEV, &f, &opts) == 0);
+ assert_se(f == (MS_NOSUID|MS_NODEV));
+ assert_se(streq(opts, "size=8143984k,nr_inodes=2035996,mode=0755"));
+ opts = mfree(opts);
+
+ assert_se(mount_option_mangle("rw,relatime,fmask=0022,,,dmask=0022", MS_RDONLY, &f, &opts) == 0);
+ assert_se(f == MS_RELATIME);
+ assert_se(streq(opts, "fmask=0022,dmask=0022"));
+ opts = mfree(opts);
+
+ assert_se(mount_option_mangle("rw,relatime,fmask=0022,dmask=0022,\"hogehoge", MS_RDONLY, &f, &opts) < 0);
+
+ assert_se(mount_option_mangle("mode=01777,size=10%,nr_inodes=400k,uid=496107520,gid=496107520,context=\"system_u:object_r:svirt_sandbox_file_t:s0:c0,c1\"", 0, &f, &opts) == 0);
+ assert_se(f == 0);
+ assert_se(streq(opts, "mode=01777,size=10%,nr_inodes=400k,uid=496107520,gid=496107520,context=\"system_u:object_r:svirt_sandbox_file_t:s0:c0,c1\""));
+ opts = mfree(opts);
+}
+
+static void test_mount_flags_to_string_one(unsigned long flags, const char *expected) {
+ _cleanup_free_ char *x = NULL;
+ int r;
+
+ r = mount_flags_to_string(flags, &x);
+ log_info("flags: %#lX → %d/\"%s\"", flags, r, strnull(x));
+ assert_se(r >= 0);
+ assert_se(streq(x, expected));
+}
+
+TEST(mount_flags_to_string) {
+ test_mount_flags_to_string_one(0, "0");
+ test_mount_flags_to_string_one(MS_RDONLY, "MS_RDONLY");
+ test_mount_flags_to_string_one(MS_NOSUID, "MS_NOSUID");
+ test_mount_flags_to_string_one(MS_NODEV, "MS_NODEV");
+ test_mount_flags_to_string_one(MS_NOEXEC, "MS_NOEXEC");
+ test_mount_flags_to_string_one(MS_SYNCHRONOUS, "MS_SYNCHRONOUS");
+ test_mount_flags_to_string_one(MS_REMOUNT, "MS_REMOUNT");
+ test_mount_flags_to_string_one(MS_MANDLOCK, "MS_MANDLOCK");
+ test_mount_flags_to_string_one(MS_DIRSYNC, "MS_DIRSYNC");
+ test_mount_flags_to_string_one(MS_NOSYMFOLLOW, "MS_NOSYMFOLLOW");
+ test_mount_flags_to_string_one(MS_NOATIME, "MS_NOATIME");
+ test_mount_flags_to_string_one(MS_NODIRATIME, "MS_NODIRATIME");
+ test_mount_flags_to_string_one(MS_BIND, "MS_BIND");
+ test_mount_flags_to_string_one(MS_MOVE, "MS_MOVE");
+ test_mount_flags_to_string_one(MS_REC, "MS_REC");
+ test_mount_flags_to_string_one(MS_SILENT, "MS_SILENT");
+ test_mount_flags_to_string_one(MS_POSIXACL, "MS_POSIXACL");
+ test_mount_flags_to_string_one(MS_UNBINDABLE, "MS_UNBINDABLE");
+ test_mount_flags_to_string_one(MS_PRIVATE, "MS_PRIVATE");
+ test_mount_flags_to_string_one(MS_SLAVE, "MS_SLAVE");
+ test_mount_flags_to_string_one(MS_SHARED, "MS_SHARED");
+ test_mount_flags_to_string_one(MS_RELATIME, "MS_RELATIME");
+ test_mount_flags_to_string_one(MS_KERNMOUNT, "MS_KERNMOUNT");
+ test_mount_flags_to_string_one(MS_I_VERSION, "MS_I_VERSION");
+ test_mount_flags_to_string_one(MS_STRICTATIME, "MS_STRICTATIME");
+ test_mount_flags_to_string_one(MS_LAZYTIME, "MS_LAZYTIME");
+ test_mount_flags_to_string_one(MS_LAZYTIME|MS_STRICTATIME, "MS_STRICTATIME|MS_LAZYTIME");
+ test_mount_flags_to_string_one(UINT_MAX,
+ "MS_RDONLY|MS_NOSUID|MS_NODEV|MS_NOEXEC|MS_SYNCHRONOUS|MS_REMOUNT|"
+ "MS_MANDLOCK|MS_DIRSYNC|MS_NOSYMFOLLOW|MS_NOATIME|MS_NODIRATIME|"
+ "MS_BIND|MS_MOVE|MS_REC|MS_SILENT|MS_POSIXACL|MS_UNBINDABLE|"
+ "MS_PRIVATE|MS_SLAVE|MS_SHARED|MS_RELATIME|MS_KERNMOUNT|"
+ "MS_I_VERSION|MS_STRICTATIME|MS_LAZYTIME|fc000200");
+}
+
+TEST(bind_remount_recursive) {
+ _cleanup_(rm_rf_physical_and_freep) char *tmp = NULL;
+ _cleanup_free_ char *subdir = NULL;
+
+ if (geteuid() != 0 || have_effective_cap(CAP_SYS_ADMIN) <= 0) {
+ (void) log_tests_skipped("not running privileged");
+ return;
+ }
+
+ assert_se(mkdtemp_malloc("/tmp/XXXXXX", &tmp) >= 0);
+ subdir = path_join(tmp, "subdir");
+ assert_se(subdir);
+ assert_se(mkdir(subdir, 0755) >= 0);
+
+ FOREACH_STRING(p, "/usr", "/sys", "/", tmp) {
+ pid_t pid;
+
+ pid = fork();
+ assert_se(pid >= 0);
+
+ if (pid == 0) {
+ struct statvfs svfs;
+ /* child */
+ assert_se(detach_mount_namespace() >= 0);
+
+ /* Check that the subdir is writable (it must be because it's in /tmp) */
+ assert_se(statvfs(subdir, &svfs) >= 0);
+ assert_se(!FLAGS_SET(svfs.f_flag, ST_RDONLY));
+
+ /* Make the subdir a bind mount */
+ assert_se(mount_nofollow(subdir, subdir, NULL, MS_BIND|MS_REC, NULL) >= 0);
+
+ /* Ensure it's still writable */
+ assert_se(statvfs(subdir, &svfs) >= 0);
+ assert_se(!FLAGS_SET(svfs.f_flag, ST_RDONLY));
+
+ /* Now mark the path we currently run for read-only */
+ assert_se(bind_remount_recursive(p, MS_RDONLY, MS_RDONLY, path_equal(p, "/sys") ? STRV_MAKE("/sys/kernel") : NULL) >= 0);
+
+ /* Ensure that this worked on the top-level */
+ assert_se(statvfs(p, &svfs) >= 0);
+ assert_se(FLAGS_SET(svfs.f_flag, ST_RDONLY));
+
+ /* And ensure this had an effect on the subdir exactly if we are talking about a path above the subdir */
+ assert_se(statvfs(subdir, &svfs) >= 0);
+ assert_se(FLAGS_SET(svfs.f_flag, ST_RDONLY) == !!path_startswith(subdir, p));
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(wait_for_terminate_and_check("test-remount-rec", pid, WAIT_LOG) == EXIT_SUCCESS);
+ }
+}
+
+TEST(bind_remount_one) {
+ pid_t pid;
+
+ if (geteuid() != 0 || have_effective_cap(CAP_SYS_ADMIN) <= 0) {
+ (void) log_tests_skipped("not running privileged");
+ return;
+ }
+
+ pid = fork();
+ assert_se(pid >= 0);
+
+ if (pid == 0) {
+ /* child */
+
+ _cleanup_fclose_ FILE *proc_self_mountinfo = NULL;
+
+ assert_se(detach_mount_namespace() >= 0);
+
+ assert_se(fopen_unlocked("/proc/self/mountinfo", "re", &proc_self_mountinfo) >= 0);
+
+ assert_se(bind_remount_one_with_mountinfo("/run", MS_RDONLY, MS_RDONLY, proc_self_mountinfo) >= 0);
+ assert_se(bind_remount_one_with_mountinfo("/run", MS_NOEXEC, MS_RDONLY|MS_NOEXEC, proc_self_mountinfo) >= 0);
+ assert_se(bind_remount_one_with_mountinfo("/proc/idontexist", MS_RDONLY, MS_RDONLY, proc_self_mountinfo) == -ENOENT);
+ assert_se(bind_remount_one_with_mountinfo("/proc/self", MS_RDONLY, MS_RDONLY, proc_self_mountinfo) == -EINVAL);
+ assert_se(bind_remount_one_with_mountinfo("/", MS_RDONLY, MS_RDONLY, proc_self_mountinfo) >= 0);
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(wait_for_terminate_and_check("test-remount-one", pid, WAIT_LOG) == EXIT_SUCCESS);
+}
+
+TEST(make_mount_point_inode) {
+ _cleanup_(rm_rf_physical_and_freep) char *d = NULL;
+ const char *src_file, *src_dir, *dst_file, *dst_dir;
+ struct stat st;
+
+ assert_se(mkdtemp_malloc(NULL, &d) >= 0);
+
+ src_file = strjoina(d, "/src/file");
+ src_dir = strjoina(d, "/src/dir");
+ dst_file = strjoina(d, "/dst/file");
+ dst_dir = strjoina(d, "/dst/dir");
+
+ assert_se(mkdir_p(src_dir, 0755) >= 0);
+ assert_se(mkdir_parents(dst_file, 0755) >= 0);
+ assert_se(touch(src_file) >= 0);
+
+ assert_se(make_mount_point_inode_from_path(src_file, dst_file, 0755) >= 0);
+ assert_se(make_mount_point_inode_from_path(src_dir, dst_dir, 0755) >= 0);
+
+ assert_se(stat(dst_dir, &st) == 0);
+ assert_se(S_ISDIR(st.st_mode));
+ assert_se(stat(dst_file, &st) == 0);
+ assert_se(S_ISREG(st.st_mode));
+ assert_se(!(S_IXUSR & st.st_mode));
+ assert_se(!(S_IXGRP & st.st_mode));
+ assert_se(!(S_IXOTH & st.st_mode));
+
+ assert_se(unlink(dst_file) == 0);
+ assert_se(rmdir(dst_dir) == 0);
+
+ assert_se(stat(src_file, &st) == 0);
+ assert_se(make_mount_point_inode_from_stat(&st, dst_file, 0755) >= 0);
+ assert_se(stat(src_dir, &st) == 0);
+ assert_se(make_mount_point_inode_from_stat(&st, dst_dir, 0755) >= 0);
+
+ assert_se(stat(dst_dir, &st) == 0);
+ assert_se(S_ISDIR(st.st_mode));
+ assert_se(stat(dst_file, &st) == 0);
+ assert_se(S_ISREG(st.st_mode));
+ assert_se(!(S_IXUSR & st.st_mode));
+ assert_se(!(S_IXGRP & st.st_mode));
+ assert_se(!(S_IXOTH & st.st_mode));
+}
+
+TEST(make_mount_switch_root) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ _cleanup_free_ char *s = NULL;
+ int r;
+
+ if (geteuid() != 0 || have_effective_cap(CAP_SYS_ADMIN) <= 0) {
+ (void) log_tests_skipped("not running privileged");
+ return;
+ }
+
+ assert_se(mkdtemp_malloc(NULL, &t) >= 0);
+
+ assert_se(asprintf(&s, "%s/somerandomname%" PRIu64, t, random_u64()) >= 0);
+ assert_se(s);
+ assert_se(touch(s) >= 0);
+
+ for (int force_ms_move = 0; force_ms_move < 2; force_ms_move++) {
+ r = safe_fork("(switch-root)",
+ FORK_RESET_SIGNALS |
+ FORK_CLOSE_ALL_FDS |
+ FORK_DEATHSIG_SIGTERM |
+ FORK_WAIT |
+ FORK_REOPEN_LOG |
+ FORK_LOG |
+ FORK_NEW_MOUNTNS |
+ FORK_MOUNTNS_SLAVE,
+ NULL);
+ assert_se(r >= 0);
+
+ if (r == 0) {
+ assert_se(make_mount_point(t) >= 0);
+ assert_se(mount_switch_root_full(t, /* mount_propagation_flag= */ 0, force_ms_move) >= 0);
+
+ assert_se(access(ASSERT_PTR(strrchr(s, '/')), F_OK) >= 0); /* absolute */
+ assert_se(access(ASSERT_PTR(strrchr(s, '/')) + 1, F_OK) >= 0); /* relative */
+ assert_se(access(s, F_OK) < 0 && errno == ENOENT); /* doesn't exist in our new environment */
+
+ _exit(EXIT_SUCCESS);
+ }
+ }
+}
+
+TEST(umount_recursive) {
+ static const struct {
+ const char *prefix;
+ const char * const keep[3];
+ } test_table[] = {
+ {
+ .prefix = NULL,
+ .keep = {},
+ },
+ {
+ .prefix = "/run",
+ .keep = {},
+ },
+ {
+ .prefix = NULL,
+ .keep = { "/dev/shm", NULL },
+ },
+ {
+ .prefix = "/dev",
+ .keep = { "/dev/pts", "/dev/shm", NULL },
+ },
+ };
+
+ int r;
+
+ FOREACH_ARRAY(t, test_table, ELEMENTSOF(test_table)) {
+
+ r = safe_fork("(umount-rec)",
+ FORK_RESET_SIGNALS |
+ FORK_CLOSE_ALL_FDS |
+ FORK_DEATHSIG_SIGTERM |
+ FORK_WAIT |
+ FORK_REOPEN_LOG |
+ FORK_LOG |
+ FORK_NEW_MOUNTNS |
+ FORK_MOUNTNS_SLAVE,
+ NULL);
+
+ if (ERRNO_IS_NEG_PRIVILEGE(r))
+ return (void) log_notice("Skipping umount_recursive() test, lacking privileges");
+
+ assert_se(r >= 0);
+ if (r == 0) { /* child */
+ _cleanup_(mnt_free_tablep) struct libmnt_table *table = NULL;
+ _cleanup_(mnt_free_iterp) struct libmnt_iter *iter = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_free_ char *k = NULL;
+
+ /* Open /p/s/m file before we unmount everything (which might include /proc/) */
+ f = fopen("/proc/self/mountinfo", "re");
+ if (!f) {
+ log_error_errno(errno, "Failed to open /proc/self/mountinfo: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ assert_se(k = strv_join((char**) t->keep, " "));
+ log_info("detaching just %s (keep: %s)", strna(t->prefix), strna(empty_to_null(k)));
+
+ assert_se(umount_recursive_full(t->prefix, MNT_DETACH, (char**) t->keep) >= 0);
+
+ r = libmount_parse("/proc/self/mountinfo", f, &table, &iter);
+ if (r < 0) {
+ log_error_errno(r, "Failed to parse /proc/self/mountinfo: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ for (;;) {
+ struct libmnt_fs *fs;
+
+ r = mnt_table_next_fs(table, iter, &fs);
+ if (r == 1)
+ break;
+ if (r < 0) {
+ log_error_errno(r, "Failed to get next entry from /proc/self/mountinfo: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ log_debug("left after complete umount: %s", mnt_fs_get_target(fs));
+ }
+
+ _exit(EXIT_SUCCESS);
+ }
+ }
+}
+
+TEST(fd_make_mount_point) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ _cleanup_free_ char *s = NULL;
+ int r;
+
+ if (geteuid() != 0 || have_effective_cap(CAP_SYS_ADMIN) <= 0) {
+ (void) log_tests_skipped("not running privileged");
+ return;
+ }
+
+ assert_se(mkdtemp_malloc(NULL, &t) >= 0);
+
+ assert_se(asprintf(&s, "%s/somerandomname%" PRIu64, t, random_u64()) >= 0);
+ assert_se(s);
+ assert_se(mkdir(s, 0700) >= 0);
+
+ r = safe_fork("(make_mount-point)",
+ FORK_RESET_SIGNALS |
+ FORK_CLOSE_ALL_FDS |
+ FORK_DEATHSIG_SIGTERM |
+ FORK_WAIT |
+ FORK_REOPEN_LOG |
+ FORK_LOG |
+ FORK_NEW_MOUNTNS |
+ FORK_MOUNTNS_SLAVE,
+ NULL);
+ assert_se(r >= 0);
+
+ if (r == 0) {
+ _cleanup_close_ int fd = -EBADF, fd2 = -EBADF;
+
+ fd = open(s, O_PATH|O_CLOEXEC);
+ assert_se(fd >= 0);
+
+ assert_se(fd_is_mount_point(fd, NULL, AT_SYMLINK_FOLLOW) == 0);
+
+ assert_se(fd_make_mount_point(fd) > 0);
+
+ /* Reopen the inode so that we end up on the new mount */
+ fd2 = open(s, O_PATH|O_CLOEXEC);
+
+ assert_se(fd_is_mount_point(fd2, NULL, AT_SYMLINK_FOLLOW) > 0);
+
+ assert_se(fd_make_mount_point(fd2) == 0);
+
+ _exit(EXIT_SUCCESS);
+ }
+}
+
+TEST(bind_mount_submounts) {
+ _cleanup_(rmdir_and_freep) char *a = NULL, *b = NULL;
+ _cleanup_free_ char *x = NULL;
+ int r;
+
+ assert_se(mkdtemp_malloc(NULL, &a) >= 0);
+ r = mount_nofollow_verbose(LOG_INFO, "tmpfs", a, "tmpfs", 0, NULL);
+ if (ERRNO_IS_NEG_PRIVILEGE(r))
+ return (void) log_tests_skipped("Skipping bind_mount_submounts() test, lacking privileges");
+
+ assert_se(r >= 0);
+
+ assert_se(x = path_join(a, "foo"));
+ assert_se(touch(x) >= 0);
+ free(x);
+
+ assert_se(x = path_join(a, "x"));
+ assert_se(mkdir(x, 0755) >= 0);
+ assert_se(mount_nofollow_verbose(LOG_INFO, "tmpfs", x, "tmpfs", 0, NULL) >= 0);
+ free(x);
+
+ assert_se(x = path_join(a, "x/xx"));
+ assert_se(touch(x) >= 0);
+ free(x);
+
+ assert_se(x = path_join(a, "y"));
+ assert_se(mkdir(x, 0755) >= 0);
+ assert_se(mount_nofollow_verbose(LOG_INFO, "tmpfs", x, "tmpfs", 0, NULL) >= 0);
+ free(x);
+
+ assert_se(x = path_join(a, "y/yy"));
+ assert_se(touch(x) >= 0);
+ free(x);
+
+ assert_se(mkdtemp_malloc(NULL, &b) >= 0);
+ assert_se(mount_nofollow_verbose(LOG_INFO, "tmpfs", b, "tmpfs", 0, NULL) >= 0);
+
+ assert_se(x = path_join(b, "x"));
+ assert_se(mkdir(x, 0755) >= 0);
+ free(x);
+
+ assert_se(x = path_join(b, "y"));
+ assert_se(mkdir(x, 0755) >= 0);
+ free(x);
+
+ assert_se(bind_mount_submounts(a, b) >= 0);
+
+ assert_se(x = path_join(b, "foo"));
+ assert_se(access(x, F_OK) < 0 && errno == ENOENT);
+ free(x);
+
+ assert_se(x = path_join(b, "x/xx"));
+ assert_se(access(x, F_OK) >= 0);
+ free(x);
+
+ assert_se(x = path_join(b, "y/yy"));
+ assert_se(access(x, F_OK) >= 0);
+ free(x);
+
+ assert_se(x = path_join(b, "x"));
+ assert_se(path_is_mount_point(x, NULL, 0) > 0);
+ free(x);
+
+ assert_se(x = path_join(b, "y"));
+ assert_se(path_is_mount_point(x, NULL, 0) > 0);
+
+ assert_se(umount_recursive(a, 0) >= 0);
+ assert_se(umount_recursive(b, 0) >= 0);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-mountpoint-util.c b/src/test/test-mountpoint-util.c
new file mode 100644
index 0000000..ff447c6
--- /dev/null
+++ b/src/test/test-mountpoint-util.c
@@ -0,0 +1,434 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sched.h>
+#include <sys/mount.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "constants.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "hashmap.h"
+#include "log.h"
+#include "mountpoint-util.h"
+#include "path-util.h"
+#include "rm-rf.h"
+#include "string-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+static void test_mount_propagation_flag_one(const char *name, int ret, unsigned long expected) {
+ unsigned long flags;
+
+ log_info("/* %s(%s) */", __func__, strnull(name));
+
+ assert_se(mount_propagation_flag_from_string(name, &flags) == ret);
+
+ if (ret >= 0) {
+ const char *c;
+
+ assert_se(flags == expected);
+
+ c = mount_propagation_flag_to_string(flags);
+ if (isempty(name))
+ assert_se(isempty(c));
+ else
+ assert_se(streq(c, name));
+ }
+}
+
+TEST(mount_propagation_flag) {
+ test_mount_propagation_flag_one("shared", 0, MS_SHARED);
+ test_mount_propagation_flag_one("slave", 0, MS_SLAVE);
+ test_mount_propagation_flag_one("private", 0, MS_PRIVATE);
+ test_mount_propagation_flag_one(NULL, 0, 0);
+ test_mount_propagation_flag_one("", 0, 0);
+ test_mount_propagation_flag_one("xxxx", -EINVAL, 0);
+ test_mount_propagation_flag_one(" ", -EINVAL, 0);
+}
+
+TEST(mnt_id) {
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_hashmap_free_free_ Hashmap *h = NULL;
+ char *p;
+ void *k;
+ int r;
+
+ assert_se(f = fopen("/proc/self/mountinfo", "re"));
+ assert_se(h = hashmap_new(&trivial_hash_ops));
+
+ for (;;) {
+ _cleanup_free_ char *line = NULL, *path = NULL;
+ int mnt_id;
+
+ r = read_line(f, LONG_LINE_MAX, &line);
+ if (r == 0)
+ break;
+ assert_se(r > 0);
+
+ assert_se(sscanf(line, "%i %*s %*s %*s %ms", &mnt_id, &path) == 2);
+#if HAS_FEATURE_MEMORY_SANITIZER
+ /* We don't know the length of the string, so we need to unpoison it one char at a time */
+ for (const char *c = path; ;c++) {
+ msan_unpoison(c, 1);
+ if (!*c)
+ break;
+ }
+#endif
+ log_debug("mountinfo: %s → %i", path, mnt_id);
+
+ assert_se(hashmap_put(h, INT_TO_PTR(mnt_id), path) >= 0);
+ path = NULL;
+ }
+
+ HASHMAP_FOREACH_KEY(p, k, h) {
+ int mnt_id = PTR_TO_INT(k), mnt_id2;
+ const char *q;
+
+ r = path_get_mnt_id(p, &mnt_id2);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to get the mnt id of %s: %m", p);
+ continue;
+ }
+
+ if (mnt_id == mnt_id2) {
+ log_debug("mnt ids of %s is %i.", p, mnt_id);
+ continue;
+ } else
+ log_debug("mnt ids of %s are %i (from /proc/self/mountinfo), %i (from path_get_mnt_id()).", p, mnt_id, mnt_id2);
+
+ /* The ids don't match? This can easily happen e.g. running with "unshare --mount-proc".
+ * See #11505. */
+ assert_se(q = hashmap_get(h, INT_TO_PTR(mnt_id2)));
+
+ assert_se((r = path_is_mount_point(p, NULL, 0)) >= 0);
+ if (r == 0) {
+ /* If the path is not a mount point anymore, then it must be a sub directory of
+ * the path corresponds to mnt_id2. */
+ log_debug("The path %s for mnt id %i is not a mount point.", p, mnt_id2);
+ assert_se(!isempty(path_startswith(p, q)));
+ } else {
+ /* If the path is still a mount point, then it must be equivalent to the path
+ * corresponds to mnt_id2 */
+ log_debug("There are multiple mounts on the same path %s.", p);
+ assert_se(path_equal(p, q));
+ }
+ }
+}
+
+TEST(path_is_mount_point) {
+ int fd;
+ char tmp_dir[] = "/tmp/test-path-is-mount-point-XXXXXX";
+ _cleanup_free_ char *file1 = NULL, *file2 = NULL, *link1 = NULL, *link2 = NULL;
+ _cleanup_free_ char *dir1 = NULL, *dir1file = NULL, *dirlink1 = NULL, *dirlink1file = NULL;
+ _cleanup_free_ char *dir2 = NULL, *dir2file = NULL;
+
+ assert_se(path_is_mount_point("/", NULL, AT_SYMLINK_FOLLOW) > 0);
+ assert_se(path_is_mount_point("/", NULL, 0) > 0);
+ assert_se(path_is_mount_point("//", NULL, AT_SYMLINK_FOLLOW) > 0);
+ assert_se(path_is_mount_point("//", NULL, 0) > 0);
+
+ assert_se(path_is_mount_point("/proc", NULL, AT_SYMLINK_FOLLOW) > 0);
+ assert_se(path_is_mount_point("/proc", NULL, 0) > 0);
+ assert_se(path_is_mount_point("/proc/", NULL, AT_SYMLINK_FOLLOW) > 0);
+ assert_se(path_is_mount_point("/proc/", NULL, 0) > 0);
+
+ assert_se(path_is_mount_point("/proc/1", NULL, AT_SYMLINK_FOLLOW) == 0);
+ assert_se(path_is_mount_point("/proc/1", NULL, 0) == 0);
+ assert_se(path_is_mount_point("/proc/1/", NULL, AT_SYMLINK_FOLLOW) == 0);
+ assert_se(path_is_mount_point("/proc/1/", NULL, 0) == 0);
+
+ assert_se(path_is_mount_point("/sys", NULL, AT_SYMLINK_FOLLOW) > 0);
+ assert_se(path_is_mount_point("/sys", NULL, 0) > 0);
+ assert_se(path_is_mount_point("/sys/", NULL, AT_SYMLINK_FOLLOW) > 0);
+ assert_se(path_is_mount_point("/sys/", NULL, 0) > 0);
+
+ /* we'll create a hierarchy of different kinds of dir/file/link
+ * layouts:
+ *
+ * <tmp>/file1, <tmp>/file2
+ * <tmp>/link1 -> file1, <tmp>/link2 -> file2
+ * <tmp>/dir1/
+ * <tmp>/dir1/file
+ * <tmp>/dirlink1 -> dir1
+ * <tmp>/dirlink1file -> dirlink1/file
+ * <tmp>/dir2/
+ * <tmp>/dir2/file
+ */
+
+ /* file mountpoints */
+ assert_se(mkdtemp(tmp_dir) != NULL);
+ file1 = path_join(tmp_dir, "file1");
+ assert_se(file1);
+ file2 = path_join(tmp_dir, "file2");
+ assert_se(file2);
+ fd = open(file1, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC, 0664);
+ assert_se(fd > 0);
+ close(fd);
+ fd = open(file2, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC, 0664);
+ assert_se(fd > 0);
+ close(fd);
+ link1 = path_join(tmp_dir, "link1");
+ assert_se(link1);
+ assert_se(symlink("file1", link1) == 0);
+ link2 = path_join(tmp_dir, "link2");
+ assert_se(link1);
+ assert_se(symlink("file2", link2) == 0);
+
+ assert_se(path_is_mount_point(file1, NULL, AT_SYMLINK_FOLLOW) == 0);
+ assert_se(path_is_mount_point(file1, NULL, 0) == 0);
+ assert_se(path_is_mount_point(link1, NULL, AT_SYMLINK_FOLLOW) == 0);
+ assert_se(path_is_mount_point(link1, NULL, 0) == 0);
+
+ /* directory mountpoints */
+ dir1 = path_join(tmp_dir, "dir1");
+ assert_se(dir1);
+ assert_se(mkdir(dir1, 0755) == 0);
+ dirlink1 = path_join(tmp_dir, "dirlink1");
+ assert_se(dirlink1);
+ assert_se(symlink("dir1", dirlink1) == 0);
+ dirlink1file = path_join(tmp_dir, "dirlink1file");
+ assert_se(dirlink1file);
+ assert_se(symlink("dirlink1/file", dirlink1file) == 0);
+ dir2 = path_join(tmp_dir, "dir2");
+ assert_se(dir2);
+ assert_se(mkdir(dir2, 0755) == 0);
+
+ assert_se(path_is_mount_point(dir1, NULL, AT_SYMLINK_FOLLOW) == 0);
+ assert_se(path_is_mount_point(dir1, NULL, 0) == 0);
+ assert_se(path_is_mount_point(dirlink1, NULL, AT_SYMLINK_FOLLOW) == 0);
+ assert_se(path_is_mount_point(dirlink1, NULL, 0) == 0);
+
+ /* file in subdirectory mountpoints */
+ dir1file = path_join(dir1, "file");
+ assert_se(dir1file);
+ fd = open(dir1file, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC, 0664);
+ assert_se(fd > 0);
+ close(fd);
+
+ assert_se(path_is_mount_point(dir1file, NULL, AT_SYMLINK_FOLLOW) == 0);
+ assert_se(path_is_mount_point(dir1file, NULL, 0) == 0);
+ assert_se(path_is_mount_point(dirlink1file, NULL, AT_SYMLINK_FOLLOW) == 0);
+ assert_se(path_is_mount_point(dirlink1file, NULL, 0) == 0);
+
+ /* these tests will only work as root */
+ if (mount(file1, file2, NULL, MS_BIND, NULL) >= 0) {
+ int rf, rt, rdf, rdt, rlf, rlt, rl1f, rl1t;
+ const char *file2d;
+
+ /* files */
+ /* capture results in vars, to avoid dangling mounts on failure */
+ log_info("%s: %s", __func__, file2);
+ rf = path_is_mount_point(file2, NULL, 0);
+ rt = path_is_mount_point(file2, NULL, AT_SYMLINK_FOLLOW);
+
+ file2d = strjoina(file2, "/");
+ log_info("%s: %s", __func__, file2d);
+ rdf = path_is_mount_point(file2d, NULL, 0);
+ rdt = path_is_mount_point(file2d, NULL, AT_SYMLINK_FOLLOW);
+
+ log_info("%s: %s", __func__, link2);
+ rlf = path_is_mount_point(link2, NULL, 0);
+ rlt = path_is_mount_point(link2, NULL, AT_SYMLINK_FOLLOW);
+
+ assert_se(umount(file2) == 0);
+
+ assert_se(rf == 1);
+ assert_se(rt == 1);
+ assert_se(rdf == -ENOTDIR);
+ assert_se(rdt == -ENOTDIR);
+ assert_se(rlf == 0);
+ assert_se(rlt == 1);
+
+ /* dirs */
+ dir2file = path_join(dir2, "file");
+ assert_se(dir2file);
+ fd = open(dir2file, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC, 0664);
+ assert_se(fd > 0);
+ close(fd);
+
+ assert_se(mount(dir2, dir1, NULL, MS_BIND, NULL) >= 0);
+
+ log_info("%s: %s", __func__, dir1);
+ rf = path_is_mount_point(dir1, NULL, 0);
+ rt = path_is_mount_point(dir1, NULL, AT_SYMLINK_FOLLOW);
+ log_info("%s: %s", __func__, dirlink1);
+ rlf = path_is_mount_point(dirlink1, NULL, 0);
+ rlt = path_is_mount_point(dirlink1, NULL, AT_SYMLINK_FOLLOW);
+ log_info("%s: %s", __func__, dirlink1file);
+ /* its parent is a mount point, but not /file itself */
+ rl1f = path_is_mount_point(dirlink1file, NULL, 0);
+ rl1t = path_is_mount_point(dirlink1file, NULL, AT_SYMLINK_FOLLOW);
+
+ assert_se(umount(dir1) == 0);
+
+ assert_se(rf == 1);
+ assert_se(rt == 1);
+ assert_se(rlf == 0);
+ assert_se(rlt == 1);
+ assert_se(rl1f == 0);
+ assert_se(rl1t == 0);
+
+ } else
+ log_info("Skipping bind mount file test");
+
+ assert_se(rm_rf(tmp_dir, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
+}
+
+TEST(fd_is_mount_point) {
+ _cleanup_(rm_rf_physical_and_freep) char *tmpdir = NULL;
+ _cleanup_close_ int fd = -EBADF;
+ int r;
+
+ fd = open("/", O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOCTTY);
+ assert_se(fd >= 0);
+
+ /* Not allowed, since "/" is a path, not a plain filename */
+ assert_se(fd_is_mount_point(fd, "/", 0) == -EINVAL);
+ assert_se(fd_is_mount_point(fd, ".", 0) == -EINVAL);
+ assert_se(fd_is_mount_point(fd, "./", 0) == -EINVAL);
+ assert_se(fd_is_mount_point(fd, "..", 0) == -EINVAL);
+ assert_se(fd_is_mount_point(fd, "../", 0) == -EINVAL);
+ assert_se(fd_is_mount_point(fd, "", 0) == -EINVAL);
+ assert_se(fd_is_mount_point(fd, "/proc", 0) == -EINVAL);
+ assert_se(fd_is_mount_point(fd, "/proc/", 0) == -EINVAL);
+ assert_se(fd_is_mount_point(fd, "proc/sys", 0) == -EINVAL);
+ assert_se(fd_is_mount_point(fd, "proc/sys/", 0) == -EINVAL);
+
+ /* This one definitely is a mount point */
+ assert_se(fd_is_mount_point(fd, "proc", 0) > 0);
+ assert_se(fd_is_mount_point(fd, "proc/", 0) > 0);
+
+ safe_close(fd);
+ fd = open("/tmp", O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOCTTY);
+ assert_se(fd >= 0);
+
+ assert_se(mkdtemp_malloc("/tmp/not-mounted-XXXXXX", &tmpdir) >= 0);
+ assert_se(fd_is_mount_point(fd, basename(tmpdir), 0) == 0);
+ assert_se(fd_is_mount_point(fd, strjoina(basename(tmpdir), "/"), 0) == 0);
+
+ safe_close(fd);
+ fd = open("/proc", O_RDONLY|O_CLOEXEC|O_DIRECTORY|O_NOCTTY);
+ assert_se(fd >= 0);
+
+ assert_se(fd_is_mount_point(fd, NULL, 0) > 0);
+ assert_se(fd_is_mount_point(fd, "", 0) == -EINVAL);
+ assert_se(fd_is_mount_point(fd, "version", 0) == 0);
+
+ safe_close(fd);
+ fd = open("/proc/version", O_RDONLY|O_CLOEXEC|O_NOCTTY);
+ assert_se(fd >= 0);
+
+ r = fd_is_mount_point(fd, NULL, 0);
+ assert_se(IN_SET(r, 0, -ENOTDIR)); /* on old kernels we can't determine if regular files are mount points if we have no directory fd */
+ assert_se(fd_is_mount_point(fd, "", 0) == -EINVAL);
+}
+
+TEST(ms_nosymfollow_supported) {
+ log_info("MS_NOSYMFOLLOW supported: %s", yes_no(ms_nosymfollow_supported()));
+}
+
+TEST(mount_option_supported) {
+ int r;
+
+ r = mount_option_supported("tmpfs", "size", "64M");
+ log_info("tmpfs supports size=64M: %s (%i)", r < 0 ? "don't know" : yes_no(r), r);
+ assert_se(r > 0 || r == -EAGAIN || (r < 0 && ERRNO_IS_PRIVILEGE(r)));
+
+ r = mount_option_supported("ext4", "discard", NULL);
+ log_info("ext4 supports discard: %s (%i)", r < 0 ? "don't know" : yes_no(r), r);
+ assert_se(r > 0 || r == -EAGAIN || (r < 0 && ERRNO_IS_PRIVILEGE(r)));
+
+ r = mount_option_supported("tmpfs", "idontexist", "64M");
+ log_info("tmpfs supports idontexist: %s (%i)", r < 0 ? "don't know" : yes_no(r), r);
+ assert_se(r == 0 || r == -EAGAIN || (r < 0 && ERRNO_IS_PRIVILEGE(r)));
+
+ r = mount_option_supported("tmpfs", "ialsodontexist", NULL);
+ log_info("tmpfs supports ialsodontexist: %s (%i)", r < 0 ? "don't know" : yes_no(r), r);
+ assert_se(r == 0 || r == -EAGAIN || (r < 0 && ERRNO_IS_PRIVILEGE(r)));
+
+ r = mount_option_supported("proc", "hidepid", "1");
+ log_info("proc supports hidepid=1: %s (%i)", r < 0 ? "don't know" : yes_no(r), r);
+ assert_se(r >= 0 || r == -EAGAIN || (r < 0 && ERRNO_IS_PRIVILEGE(r)));
+}
+
+TEST(fstype_can_discard) {
+ assert_se(fstype_can_discard("ext4"));
+ assert_se(!fstype_can_discard("squashfs"));
+ assert_se(!fstype_can_discard("iso9660"));
+}
+
+TEST(fstype_can_norecovery) {
+ assert_se(fstype_can_norecovery("ext4"));
+ assert_se(!fstype_can_norecovery("vfat"));
+ assert_se(!fstype_can_norecovery("tmpfs"));
+}
+
+TEST(fstype_can_umask) {
+ assert_se(fstype_can_umask("vfat"));
+ assert_se(!fstype_can_umask("tmpfs"));
+}
+
+TEST(path_get_mnt_id_at_null) {
+ _cleanup_close_ int root_fd = -EBADF, run_fd = -EBADF;
+ int id1, id2;
+
+ assert_se(path_get_mnt_id_at(AT_FDCWD, "/run/", &id1) >= 0);
+ assert_se(id1 > 0);
+
+ assert_se(path_get_mnt_id_at(AT_FDCWD, "/run", &id2) >= 0);
+ assert_se(id1 == id2);
+ id2 = -1;
+
+ root_fd = open("/", O_DIRECTORY|O_CLOEXEC);
+ assert_se(root_fd >= 0);
+
+ assert_se(path_get_mnt_id_at(root_fd, "/run/", &id2) >= 0);
+ assert_se(id1 = id2);
+ id2 = -1;
+
+ assert_se(path_get_mnt_id_at(root_fd, "/run", &id2) >= 0);
+ assert_se(id1 = id2);
+ id2 = -1;
+
+ assert_se(path_get_mnt_id_at(root_fd, "run", &id2) >= 0);
+ assert_se(id1 = id2);
+ id2 = -1;
+
+ assert_se(path_get_mnt_id_at(root_fd, "run/", &id2) >= 0);
+ assert_se(id1 = id2);
+ id2 = -1;
+
+ run_fd = openat(root_fd, "run", O_DIRECTORY|O_CLOEXEC);
+ assert_se(run_fd >= 0);
+
+ id2 = -1;
+ assert_se(path_get_mnt_id_at(run_fd, "", &id2) >= 0);
+ assert_se(id1 = id2);
+ id2 = -1;
+
+ assert_se(path_get_mnt_id_at(run_fd, NULL, &id2) >= 0);
+ assert_se(id1 = id2);
+ id2 = -1;
+
+ assert_se(path_get_mnt_id_at(run_fd, ".", &id2) >= 0);
+ assert_se(id1 = id2);
+ id2 = -1;
+}
+
+static int intro(void) {
+ /* let's move into our own mount namespace with all propagation from the host turned off, so
+ * that /proc/self/mountinfo is static and constant for the whole time our test runs. */
+
+ if (unshare(CLONE_NEWNS) < 0) {
+ if (!ERRNO_IS_PRIVILEGE(errno))
+ return log_error_errno(errno, "Failed to detach mount namespace: %m");
+
+ log_notice("Lacking privilege to create separate mount namespace, proceeding in originating mount namespace.");
+ } else
+ assert_se(mount(NULL, "/", NULL, MS_PRIVATE | MS_REC, NULL) >= 0);
+
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_DEBUG, intro);
diff --git a/src/test/test-namespace.c b/src/test/test-namespace.c
new file mode 100644
index 0000000..65d0825
--- /dev/null
+++ b/src/test/test-namespace.c
@@ -0,0 +1,199 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+
+#include "alloc-util.h"
+#include "fd-util.h"
+#include "namespace.h"
+#include "process-util.h"
+#include "string-util.h"
+#include "tests.h"
+#include "user-util.h"
+#include "virt.h"
+
+TEST(namespace_cleanup_tmpdir) {
+ {
+ _cleanup_(namespace_cleanup_tmpdirp) char *dir;
+ assert_se(dir = strdup(RUN_SYSTEMD_EMPTY));
+ }
+
+ {
+ _cleanup_(namespace_cleanup_tmpdirp) char *dir;
+ assert_se(dir = strdup("/tmp/systemd-test-namespace.XXXXXX"));
+ assert_se(mkdtemp(dir));
+ }
+}
+
+static void test_tmpdir_one(const char *id, const char *A, const char *B) {
+ _cleanup_free_ char *a, *b;
+ struct stat x, y;
+ char *c, *d;
+
+ assert_se(setup_tmp_dirs(id, &a, &b) == 0);
+
+ assert_se(stat(a, &x) >= 0);
+ assert_se(stat(b, &y) >= 0);
+
+ assert_se(S_ISDIR(x.st_mode));
+ assert_se(S_ISDIR(y.st_mode));
+
+ if (!streq(a, RUN_SYSTEMD_EMPTY)) {
+ assert_se(startswith(a, A));
+ assert_se((x.st_mode & 01777) == 0700);
+ c = strjoina(a, "/tmp");
+ assert_se(stat(c, &x) >= 0);
+ assert_se(S_ISDIR(x.st_mode));
+ assert_se(FLAGS_SET(x.st_mode, 01777));
+ assert_se(rmdir(c) >= 0);
+ assert_se(rmdir(a) >= 0);
+ }
+
+ if (!streq(b, RUN_SYSTEMD_EMPTY)) {
+ assert_se(startswith(b, B));
+ assert_se((y.st_mode & 01777) == 0700);
+ d = strjoina(b, "/tmp");
+ assert_se(stat(d, &y) >= 0);
+ assert_se(S_ISDIR(y.st_mode));
+ assert_se(FLAGS_SET(y.st_mode, 01777));
+ assert_se(rmdir(d) >= 0);
+ assert_se(rmdir(b) >= 0);
+ }
+}
+
+TEST(tmpdir) {
+ _cleanup_free_ char *x = NULL, *y = NULL, *z = NULL, *zz = NULL;
+ sd_id128_t bid;
+
+ assert_se(sd_id128_get_boot(&bid) >= 0);
+
+ x = strjoin("/tmp/systemd-private-", SD_ID128_TO_STRING(bid), "-abcd.service-");
+ y = strjoin("/var/tmp/systemd-private-", SD_ID128_TO_STRING(bid), "-abcd.service-");
+ assert_se(x && y);
+
+ test_tmpdir_one("abcd.service", x, y);
+
+ z = strjoin("/tmp/systemd-private-", SD_ID128_TO_STRING(bid), "-sys-devices-pci0000:00-0000:00:1a.0-usb3-3\\x2d1-3\\x2d1:1.0-bluetooth-hci0.device-");
+ zz = strjoin("/var/tmp/systemd-private-", SD_ID128_TO_STRING(bid), "-sys-devices-pci0000:00-0000:00:1a.0-usb3-3\\x2d1-3\\x2d1:1.0-bluetooth-hci0.device-");
+
+ assert_se(z && zz);
+
+ test_tmpdir_one("sys-devices-pci0000:00-0000:00:1a.0-usb3-3\\x2d1-3\\x2d1:1.0-bluetooth-hci0.device", z, zz);
+}
+
+static void test_shareable_ns(unsigned long nsflag) {
+ _cleanup_close_pair_ int s[2] = EBADF_PAIR;
+ pid_t pid1, pid2, pid3;
+ int r, n = 0;
+ siginfo_t si;
+
+ if (geteuid() > 0) {
+ (void) log_tests_skipped("not root");
+ return;
+ }
+
+ assert_se(socketpair(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0, s) >= 0);
+
+ pid1 = fork();
+ assert_se(pid1 >= 0);
+
+ if (pid1 == 0) {
+ r = setup_shareable_ns(s, nsflag);
+ assert_se(r >= 0);
+ _exit(r);
+ }
+
+ pid2 = fork();
+ assert_se(pid2 >= 0);
+
+ if (pid2 == 0) {
+ r = setup_shareable_ns(s, nsflag);
+ assert_se(r >= 0);
+ exit(r);
+ }
+
+ pid3 = fork();
+ assert_se(pid3 >= 0);
+
+ if (pid3 == 0) {
+ r = setup_shareable_ns(s, nsflag);
+ assert_se(r >= 0);
+ exit(r);
+ }
+
+ r = wait_for_terminate(pid1, &si);
+ assert_se(r >= 0);
+ assert_se(si.si_code == CLD_EXITED);
+ n += si.si_status;
+
+ r = wait_for_terminate(pid2, &si);
+ assert_se(r >= 0);
+ assert_se(si.si_code == CLD_EXITED);
+ n += si.si_status;
+
+ r = wait_for_terminate(pid3, &si);
+ assert_se(r >= 0);
+ assert_se(si.si_code == CLD_EXITED);
+ n += si.si_status;
+
+ assert_se(n == 1);
+}
+
+TEST(netns) {
+ test_shareable_ns(CLONE_NEWNET);
+}
+
+TEST(ipcns) {
+ test_shareable_ns(CLONE_NEWIPC);
+}
+
+TEST(protect_kernel_logs) {
+ static const NamespaceParameters p = {
+ .runtime_scope = RUNTIME_SCOPE_SYSTEM,
+ .protect_kernel_logs = true,
+ };
+ pid_t pid;
+ int r;
+
+ if (geteuid() > 0) {
+ (void) log_tests_skipped("not root");
+ return;
+ }
+
+ /* In a container we likely don't have access to /dev/kmsg */
+ if (detect_container() > 0) {
+ (void) log_tests_skipped("in container");
+ return;
+ }
+
+ pid = fork();
+ assert_se(pid >= 0);
+
+ if (pid == 0) {
+ _cleanup_close_ int fd = -EBADF;
+
+ fd = open("/dev/kmsg", O_RDONLY | O_CLOEXEC);
+ assert_se(fd > 0);
+
+ r = setup_namespace(&p, NULL);
+ assert_se(r == 0);
+
+ assert_se(setresuid(UID_NOBODY, UID_NOBODY, UID_NOBODY) >= 0);
+ assert_se(open("/dev/kmsg", O_RDONLY | O_CLOEXEC) < 0);
+ assert_se(errno == EACCES);
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(wait_for_terminate_and_check("ns-kernellogs", pid, WAIT_LOG) == EXIT_SUCCESS);
+}
+
+static int intro(void) {
+ if (!have_namespaces())
+ return log_tests_skipped("Don't have namespace support");
+
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_INFO, intro);
diff --git a/src/test/test-net-naming-scheme.c b/src/test/test-net-naming-scheme.c
new file mode 100644
index 0000000..f7ec5a6
--- /dev/null
+++ b/src/test/test-net-naming-scheme.c
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "netif-naming-scheme.h"
+#include "string-util.h"
+#include "tests.h"
+
+#ifdef _DEFAULT_NET_NAMING_SCHEME
+/* The primary purpose of this check is to verify that _DEFAULT_NET_NAMING_SCHEME_TEST
+ * is a valid identifier. If an invalid name is given during configuration, this will
+ * fail with a name error. */
+assert_cc(_DEFAULT_NET_NAMING_SCHEME >= 0);
+#endif
+
+TEST(default_net_naming_scheme) {
+ const NamingScheme *n;
+ assert_se(n = naming_scheme_from_name(DEFAULT_NET_NAMING_SCHEME));
+ log_info("default → %s", n->name);
+
+ assert_se(naming_scheme_from_name(n->name) == n);
+}
+
+TEST(naming_scheme_conversions) {
+ const NamingScheme *n;
+ assert_se(n = naming_scheme_from_name("latest"));
+ log_info("latest → %s", n->name);
+
+ assert_se(n = naming_scheme_from_name("v238"));
+ assert_se(streq(n->name, "v238"));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-netlink-manual.c b/src/test/test-netlink-manual.c
new file mode 100644
index 0000000..6543c61
--- /dev/null
+++ b/src/test/test-netlink-manual.c
@@ -0,0 +1,126 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <arpa/inet.h>
+#include <linux/if_tunnel.h>
+#include <linux/ip.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "sd-netlink.h"
+
+#include "macro.h"
+#include "module-util.h"
+#include "tests.h"
+
+static int load_module(const char *mod_name) {
+ _cleanup_(kmod_unrefp) struct kmod_ctx *ctx = NULL;
+ _cleanup_(kmod_module_unref_listp) struct kmod_list *list = NULL;
+ struct kmod_list *l;
+ int r;
+
+ ctx = kmod_new(NULL, NULL);
+ if (!ctx)
+ return log_oom();
+
+ r = kmod_module_new_from_lookup(ctx, mod_name, &list);
+ if (r < 0)
+ return r;
+
+ kmod_list_foreach(l, list) {
+ _cleanup_(kmod_module_unrefp) struct kmod_module *mod = NULL;
+
+ mod = kmod_module_get_module(l);
+
+ r = kmod_module_probe_insert_module(mod, 0, NULL, NULL, NULL, NULL);
+ if (r > 0)
+ r = -EINVAL;
+ }
+
+ return r;
+}
+
+static int test_tunnel_configure(sd_netlink *rtnl) {
+ int r;
+ sd_netlink_message *m, *n;
+ struct in_addr local, remote;
+
+ /* skip test if module cannot be loaded */
+ r = load_module("ipip");
+ if (r < 0)
+ return log_tests_skipped_errno(r, "failed to load module 'ipip'");
+
+ r = load_module("sit");
+ if (r < 0)
+ return log_tests_skipped_errno(r, "failed to load module 'sit'");
+
+ if (getuid() != 0)
+ return log_tests_skipped("not root");
+
+ /* IPIP tunnel */
+ assert_se(sd_rtnl_message_new_link(rtnl, &m, RTM_NEWLINK, 0) >= 0);
+ assert_se(m);
+
+ assert_se(sd_netlink_message_append_string(m, IFLA_IFNAME, "ipip-tunnel") >= 0);
+ assert_se(sd_netlink_message_append_u32(m, IFLA_MTU, 1234)>= 0);
+
+ assert_se(sd_netlink_message_open_container(m, IFLA_LINKINFO) >= 0);
+
+ assert_se(sd_netlink_message_open_container_union(m, IFLA_INFO_DATA, "ipip") >= 0);
+
+ inet_pton(AF_INET, "192.168.21.1", &local.s_addr);
+ assert_se(sd_netlink_message_append_u32(m, IFLA_IPTUN_LOCAL, local.s_addr) >= 0);
+
+ inet_pton(AF_INET, "192.168.21.2", &remote.s_addr);
+ assert_se(sd_netlink_message_append_u32(m, IFLA_IPTUN_REMOTE, remote.s_addr) >= 0);
+
+ assert_se(sd_netlink_message_close_container(m) >= 0);
+ assert_se(sd_netlink_message_close_container(m) >= 0);
+
+ assert_se(sd_netlink_call(rtnl, m, -1, 0) == 1);
+
+ assert_se((m = sd_netlink_message_unref(m)) == NULL);
+
+ /* sit */
+ assert_se(sd_rtnl_message_new_link(rtnl, &n, RTM_NEWLINK, 0) >= 0);
+ assert_se(n);
+
+ assert_se(sd_netlink_message_append_string(n, IFLA_IFNAME, "sit-tunnel") >= 0);
+ assert_se(sd_netlink_message_append_u32(n, IFLA_MTU, 1234)>= 0);
+
+ assert_se(sd_netlink_message_open_container(n, IFLA_LINKINFO) >= 0);
+
+ assert_se(sd_netlink_message_open_container_union(n, IFLA_INFO_DATA, "sit") >= 0);
+
+ assert_se(sd_netlink_message_append_u8(n, IFLA_IPTUN_PROTO, IPPROTO_IPIP) >= 0);
+
+ inet_pton(AF_INET, "192.168.21.3", &local.s_addr);
+ assert_se(sd_netlink_message_append_u32(n, IFLA_IPTUN_LOCAL, local.s_addr) >= 0);
+
+ inet_pton(AF_INET, "192.168.21.4", &remote.s_addr);
+ assert_se(sd_netlink_message_append_u32(n, IFLA_IPTUN_REMOTE, remote.s_addr) >= 0);
+
+ assert_se(sd_netlink_message_close_container(n) >= 0);
+ assert_se(sd_netlink_message_close_container(n) >= 0);
+
+ assert_se(sd_netlink_call(rtnl, n, -1, 0) == 1);
+
+ assert_se((n = sd_netlink_message_unref(n)) == NULL);
+
+ return EXIT_SUCCESS;
+}
+
+int main(int argc, char *argv[]) {
+ sd_netlink *rtnl;
+ int r;
+
+ test_setup_logging(LOG_INFO);
+
+ assert_se(sd_netlink_open(&rtnl) >= 0);
+ assert_se(rtnl);
+
+ r = test_tunnel_configure(rtnl);
+
+ assert_se((rtnl = sd_netlink_unref(rtnl)) == NULL);
+
+ return r;
+}
diff --git a/src/test/test-nft-set.c b/src/test/test-nft-set.c
new file mode 100644
index 0000000..bb0c902
--- /dev/null
+++ b/src/test/test-nft-set.c
@@ -0,0 +1,78 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <assert.h>
+#include <unistd.h>
+
+#include "firewall-util.h"
+#include "in-addr-util.h"
+#include "log.h"
+#include "netlink-internal.h"
+#include "parse-util.h"
+#include "string-util.h"
+#include "tests.h"
+
+int main(int argc, char **argv) {
+ int r;
+
+ assert_se(argc == 7);
+
+ test_setup_logging(LOG_DEBUG);
+
+ if (getuid() != 0)
+ return log_tests_skipped("not root");
+
+ int nfproto;
+ nfproto = nfproto_from_string(argv[2]);
+ assert_se(nfproto_is_valid(nfproto));
+
+ const char *table = argv[3], *set = argv[4];
+
+ FirewallContext *ctx;
+ r = fw_ctx_new(&ctx);
+ assert_se(r == 0);
+
+ bool add;
+ if (streq(argv[1], "add"))
+ add = true;
+ else
+ add = false;
+
+ if (streq(argv[5], "uint32")) {
+ uint32_t element;
+
+ r = safe_atou32(argv[6], &element);
+ assert_se(r == 0);
+
+ r = nft_set_element_modify_any(ctx, add, nfproto, table, set, &element, sizeof(element));
+ assert_se(r == 0);
+ } else if (streq(argv[5], "uint64")) {
+ uint64_t element;
+
+ r = safe_atou64(argv[6], &element);
+ assert_se(r == 0);
+
+ r = nft_set_element_modify_any(ctx, add, nfproto, table, set, &element, sizeof(element));
+ assert_se(r == 0);
+ } else if (streq(argv[5], "in_addr")) {
+ union in_addr_union addr;
+ int af;
+
+ r = in_addr_from_string_auto(argv[6], &af, &addr);
+ assert_se(r == 0);
+
+ r = nft_set_element_modify_ip(ctx, add, nfproto, af, table, set, &addr);
+ assert_se(r == 0);
+ } else if (streq(argv[5], "network")) {
+ union in_addr_union addr;
+ int af;
+ unsigned char prefixlen;
+
+ r = in_addr_prefix_from_string_auto(argv[6], &af, &addr, &prefixlen);
+ assert_se(r == 0);
+
+ r = nft_set_element_modify_iprange(ctx, add, nfproto, af, table, set, &addr, prefixlen);
+ assert_se(r == 0);
+ }
+
+ return 0;
+}
diff --git a/src/test/test-ns.c b/src/test/test-ns.c
new file mode 100644
index 0000000..97b9fc9
--- /dev/null
+++ b/src/test/test-ns.c
@@ -0,0 +1,125 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "log.h"
+#include "namespace.h"
+#include "tests.h"
+
+int main(int argc, char *argv[]) {
+ const char * const writable[] = {
+ "/home",
+ "-/home/lennart/projects/foobar", /* this should be masked automatically */
+ NULL
+ };
+
+ const char * const readonly[] = {
+ /* "/", */
+ /* "/usr", */
+ "/boot",
+ "/lib",
+ "/usr/lib",
+ "-/lib64",
+ "-/usr/lib64",
+ NULL
+ };
+
+ const char * const exec[] = {
+ "/lib",
+ "/usr",
+ "-/lib64",
+ "-/usr/lib64",
+ NULL
+ };
+
+ const char * const no_exec[] = {
+ "/var",
+ NULL
+ };
+
+ const char *inaccessible[] = {
+ "/home/lennart/projects",
+ NULL
+ };
+
+ static const BindMount bind_mount = {
+ .source = (char*) "/usr/bin",
+ .destination = (char*) "/etc/systemd",
+ .read_only = true,
+ };
+
+ static const TemporaryFileSystem tmpfs = {
+ .path = (char*) "/var",
+ .options = (char*) "ro",
+ };
+
+ char *root_directory;
+ char *projects_directory;
+ int r;
+ char tmp_dir[] = "/tmp/systemd-private-XXXXXX",
+ var_tmp_dir[] = "/var/tmp/systemd-private-XXXXXX";
+
+ test_setup_logging(LOG_DEBUG);
+
+ assert_se(mkdtemp(tmp_dir));
+ assert_se(mkdtemp(var_tmp_dir));
+
+ root_directory = getenv("TEST_NS_CHROOT");
+ projects_directory = getenv("TEST_NS_PROJECTS");
+
+ if (projects_directory)
+ inaccessible[0] = projects_directory;
+
+ log_info("Inaccessible directory: '%s'", inaccessible[0]);
+ if (root_directory)
+ log_info("Chroot: '%s'", root_directory);
+ else
+ log_info("Not chrooted");
+
+ NamespaceParameters p = {
+ .runtime_scope = RUNTIME_SCOPE_SYSTEM,
+
+ .root_directory = root_directory,
+
+ .read_write_paths = (char**) writable,
+ .read_only_paths = (char**) readonly,
+ .inaccessible_paths = (char**) inaccessible,
+
+ .exec_paths = (char**) exec,
+ .no_exec_paths = (char**) no_exec,
+
+ .tmp_dir = tmp_dir,
+ .var_tmp_dir = var_tmp_dir,
+
+ .bind_mounts = &bind_mount,
+ .n_bind_mounts = 1,
+
+ .temporary_filesystems = &tmpfs,
+ .n_temporary_filesystems = 1,
+
+ .private_dev = true,
+ .protect_control_groups = true,
+ .protect_kernel_tunables = true,
+ .protect_kernel_modules = true,
+ .protect_proc = PROTECT_PROC_NOACCESS,
+ .proc_subset = PROC_SUBSET_PID,
+ };
+
+ r = setup_namespace(&p, NULL);
+ if (r < 0) {
+ log_error_errno(r, "Failed to set up namespace: %m");
+
+ log_info("Usage:\n"
+ " sudo TEST_NS_PROJECTS=/home/lennart/projects ./test-ns\n"
+ " sudo TEST_NS_CHROOT=/home/alban/debian-tree TEST_NS_PROJECTS=/home/alban/debian-tree/home/alban/Documents ./test-ns");
+
+ return 1;
+ }
+
+ execl("/bin/sh", "/bin/sh", NULL);
+ log_error_errno(errno, "execl(): %m");
+
+ return 1;
+}
diff --git a/src/test/test-nscd-flush.c b/src/test/test-nscd-flush.c
new file mode 100644
index 0000000..1a5a808
--- /dev/null
+++ b/src/test/test-nscd-flush.c
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "main-func.h"
+#include "nscd-flush.h"
+#include "strv.h"
+#include "tests.h"
+
+static int run(int argc, char *argv[]) {
+ int r;
+
+ test_setup_logging(LOG_DEBUG);
+
+ r = nscd_flush_cache(STRV_MAKE("group", "passwd", "hosts"));
+ if (r < 0)
+ return log_error_errno(r, "Failed to flush NSCD cache");
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/test/test-nss-hosts.c b/src/test/test-nss-hosts.c
new file mode 100644
index 0000000..72a9c64
--- /dev/null
+++ b/src/test/test-nss-hosts.c
@@ -0,0 +1,495 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "af-list.h"
+#include "alloc-util.h"
+#include "dlfcn-util.h"
+#include "env-util.h"
+#include "errno-list.h"
+#include "format-util.h"
+#include "hexdecoct.h"
+#include "hostname-util.h"
+#include "in-addr-util.h"
+#include "local-addresses.h"
+#include "log.h"
+#include "main-func.h"
+#include "nss-test-util.h"
+#include "nss-util.h"
+#include "parse-util.h"
+#include "path-util.h"
+#include "socket-util.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+
+static size_t arg_bufsize = 1024;
+
+static const char* af_to_string(int family, char *buf, size_t buf_len) {
+ const char *name;
+
+ if (family == AF_UNSPEC)
+ return "*";
+
+ name = af_to_name(family);
+ if (name)
+ return name;
+
+ (void) snprintf(buf, buf_len, "%i", family);
+ return buf;
+}
+
+static int print_gaih_addrtuples(const struct gaih_addrtuple *tuples) {
+ int r, n = 0;
+
+ for (const struct gaih_addrtuple *it = tuples; it; it = it->next) {
+ _cleanup_free_ char *a = NULL;
+ union in_addr_union u;
+ char family_name[DECIMAL_STR_MAX(int)];
+
+ memcpy(&u, it->addr, 16);
+ r = in_addr_to_string(it->family, &u, &a);
+ assert_se(IN_SET(r, 0, -EAFNOSUPPORT));
+ if (r == -EAFNOSUPPORT)
+ assert_se(a = hexmem(it->addr, 16));
+
+ log_info(" \"%s\" %s %s %s",
+ it->name,
+ af_to_string(it->family, family_name, sizeof family_name),
+ a,
+ FORMAT_IFNAME_FULL(it->scopeid, FORMAT_IFNAME_IFINDEX_WITH_PERCENT));
+
+ n++;
+ }
+ return n;
+}
+
+static void print_struct_hostent(struct hostent *host, const char *canon) {
+ log_info(" \"%s\"", host->h_name);
+ STRV_FOREACH(s, host->h_aliases)
+ log_info(" alias \"%s\"", *s);
+ STRV_FOREACH(s, host->h_addr_list) {
+ union in_addr_union u;
+ _cleanup_free_ char *a = NULL;
+ char family_name[DECIMAL_STR_MAX(int)];
+ int r;
+
+ assert_se((unsigned) host->h_length == FAMILY_ADDRESS_SIZE(host->h_addrtype));
+ memcpy(&u, *s, host->h_length);
+ r = in_addr_to_string(host->h_addrtype, &u, &a);
+ assert_se(r == 0);
+ log_info(" %s %s",
+ af_to_string(host->h_addrtype, family_name, sizeof family_name),
+ a);
+ }
+ if (canon)
+ log_info(" canonical: \"%s\"", canon);
+}
+
+static void test_gethostbyname4_r(void *handle, const char *module, const char *name) {
+ const char *fname;
+ _nss_gethostbyname4_r_t f;
+ char buffer[arg_bufsize];
+ struct gaih_addrtuple *pat = NULL;
+ int errno1 = 999, errno2 = 999; /* nss-dns doesn't set those */
+ int32_t ttl = INT32_MAX; /* nss-dns wants to return the lowest ttl,
+ and will access this variable through *ttlp,
+ so we need to set it to something.
+ I'm not sure if this is a bug in nss-dns
+ or not. */
+ enum nss_status status;
+ char pretty_status[DECIMAL_STR_MAX(enum nss_status)];
+ int n;
+
+ fname = strjoina("_nss_", module, "_gethostbyname4_r");
+ f = dlsym(handle, fname);
+ log_debug("dlsym(0x%p, %s) → 0x%p", handle, fname, f);
+ if (!f) {
+ log_info("%s not defined", fname);
+ return;
+ }
+
+ status = f(name, &pat, buffer, sizeof buffer, &errno1, &errno2, &ttl);
+ if (status == NSS_STATUS_SUCCESS) {
+ log_info("%s(\"%s\") → status=%s%-20spat=buffer+0x%"PRIxPTR" errno=%d/%s h_errno=%d/%s ttl=%"PRIi32,
+ fname, name,
+ nss_status_to_string(status, pretty_status, sizeof pretty_status), "\n",
+ pat ? (uintptr_t) pat - (uintptr_t) buffer : 0,
+ errno1, errno_to_name(errno1) ?: "---",
+ errno2, hstrerror(errno2),
+ ttl);
+ n = print_gaih_addrtuples(pat);
+ } else {
+ log_info("%s(\"%s\") → status=%s%-20spat=0x%p errno=%d/%s h_errno=%d/%s",
+ fname, name,
+ nss_status_to_string(status, pretty_status, sizeof pretty_status), "\n",
+ pat,
+ errno1, errno_to_name(errno1) ?: "---",
+ errno2, hstrerror(errno2));
+ n = 0;
+ }
+
+ if (STR_IN_SET(module, "resolve", "mymachines") && status == NSS_STATUS_UNAVAIL)
+ return;
+
+ if (streq(name, "localhost")) {
+ if (streq(module, "myhostname")) {
+ assert_se(status == NSS_STATUS_SUCCESS);
+ assert_se(n == socket_ipv6_is_enabled() + 1);
+
+ } else if (streq(module, "resolve") && getenv_bool_secure("SYSTEMD_NSS_RESOLVE_SYNTHESIZE") != 0) {
+ assert_se(status == NSS_STATUS_SUCCESS);
+ if (socket_ipv6_is_enabled())
+ assert_se(n == 2);
+ else
+ assert_se(n <= 2); /* Even if IPv6 is disabled, /etc/hosts may contain ::1. */
+ }
+ }
+}
+
+static void test_gethostbyname3_r(void *handle, const char *module, const char *name, int af) {
+ const char *fname;
+ _nss_gethostbyname3_r_t f;
+ char buffer[arg_bufsize];
+ int errno1 = 999, errno2 = 999; /* nss-dns doesn't set those */
+ int32_t ttl = INT32_MAX; /* nss-dns wants to return the lowest ttl,
+ and will access this variable through *ttlp,
+ so we need to set it to something.
+ I'm not sure if this is a bug in nss-dns
+ or not. */
+ enum nss_status status;
+ char pretty_status[DECIMAL_STR_MAX(enum nss_status)];
+ struct hostent host;
+ char *canon;
+ char family_name[DECIMAL_STR_MAX(int)];
+
+ fname = strjoina("_nss_", module, "_gethostbyname3_r");
+ f = dlsym(handle, fname);
+ log_debug("dlsym(0x%p, %s) → 0x%p", handle, fname, f);
+ if (!f) {
+ log_info("%s not defined", fname);
+ return;
+ }
+
+ status = f(name, af, &host, buffer, sizeof buffer, &errno1, &errno2, &ttl, &canon);
+ log_info("%s(\"%s\", %s) → status=%s%-20serrno=%d/%s h_errno=%d/%s ttl=%"PRIi32,
+ fname, name, af_to_string(af, family_name, sizeof family_name),
+ nss_status_to_string(status, pretty_status, sizeof pretty_status), "\n",
+ errno1, errno_to_name(errno1) ?: "---",
+ errno2, hstrerror(errno2),
+ ttl);
+ if (status == NSS_STATUS_SUCCESS)
+ print_struct_hostent(&host, canon);
+}
+
+static void test_gethostbyname2_r(void *handle, const char *module, const char *name, int af) {
+ const char *fname;
+ _nss_gethostbyname2_r_t f;
+ char buffer[arg_bufsize];
+ int errno1 = 999, errno2 = 999; /* nss-dns doesn't set those */
+ enum nss_status status;
+ char pretty_status[DECIMAL_STR_MAX(enum nss_status)];
+ struct hostent host;
+ char family_name[DECIMAL_STR_MAX(int)];
+
+ fname = strjoina("_nss_", module, "_gethostbyname2_r");
+ f = dlsym(handle, fname);
+ log_debug("dlsym(0x%p, %s) → 0x%p", handle, fname, f);
+ if (!f) {
+ log_info("%s not defined", fname);
+ return;
+ }
+
+ status = f(name, af, &host, buffer, sizeof buffer, &errno1, &errno2);
+ log_info("%s(\"%s\", %s) → status=%s%-20serrno=%d/%s h_errno=%d/%s",
+ fname, name, af_to_string(af, family_name, sizeof family_name),
+ nss_status_to_string(status, pretty_status, sizeof pretty_status), "\n",
+ errno1, errno_to_name(errno1) ?: "---",
+ errno2, hstrerror(errno2));
+ if (status == NSS_STATUS_SUCCESS)
+ print_struct_hostent(&host, NULL);
+}
+
+static void test_gethostbyname_r(void *handle, const char *module, const char *name) {
+ const char *fname;
+ _nss_gethostbyname_r_t f;
+ char buffer[arg_bufsize];
+ int errno1 = 999, errno2 = 999; /* nss-dns doesn't set those */
+ enum nss_status status;
+ char pretty_status[DECIMAL_STR_MAX(enum nss_status)];
+ struct hostent host;
+
+ fname = strjoina("_nss_", module, "_gethostbyname_r");
+ f = dlsym(handle, fname);
+ log_debug("dlsym(0x%p, %s) → 0x%p", handle, fname, f);
+ if (!f) {
+ log_info("%s not defined", fname);
+ return;
+ }
+
+ status = f(name, &host, buffer, sizeof buffer, &errno1, &errno2);
+ log_info("%s(\"%s\") → status=%s%-20serrno=%d/%s h_errno=%d/%s",
+ fname, name,
+ nss_status_to_string(status, pretty_status, sizeof pretty_status), "\n",
+ errno1, errno_to_name(errno1) ?: "---",
+ errno2, hstrerror(errno2));
+ if (status == NSS_STATUS_SUCCESS)
+ print_struct_hostent(&host, NULL);
+}
+
+static void test_gethostbyaddr2_r(void *handle,
+ const char *module,
+ const void* addr, socklen_t len,
+ int af) {
+
+ const char *fname;
+ _nss_gethostbyaddr2_r_t f;
+ char buffer[arg_bufsize];
+ int errno1 = 999, errno2 = 999; /* nss-dns doesn't set those */
+ enum nss_status status;
+ char pretty_status[DECIMAL_STR_MAX(enum nss_status)];
+ struct hostent host;
+ int32_t ttl = INT32_MAX;
+ _cleanup_free_ char *addr_pretty = NULL;
+
+ fname = strjoina("_nss_", module, "_gethostbyaddr2_r");
+ f = dlsym(handle, fname);
+
+ log_full_errno(f ? LOG_DEBUG : LOG_INFO, errno,
+ "dlsym(0x%p, %s) → 0x%p: %m", handle, fname, f);
+ if (!f) {
+ log_info("%s not defined", fname);
+ return;
+ }
+
+ assert_se(in_addr_to_string(af, addr, &addr_pretty) >= 0);
+
+ status = f(addr, len, af, &host, buffer, sizeof buffer, &errno1, &errno2, &ttl);
+ log_info("%s(\"%s\") → status=%s%-20serrno=%d/%s h_errno=%d/%s ttl=%"PRIi32,
+ fname, addr_pretty,
+ nss_status_to_string(status, pretty_status, sizeof pretty_status), "\n",
+ errno1, errno_to_name(errno1) ?: "---",
+ errno2, hstrerror(errno2),
+ ttl);
+ if (status == NSS_STATUS_SUCCESS)
+ print_struct_hostent(&host, NULL);
+}
+
+static void test_gethostbyaddr_r(void *handle,
+ const char *module,
+ const void* addr, socklen_t len,
+ int af) {
+
+ const char *fname;
+ _nss_gethostbyaddr_r_t f;
+ char buffer[arg_bufsize];
+ int errno1 = 999, errno2 = 999; /* nss-dns doesn't set those */
+ enum nss_status status;
+ char pretty_status[DECIMAL_STR_MAX(enum nss_status)];
+ struct hostent host;
+ _cleanup_free_ char *addr_pretty = NULL;
+
+ fname = strjoina("_nss_", module, "_gethostbyaddr_r");
+ f = dlsym(handle, fname);
+
+ log_full_errno(f ? LOG_DEBUG : LOG_INFO, errno,
+ "dlsym(0x%p, %s) → 0x%p: %m", handle, fname, f);
+ if (!f) {
+ log_info("%s not defined", fname);
+ return;
+ }
+
+ assert_se(in_addr_to_string(af, addr, &addr_pretty) >= 0);
+
+ status = f(addr, len, af, &host, buffer, sizeof buffer, &errno1, &errno2);
+ log_info("%s(\"%s\") → status=%s%-20serrno=%d/%s h_errno=%d/%s",
+ fname, addr_pretty,
+ nss_status_to_string(status, pretty_status, sizeof pretty_status), "\n",
+ errno1, errno_to_name(errno1) ?: "---",
+ errno2, hstrerror(errno2));
+ if (status == NSS_STATUS_SUCCESS)
+ print_struct_hostent(&host, NULL);
+}
+
+static void test_byname(void *handle, const char *module, const char *name) {
+ test_gethostbyname4_r(handle, module, name);
+ puts("");
+
+ test_gethostbyname3_r(handle, module, name, AF_INET);
+ puts("");
+ test_gethostbyname3_r(handle, module, name, AF_INET6);
+ puts("");
+ test_gethostbyname3_r(handle, module, name, AF_UNSPEC);
+ puts("");
+ test_gethostbyname3_r(handle, module, name, AF_UNIX);
+ puts("");
+
+ test_gethostbyname2_r(handle, module, name, AF_INET);
+ puts("");
+ test_gethostbyname2_r(handle, module, name, AF_INET6);
+ puts("");
+ test_gethostbyname2_r(handle, module, name, AF_UNSPEC);
+ puts("");
+ test_gethostbyname2_r(handle, module, name, AF_UNIX);
+ puts("");
+
+ test_gethostbyname_r(handle, module, name);
+ puts("");
+}
+
+static void test_byaddr(void *handle,
+ const char *module,
+ const void* addr, socklen_t len,
+ int af) {
+ test_gethostbyaddr2_r(handle, module, addr, len, af);
+ puts("");
+
+ test_gethostbyaddr_r(handle, module, addr, len, af);
+ puts("");
+}
+
+static int make_addresses(struct local_address **addresses) {
+ int n;
+ _cleanup_free_ struct local_address *addrs = NULL;
+
+ n = local_addresses(NULL, 0, AF_UNSPEC, &addrs);
+ if (n < 0)
+ log_info_errno(n, "Failed to query local addresses: %m");
+
+ assert_se(GREEDY_REALLOC(addrs, n + 3));
+
+ addrs[n++] = (struct local_address) { .family = AF_INET,
+ .address.in = { htobe32(0x7F000001) } };
+ addrs[n++] = (struct local_address) { .family = AF_INET,
+ .address.in = { htobe32(0x7F000002) } };
+ addrs[n++] = (struct local_address) { .family = AF_INET6,
+ .address.in6 = in6addr_loopback };
+
+ *addresses = TAKE_PTR(addrs);
+ return n;
+}
+
+static int test_one_module(const char *dir,
+ const char *module,
+ char **names,
+ struct local_address *addresses,
+ int n_addresses) {
+
+ log_info("======== %s ========", module);
+
+ _cleanup_(dlclosep) void *handle = nss_open_handle(dir, module, RTLD_LAZY|RTLD_NODELETE);
+ if (!handle)
+ return -EINVAL;
+
+ STRV_FOREACH(name, names)
+ test_byname(handle, module, *name);
+
+ for (int i = 0; i < n_addresses; i++)
+ test_byaddr(handle, module,
+ &addresses[i].address,
+ FAMILY_ADDRESS_SIZE(addresses[i].family),
+ addresses[i].family);
+
+ log_info(" ");
+ return 0;
+}
+
+static int parse_argv(int argc, char **argv,
+ char ***the_modules,
+ char ***the_names,
+ struct local_address **the_addresses, int *n_addresses) {
+
+ _cleanup_strv_free_ char **modules = NULL, **names = NULL;
+ _cleanup_free_ struct local_address *addrs = NULL;
+ const char *p;
+ int r, n = 0;
+
+ p = getenv("SYSTEMD_TEST_NSS_BUFSIZE");
+ if (p) {
+ r = safe_atozu(p, &arg_bufsize);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse $SYSTEMD_TEST_NSS_BUFSIZE");
+ }
+
+ if (argc > 1)
+ modules = strv_new(argv[1]);
+ else
+ modules = strv_new(
+#if ENABLE_NSS_MYHOSTNAME
+ "myhostname",
+#endif
+#if ENABLE_NSS_RESOLVE
+ "resolve",
+#endif
+#if ENABLE_NSS_MYMACHINES
+ "mymachines",
+#endif
+ NULL);
+ assert_se(modules);
+
+ if (argc > 2) {
+ int family;
+ union in_addr_union address;
+
+ STRV_FOREACH(name, argv + 2) {
+ r = in_addr_from_string_auto(*name, &family, &address);
+ if (r < 0) {
+ /* assume this is a name */
+ r = strv_extend(&names, *name);
+ if (r < 0)
+ return r;
+ } else {
+ assert_se(GREEDY_REALLOC0(addrs, n + 1));
+
+ addrs[n++] = (struct local_address) { .family = family,
+ .address = address };
+ }
+ }
+ } else {
+ _cleanup_free_ char *hostname = NULL;
+ assert_se(hostname = gethostname_malloc());
+ assert_se(names = strv_new("localhost",
+ "_gateway",
+ "_outbound",
+ hostname,
+ slow_tests_enabled() ? "foo_no_such_host" : NULL));
+
+ n = make_addresses(&addrs);
+ assert_se(n >= 0);
+ }
+
+ *the_modules = TAKE_PTR(modules);
+ *the_names = TAKE_PTR(names);
+ *the_addresses = TAKE_PTR(addrs);
+ *n_addresses = n;
+ return 0;
+}
+
+static int run(int argc, char **argv) {
+ _cleanup_free_ char *dir = NULL;
+ _cleanup_strv_free_ char **modules = NULL, **names = NULL;
+ _cleanup_free_ struct local_address *addresses = NULL;
+ int n_addresses = 0;
+ int r;
+
+ test_setup_logging(LOG_INFO);
+
+ r = parse_argv(argc, argv, &modules, &names, &addresses, &n_addresses);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse arguments: %m");
+
+ assert_se(path_extract_directory(argv[0], &dir) >= 0);
+
+ STRV_FOREACH(module, modules) {
+ r = test_one_module(dir, *module, names, addresses, n_addresses);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/test/test-nss-users.c b/src/test/test-nss-users.c
new file mode 100644
index 0000000..5178779
--- /dev/null
+++ b/src/test/test-nss-users.c
@@ -0,0 +1,256 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <pwd.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "dlfcn-util.h"
+#include "errno-list.h"
+#include "format-util.h"
+#include "log.h"
+#include "main-func.h"
+#include "nss-test-util.h"
+#include "nss-util.h"
+#include "path-util.h"
+#include "parse-util.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "user-util.h"
+
+static size_t arg_bufsize = 1024;
+
+static void print_struct_passwd(const struct passwd *pwd) {
+ log_info(" \"%s\" / "UID_FMT":"GID_FMT,
+ pwd->pw_name, pwd->pw_uid, pwd->pw_gid);
+ log_info(" passwd=\"%s\"", pwd->pw_passwd);
+ log_info(" gecos=\"%s\"", pwd->pw_gecos);
+ log_info(" dir=\"%s\"", pwd->pw_dir);
+ log_info(" shell=\"%s\"", pwd->pw_shell);
+}
+
+static void print_struct_group(const struct group *gr) {
+ _cleanup_free_ char *members = NULL;
+
+ log_info(" \"%s\" / "GID_FMT,
+ gr->gr_name, gr->gr_gid);
+ log_info(" passwd=\"%s\"", gr->gr_passwd);
+
+ assert_se(members = strv_join(gr->gr_mem, ", "));
+ // FIXME: use shell_maybe_quote(SHELL_ESCAPE_EMPTY) when it becomes available
+ log_info(" members=%s", members);
+}
+
+static void test_getpwnam_r(void *handle, const char *module, const char *name) {
+ const char *fname;
+ _nss_getpwnam_r_t f;
+ char buffer[arg_bufsize];
+ int errno1 = 999; /* nss-dns doesn't set those */
+ enum nss_status status;
+ char pretty_status[DECIMAL_STR_MAX(enum nss_status)];
+ struct passwd pwd;
+
+ fname = strjoina("_nss_", module, "_getpwnam_r");
+ f = dlsym(handle, fname);
+ log_debug("dlsym(0x%p, %s) → 0x%p", handle, fname, f);
+ if (!f) {
+ log_info("%s not defined", fname);
+ return;
+ }
+
+ status = f(name, &pwd, buffer, sizeof buffer, &errno1);
+ log_info("%s(\"%s\") → status=%s%-20serrno=%d/%s",
+ fname, name,
+ nss_status_to_string(status, pretty_status, sizeof pretty_status), "\n",
+ errno1, errno_to_name(errno1) ?: "---");
+ if (status == NSS_STATUS_SUCCESS)
+ print_struct_passwd(&pwd);
+}
+
+static void test_getgrnam_r(void *handle, const char *module, const char *name) {
+ const char *fname;
+ _nss_getgrnam_r_t f;
+ char buffer[arg_bufsize];
+ int errno1 = 999; /* nss-dns doesn't set those */
+ enum nss_status status;
+ char pretty_status[DECIMAL_STR_MAX(enum nss_status)];
+ struct group gr;
+
+ fname = strjoina("_nss_", module, "_getgrnam_r");
+ f = dlsym(handle, fname);
+ log_debug("dlsym(0x%p, %s) → 0x%p", handle, fname, f);
+ if (!f) {
+ log_info("%s not defined", fname);
+ return;
+ }
+
+ status = f(name, &gr, buffer, sizeof buffer, &errno1);
+ log_info("%s(\"%s\") → status=%s%-20serrno=%d/%s",
+ fname, name,
+ nss_status_to_string(status, pretty_status, sizeof pretty_status), "\n",
+ errno1, errno_to_name(errno1) ?: "---");
+ if (status == NSS_STATUS_SUCCESS)
+ print_struct_group(&gr);
+}
+
+static void test_getpwuid_r(void *handle, const char *module, uid_t uid) {
+ const char *fname;
+ _nss_getpwuid_r_t f;
+ char buffer[arg_bufsize];
+ int errno1 = 999; /* nss-dns doesn't set those */
+ enum nss_status status;
+ char pretty_status[DECIMAL_STR_MAX(enum nss_status)];
+ struct passwd pwd;
+
+ fname = strjoina("_nss_", module, "_getpwuid_r");
+ f = dlsym(handle, fname);
+ log_debug("dlsym(0x%p, %s) → 0x%p", handle, fname, f);
+ if (!f) {
+ log_info("%s not defined", fname);
+ return;
+ }
+
+ status = f(uid, &pwd, buffer, sizeof buffer, &errno1);
+ log_info("%s("UID_FMT") → status=%s%-20serrno=%d/%s",
+ fname, uid,
+ nss_status_to_string(status, pretty_status, sizeof pretty_status), "\n",
+ errno1, errno_to_name(errno1) ?: "---");
+ if (status == NSS_STATUS_SUCCESS)
+ print_struct_passwd(&pwd);
+}
+
+static void test_getgrgid_r(void *handle, const char *module, gid_t gid) {
+ const char *fname;
+ _nss_getgrgid_r_t f;
+ char buffer[arg_bufsize];
+ int errno1 = 999; /* nss-dns doesn't set those */
+ enum nss_status status;
+ char pretty_status[DECIMAL_STR_MAX(enum nss_status)];
+ struct group gr;
+
+ fname = strjoina("_nss_", module, "_getgrgid_r");
+ f = dlsym(handle, fname);
+ log_debug("dlsym(0x%p, %s) → 0x%p", handle, fname, f);
+ if (!f) {
+ log_info("%s not defined", fname);
+ return;
+ }
+
+ status = f(gid, &gr, buffer, sizeof buffer, &errno1);
+ log_info("%s("GID_FMT") → status=%s%-20serrno=%d/%s",
+ fname, gid,
+ nss_status_to_string(status, pretty_status, sizeof pretty_status), "\n",
+ errno1, errno_to_name(errno1) ?: "---");
+ if (status == NSS_STATUS_SUCCESS)
+ print_struct_group(&gr);
+}
+
+static void test_byname(void *handle, const char *module, const char *name) {
+ test_getpwnam_r(handle, module, name);
+ test_getgrnam_r(handle, module, name);
+ puts("");
+}
+
+static void test_byuid(void *handle, const char *module, uid_t uid) {
+ test_getpwuid_r(handle, module, uid);
+ test_getgrgid_r(handle, module, uid);
+ puts("");
+}
+
+static int test_one_module(const char *dir,
+ const char *module,
+ char **names) {
+
+ log_info("======== %s ========", module);
+
+ _cleanup_(dlclosep) void *handle = nss_open_handle(dir, module, RTLD_LAZY|RTLD_NODELETE);
+ if (!handle)
+ return -EINVAL;
+
+ STRV_FOREACH(name, names)
+ test_byname(handle, module, *name);
+
+ STRV_FOREACH(name, names) {
+ uid_t uid;
+
+ assert_cc(sizeof(uid_t) == sizeof(uint32_t));
+ /* We use safe_atou32 because we don't want to refuse invalid uids. */
+ if (safe_atou32(*name, &uid) < 0)
+ continue;
+
+ test_byuid(handle, module, uid);
+ }
+
+ log_info(" ");
+ return 0;
+}
+
+static int parse_argv(int argc, char **argv,
+ char ***the_modules,
+ char ***the_names) {
+
+ _cleanup_strv_free_ char **modules = NULL, **names = NULL;
+ const char *p;
+ int r;
+
+ p = getenv("SYSTEMD_TEST_NSS_BUFSIZE");
+ if (p) {
+ r = safe_atozu(p, &arg_bufsize);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse $SYSTEMD_TEST_NSS_BUFSIZE");
+ }
+
+ if (argc > 1)
+ modules = strv_new(argv[1]);
+ else
+ modules = strv_new(
+#if ENABLE_NSS_SYSTEMD
+ "systemd",
+#endif
+#if ENABLE_NSS_MYMACHINES
+ "mymachines",
+#endif
+ NULL);
+ assert_se(modules);
+
+ if (argc > 2)
+ names = strv_copy(strv_skip(argv, 2));
+ else
+ names = strv_new("root",
+ NOBODY_USER_NAME,
+ "foo_no_such_user",
+ "0",
+ "65534");
+ assert_se(names);
+
+ *the_modules = TAKE_PTR(modules);
+ *the_names = TAKE_PTR(names);
+ return 0;
+}
+
+static int run(int argc, char **argv) {
+ _cleanup_free_ char *dir = NULL;
+ _cleanup_strv_free_ char **modules = NULL, **names = NULL;
+ int r;
+
+ test_setup_logging(LOG_INFO);
+
+ r = parse_argv(argc, argv, &modules, &names);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse arguments: %m");
+
+ assert_se(path_extract_directory(argv[0], &dir) >= 0);
+
+ STRV_FOREACH(module, modules) {
+ r = test_one_module(dir, *module, names);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/test/test-nulstr-util.c b/src/test/test-nulstr-util.c
new file mode 100644
index 0000000..95c25f1
--- /dev/null
+++ b/src/test/test-nulstr-util.c
@@ -0,0 +1,184 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "nulstr-util.h"
+#include "set.h"
+#include "strv.h"
+#include "tests.h"
+
+TEST(strv_split_nulstr) {
+ _cleanup_strv_free_ char **l = NULL;
+ const char nulstr[] = "str0\0str1\0str2\0str3\0";
+
+ l = strv_split_nulstr(nulstr);
+ assert_se(l);
+
+ assert_se(streq(l[0], "str0"));
+ assert_se(streq(l[1], "str1"));
+ assert_se(streq(l[2], "str2"));
+ assert_se(streq(l[3], "str3"));
+}
+
+#define strv_parse_nulstr_full_one(s, n, e0, e1) \
+ ({ \
+ _cleanup_strv_free_ char **v0 = NULL, **v1 = NULL; \
+ \
+ assert_se(v0 = strv_parse_nulstr_full(s, n, false)); \
+ assert_se(strv_equal(v0, e0)); \
+ assert_se(v1 = strv_parse_nulstr_full(s, n, true)); \
+ assert_se(strv_equal(v1, e1)); \
+ })
+
+TEST(strv_parse_nulstr_full) {
+ const char nulstr1[] = "hoge\0hoge2\0hoge3\0\0hoge5\0\0xxx";
+ const char nulstr2[] = "hoge\0hoge2\0hoge3\0\0hoge5\0\0xxx\0\0\0";
+
+ strv_parse_nulstr_full_one(nulstr1, sizeof(nulstr1) - 1,
+ STRV_MAKE("hoge", "hoge2", "hoge3", "", "hoge5", "", "xxx"),
+ STRV_MAKE("hoge", "hoge2", "hoge3", "", "hoge5", "", "xxx"));
+
+ strv_parse_nulstr_full_one(nulstr2, sizeof(nulstr2) - 1,
+ STRV_MAKE("hoge", "hoge2", "hoge3", "", "hoge5", "", "xxx", "", ""),
+ STRV_MAKE("hoge", "hoge2", "hoge3", "", "hoge5", "", "xxx"));
+
+ strv_parse_nulstr_full_one(((const char[0]) {}), 0,
+ STRV_MAKE_EMPTY, STRV_MAKE_EMPTY);
+
+ strv_parse_nulstr_full_one(((const char[1]) { 0 }), 1,
+ STRV_MAKE(""), STRV_MAKE_EMPTY);
+
+ strv_parse_nulstr_full_one(((const char[1]) { 'x' }), 1,
+ STRV_MAKE("x"), STRV_MAKE("x"));
+
+ strv_parse_nulstr_full_one(((const char[2]) { 0, 0 }), 2,
+ STRV_MAKE("", ""), STRV_MAKE_EMPTY);
+
+ strv_parse_nulstr_full_one(((const char[2]) { 'x', 0 }), 2,
+ STRV_MAKE("x"), STRV_MAKE("x"));
+
+ strv_parse_nulstr_full_one(((const char[3]) { 0, 0, 0 }), 3,
+ STRV_MAKE("", "", ""), STRV_MAKE_EMPTY);
+
+ strv_parse_nulstr_full_one(((const char[3]) { 'x', 0, 0 }), 3,
+ STRV_MAKE("x", ""), STRV_MAKE("x"));
+
+ strv_parse_nulstr_full_one(((const char[3]) { 0, 'x', 0 }), 3,
+ STRV_MAKE("", "x"), STRV_MAKE("", "x"));
+
+ strv_parse_nulstr_full_one(((const char[3]) { 0, 0, 'x' }), 3,
+ STRV_MAKE("", "", "x"), STRV_MAKE("", "", "x"));
+
+ strv_parse_nulstr_full_one(((const char[3]) { 'x', 'x', 0 }), 3,
+ STRV_MAKE("xx"), STRV_MAKE("xx"));
+
+ strv_parse_nulstr_full_one(((const char[3]) { 0, 'x', 'x' }), 3,
+ STRV_MAKE("", "xx"), STRV_MAKE("", "xx"));
+
+ strv_parse_nulstr_full_one(((const char[3]) { 'x', 0, 'x' }), 3,
+ STRV_MAKE("x", "x"), STRV_MAKE("x", "x"));
+
+ strv_parse_nulstr_full_one(((const char[3]) { 'x', 'x', 'x' }), 3,
+ STRV_MAKE("xxx"), STRV_MAKE("xxx"));
+}
+
+static void test_strv_make_nulstr_one(char **l) {
+ _cleanup_free_ char *b = NULL, *c = NULL;
+ _cleanup_strv_free_ char **q = NULL;
+ size_t n, m;
+ unsigned i = 0;
+
+ log_info("/* %s */", __func__);
+
+ assert_se(strv_make_nulstr(l, &b, &n) >= 0);
+ assert_se(q = strv_parse_nulstr(b, n));
+ assert_se(strv_equal(l, q));
+
+ assert_se(strv_make_nulstr(q, &c, &m) >= 0);
+ assert_se(memcmp_nn(b, n, c, m) == 0);
+
+ NULSTR_FOREACH(s, b)
+ assert_se(streq(s, l[i++]));
+ assert_se(i == strv_length(l));
+}
+
+TEST(strv_make_nulstr) {
+ test_strv_make_nulstr_one(NULL);
+ test_strv_make_nulstr_one(STRV_MAKE(NULL));
+ test_strv_make_nulstr_one(STRV_MAKE("foo"));
+ test_strv_make_nulstr_one(STRV_MAKE("foo", "bar"));
+ test_strv_make_nulstr_one(STRV_MAKE("foo", "bar", "quuux"));
+}
+
+TEST(set_make_nulstr) {
+ _cleanup_set_free_free_ Set *set = NULL;
+ size_t len = 0;
+ int r;
+
+ {
+ /* Unallocated and empty set. */
+ static const char expect[] = { 0x00, 0x00 };
+ _cleanup_free_ char *nulstr = NULL;
+
+ r = set_make_nulstr(set, &nulstr, &len);
+ assert_se(r == 0);
+ assert_se(len == 0);
+ assert_se(memcmp(expect, nulstr, len + 2) == 0);
+ }
+
+ {
+ /* Allocated by empty set. */
+ static const char expect[] = { 0x00, 0x00 };
+ _cleanup_free_ char *nulstr = NULL;
+
+ set = set_new(NULL);
+ assert_se(set);
+
+ r = set_make_nulstr(set, &nulstr, &len);
+ assert_se(r == 0);
+ assert_se(len == 0);
+ assert_se(memcmp(expect, nulstr, len + 2) == 0);
+ }
+
+ {
+ /* Non-empty set. */
+ static const char expect[] = { 'a', 'a', 'a', 0x00, 0x00 };
+ _cleanup_free_ char *nulstr = NULL;
+
+ assert_se(set_put_strdup(&set, "aaa") >= 0);
+
+ r = set_make_nulstr(set, &nulstr, &len);
+ assert_se(r == 0);
+ assert_se(len == 4);
+ assert_se(memcmp(expect, nulstr, len + 1) == 0);
+ }
+}
+
+static void test_strv_make_nulstr_binary_one(char **l, const char *b, size_t n) {
+ _cleanup_strv_free_ char **z = NULL;
+ _cleanup_free_ char *a = NULL;
+ size_t m;
+
+ assert_se(strv_make_nulstr(l, &a, &m) >= 0);
+ assert_se(memcmp_nn(a, m, b, n) == 0);
+ assert_se(z = strv_parse_nulstr(a, m));
+ assert_se(strv_equal(l, z));
+}
+
+TEST(strv_make_nulstr_binary) {
+ test_strv_make_nulstr_binary_one(NULL, (const char[0]) {}, 0);
+ test_strv_make_nulstr_binary_one(STRV_MAKE(NULL), (const char[0]) {}, 0);
+ test_strv_make_nulstr_binary_one(STRV_MAKE(""), (const char[1]) { 0 }, 1);
+ test_strv_make_nulstr_binary_one(STRV_MAKE("", ""), (const char[2]) { 0, 0 }, 2);
+ test_strv_make_nulstr_binary_one(STRV_MAKE("x", ""), (const char[3]) { 'x', 0, 0 }, 3);
+ test_strv_make_nulstr_binary_one(STRV_MAKE("", "x"), (const char[3]) { 0, 'x', 0 }, 3);
+ test_strv_make_nulstr_binary_one(STRV_MAKE("", "", ""), (const char[3]) { 0, 0, 0 }, 3);
+ test_strv_make_nulstr_binary_one(STRV_MAKE("x", "", ""), (const char[4]) { 'x', 0, 0, 0 }, 4);
+ test_strv_make_nulstr_binary_one(STRV_MAKE("", "x", ""), (const char[4]) { 0, 'x', 0, 0 }, 4);
+ test_strv_make_nulstr_binary_one(STRV_MAKE("", "", "x"), (const char[4]) { 0, 0, 'x', 0 }, 4);
+ test_strv_make_nulstr_binary_one(STRV_MAKE("x", "x", ""), (const char[5]) { 'x', 0, 'x', 0, 0 }, 5);
+ test_strv_make_nulstr_binary_one(STRV_MAKE("", "x", "x"), (const char[5]) { 0, 'x', 0, 'x', 0 }, 5);
+ test_strv_make_nulstr_binary_one(STRV_MAKE("x", "", "x"), (const char[5]) { 'x', 0, 0, 'x', 0 }, 5);
+ test_strv_make_nulstr_binary_one(STRV_MAKE("x", "x", "x"), (const char[6]) { 'x', 0, 'x', 0, 'x', 0 }, 6);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-open-file.c b/src/test/test-open-file.c
new file mode 100644
index 0000000..1b938ec
--- /dev/null
+++ b/src/test/test-open-file.c
@@ -0,0 +1,185 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "open-file.h"
+#include "string-util.h"
+#include "tests.h"
+
+TEST(open_file_parse) {
+ _cleanup_(open_file_freep) OpenFile *of = NULL;
+ int r;
+
+ r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:read-only", &of);
+
+ assert_se(r >= 0);
+ assert_se(streq(of->path, "/proc/1/ns/mnt"));
+ assert_se(streq(of->fdname, "host-mount-namespace"));
+ assert_se(of->flags == OPENFILE_READ_ONLY);
+
+ of = open_file_free(of);
+ r = open_file_parse("/proc/1/ns/mnt", &of);
+
+ assert_se(r >= 0);
+ assert_se(streq(of->path, "/proc/1/ns/mnt"));
+ assert_se(streq(of->fdname, "mnt"));
+ assert_se(of->flags == 0);
+
+ of = open_file_free(of);
+ r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace", &of);
+
+ assert_se(r >= 0);
+ assert_se(streq(of->path, "/proc/1/ns/mnt"));
+ assert_se(streq(of->fdname, "host-mount-namespace"));
+ assert_se(of->flags == 0);
+
+ of = open_file_free(of);
+ r = open_file_parse("/proc/1/ns/mnt::read-only", &of);
+
+ assert_se(r >= 0);
+ assert_se(streq(of->path, "/proc/1/ns/mnt"));
+ assert_se(streq(of->fdname, "mnt"));
+ assert_se(of->flags == OPENFILE_READ_ONLY);
+
+ of = open_file_free(of);
+ r = open_file_parse("../file.dat:file:read-only", &of);
+
+ assert_se(r == -EINVAL);
+
+ of = open_file_free(of);
+ r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:rw", &of);
+
+ assert_se(r == -EINVAL);
+
+ of = open_file_free(of);
+ r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:append", &of);
+
+ assert_se(r >= 0);
+ assert_se(streq(of->path, "/proc/1/ns/mnt"));
+ assert_se(streq(of->fdname, "host-mount-namespace"));
+ assert_se(of->flags == OPENFILE_APPEND);
+
+ of = open_file_free(of);
+ r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:truncate", &of);
+
+ assert_se(r >= 0);
+ assert_se(streq(of->path, "/proc/1/ns/mnt"));
+ assert_se(streq(of->fdname, "host-mount-namespace"));
+ assert_se(of->flags == OPENFILE_TRUNCATE);
+
+ of = open_file_free(of);
+ r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:read-only,append", &of);
+
+ assert_se(r == -EINVAL);
+
+ of = open_file_free(of);
+ r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:read-only,truncate", &of);
+
+ assert_se(r == -EINVAL);
+
+ of = open_file_free(of);
+ r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:append,truncate", &of);
+
+ assert_se(r == -EINVAL);
+
+ of = open_file_free(of);
+ r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:read-only,read-only", &of);
+
+ assert_se(r == -EINVAL);
+
+ of = open_file_free(of);
+ r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:graceful", &of);
+
+ assert_se(r >= 0);
+ assert_se(streq(of->path, "/proc/1/ns/mnt"));
+ assert_se(streq(of->fdname, "host-mount-namespace"));
+ assert_se(of->flags == OPENFILE_GRACEFUL);
+
+ of = open_file_free(of);
+ r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:read-only,graceful", &of);
+
+ assert_se(r >= 0);
+ assert_se(streq(of->path, "/proc/1/ns/mnt"));
+ assert_se(streq(of->fdname, "host-mount-namespace"));
+ assert_se(of->flags == (OPENFILE_READ_ONLY | OPENFILE_GRACEFUL));
+
+ of = open_file_free(of);
+ r = open_file_parse("/proc/1/ns/mnt:host-mount-namespace:read-only:other", &of);
+
+ assert_se(r == -EINVAL);
+}
+
+TEST(open_file_to_string) {
+ _cleanup_free_ char *s = NULL;
+ _cleanup_(open_file_freep) OpenFile *of = NULL;
+ int r;
+
+ assert_se(of = new (OpenFile, 1));
+ *of = (OpenFile){ .path = strdup("/proc/1/ns/mnt"),
+ .fdname = strdup("host-mount-namespace"),
+ .flags = OPENFILE_READ_ONLY };
+
+ r = open_file_to_string(of, &s);
+
+ assert_se(r >= 0);
+ assert_se(streq(s, "/proc/1/ns/mnt:host-mount-namespace:read-only"));
+
+ s = mfree(s);
+ of->flags = OPENFILE_APPEND;
+
+ r = open_file_to_string(of, &s);
+
+ assert_se(r >= 0);
+ assert_se(streq(s, "/proc/1/ns/mnt:host-mount-namespace:append"));
+
+ s = mfree(s);
+ of->flags = OPENFILE_TRUNCATE;
+
+ r = open_file_to_string(of, &s);
+
+ assert_se(r >= 0);
+ assert_se(streq(s, "/proc/1/ns/mnt:host-mount-namespace:truncate"));
+
+ s = mfree(s);
+ of->flags = OPENFILE_GRACEFUL;
+
+ r = open_file_to_string(of, &s);
+
+ assert_se(r >= 0);
+ assert_se(streq(s, "/proc/1/ns/mnt:host-mount-namespace:graceful"));
+
+ s = mfree(s);
+ of->flags = OPENFILE_READ_ONLY | OPENFILE_GRACEFUL;
+
+ r = open_file_to_string(of, &s);
+
+ assert_se(r >= 0);
+ assert_se(streq(s, "/proc/1/ns/mnt:host-mount-namespace:read-only,graceful"));
+
+ s = mfree(s);
+ of->flags = 0;
+
+ r = open_file_to_string(of, &s);
+
+ assert_se(r >= 0);
+ assert_se(streq(s, "/proc/1/ns/mnt:host-mount-namespace"));
+
+ s = mfree(s);
+ assert_se(free_and_strdup(&of->fdname, "mnt"));
+ of->flags = OPENFILE_READ_ONLY;
+
+ r = open_file_to_string(of, &s);
+
+ assert_se(r >= 0);
+ assert_se(streq(s, "/proc/1/ns/mnt::read-only"));
+
+ s = mfree(s);
+ assert_se(free_and_strdup(&of->path, "/path:with:colon"));
+ assert_se(free_and_strdup(&of->fdname, "path:with:colon"));
+ of->flags = 0;
+
+ r = open_file_to_string(of, &s);
+
+ assert_se(r >= 0);
+ assert_se(streq(s, "/path\\:with\\:colon"));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-openssl.c b/src/test/test-openssl.c
new file mode 100644
index 0000000..dfdd1ab
--- /dev/null
+++ b/src/test/test-openssl.c
@@ -0,0 +1,483 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "hexdecoct.h"
+#include "openssl-util.h"
+#include "tests.h"
+
+TEST(openssl_pkey_from_pem) {
+ DEFINE_HEX_PTR(key_ecc, "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d466b77457759484b6f5a497a6a3043415159494b6f5a497a6a30444151634451674145726a6e4575424c73496c3972687068777976584e50686a346a426e500a44586e794a304b395579724e6764365335413532542b6f5376746b436a365a726c34685847337741515558706f426c532b7448717452714c35513d3d0a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a");
+ _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey_ecc = NULL;
+ assert_se(openssl_pkey_from_pem(key_ecc, key_ecc_len, &pkey_ecc) >= 0);
+
+ _cleanup_free_ void *x = NULL, *y = NULL;
+ size_t x_len, y_len;
+ int curve_id;
+ assert_se(ecc_pkey_to_curve_x_y(pkey_ecc, &curve_id, &x, &x_len, &y, &y_len) >= 0);
+ assert_se(curve_id == NID_X9_62_prime256v1);
+
+ DEFINE_HEX_PTR(expected_x, "ae39c4b812ec225f6b869870caf5cd3e18f88c19cf0d79f22742bd532acd81de");
+ assert_se(memcmp_nn(x, x_len, expected_x, expected_x_len) == 0);
+
+ DEFINE_HEX_PTR(expected_y, "92e40e764fea12bed9028fa66b9788571b7c004145e9a01952fad1eab51a8be5");
+ assert_se(memcmp_nn(y, y_len, expected_y, expected_y_len) == 0);
+
+ DEFINE_HEX_PTR(key_rsa, "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d494942496a414e42676b71686b6947397730424151454641414f43415138414d49494243674b4341514541795639434950652f505852337a436f63787045300a6a575262546c3568585844436b472f584b79374b6d2f4439584942334b734f5a31436a5937375571372f674359363170697838697552756a73413464503165380a593445336c68556d374a332b6473766b626f4b64553243626d52494c2f6675627771694c4d587a41673342575278747234547545443533527a373634554650640a307a70304b68775231496230444c67772f344e67566f314146763378784b4d6478774d45683567676b73733038326332706c354a504e32587677426f744e6b4d0a5471526c745a4a35355244436170696e7153334577376675646c4e735851357746766c7432377a7637344b585165616d704c59433037584f6761304c676c536b0a79754774586b6a50542f735542544a705374615769674d5a6f714b7479563463515a58436b4a52684459614c47587673504233687a766d5671636e6b47654e540a65774944415141420a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a");
+ _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey_rsa = NULL;
+ assert_se(openssl_pkey_from_pem(key_rsa, key_rsa_len, &pkey_rsa) >= 0);
+
+ _cleanup_free_ void *n = NULL, *e = NULL;
+ size_t n_len, e_len;
+ assert_se(rsa_pkey_to_n_e(pkey_rsa, &n, &n_len, &e, &e_len) >= 0);
+
+ DEFINE_HEX_PTR(expected_n, "c95f4220f7bf3d7477cc2a1cc691348d645b4e5e615d70c2906fd72b2eca9bf0fd5c80772ac399d428d8efb52aeff80263ad698b1f22b91ba3b00e1d3f57bc638137961526ec9dfe76cbe46e829d53609b99120bfdfb9bc2a88b317cc0837056471b6be13b840f9dd1cfbeb85053ddd33a742a1c11d486f40cb830ff8360568d4016fdf1c4a31dc7030487982092cb34f36736a65e493cdd97bf0068b4d90c4ea465b59279e510c26a98a7a92dc4c3b7ee76536c5d0e7016f96ddbbcefef829741e6a6a4b602d3b5ce81ad0b8254a4cae1ad5e48cf4ffb140532694ad6968a0319a2a2adc95e1c4195c29094610d868b197bec3c1de1cef995a9c9e419e3537b");
+ assert_se(memcmp_nn(n, n_len, expected_n, expected_n_len) == 0);
+
+ DEFINE_HEX_PTR(expected_e, "010001");
+ assert_se(memcmp_nn(e, e_len, expected_e, expected_e_len) == 0);
+}
+
+TEST(rsa_pkey_n_e) {
+ DEFINE_HEX_PTR(n, "e3975a2124a7c9fe57752d106314ff62f6da731632eac221f1c0255bdcf2a34eeb21e3ab89ba8759ddad3b68be99463c7f03f3d004028a35e6f7c6596aeab2558d490f1e1c38aed2ff796bda8d6d55704eefb6ac55842dd6e606bb707f66acc02f0db2aed0dabab885bd0c850f1bdc8ac4b6bc1f74858db8ca2ab57a3d4217c091e9cd78727a2e36b8126ea629e81fecc69b0bea601000a6c0b749c5be16f53f4fa9f208a581d804234eb6526ba3fee9822d58d1ab9cac2761d7f630eb7ad6054dff0856d41aea219e1adfd87256aa1532202a070f4b1044e718d1f38bbc5a4b1fcb024f04afaafda5edeacfdf0d0bdf35c359acd059e3edb5024e588458f9b5");
+ uint32_t e = htobe32(0x10001);
+
+ _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL;
+ assert_se(rsa_pkey_from_n_e(n, n_len, &e, sizeof(e), &pkey) >= 0);
+
+ _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new((EVP_PKEY*) pkey, NULL);
+ assert_se(ctx);
+ assert_se(EVP_PKEY_verify_init(ctx) == 1);
+
+ const char *msg = "this is a secret";
+ DEFINE_HEX_PTR(sig, "14b53e0c6ad99a350c3d7811e8160f4ae03ad159815bb91bddb9735b833588df2eac221fbd3fc4ece0dd63bfaeddfdaf4ae67021e759f3638bc194836413414f54e8c4d01c9c37fa4488ea2ef772276b8a33822a53c97b1c35acfb4bc621cfb8fad88f0cf7d5491f05236886afbf9ed47f9469536482f50f74a20defa59d99676bed62a17b5eb98641df5a2f8080fa4b24f2749cc152fa65ba34c14022fcb27f1b36f52021950d7b9b6c3042c50b84cfb7d55a5f9235bfd58e1bf1f604eb93416c5fb5fd90cb68f1270dfa9daf67f52c604f62c2f2beee5e7e672b0e6e9833dd43dba99b77668540c850c9a81a5ea7aaf6297383e6135bd64572362333121fc7");
+ assert_se(EVP_PKEY_verify(ctx, sig, sig_len, (unsigned char*) msg, strlen(msg)) == 1);
+
+ DEFINE_HEX_PTR(invalid_sig, "1234");
+ assert_se(EVP_PKEY_verify(ctx, invalid_sig, invalid_sig_len, (unsigned char*) msg, strlen(msg)) != 1);
+
+ _cleanup_free_ void *n2 = NULL, *e2 = NULL;
+ size_t n2_size, e2_size;
+ assert_se(rsa_pkey_to_n_e(pkey, &n2, &n2_size, &e2, &e2_size) >= 0);
+ assert_se(memcmp_nn(n, n_len, n2, n2_size) == 0);
+ assert_se(e2_size <= sizeof(uint32_t));
+ assert_se(memcmp(&((uint8_t*) &e)[sizeof(uint32_t) - e2_size], e2, e2_size) == 0);
+}
+
+TEST(ecc_pkey_curve_x_y) {
+ int curveid = NID_X9_62_prime256v1;
+ DEFINE_HEX_PTR(x, "2830d2c8f65d3efbef12303b968b91692f8bd04045dcb8a9656374e4ae61d818");
+ DEFINE_HEX_PTR(y, "8a80750f76729defdcc2a4bc1a91c22e60109dd6e1ffde634a650a20bab172e9");
+
+ _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL;
+ assert_se(ecc_pkey_from_curve_x_y(curveid, x, x_len, y, y_len, &pkey) >= 0);
+
+ _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new((EVP_PKEY*) pkey, NULL);
+ assert_se(ctx);
+ assert_se(EVP_PKEY_verify_init(ctx) == 1);
+
+ const char *msg = "this is a secret";
+ DEFINE_HEX_PTR(sig, "3045022100f6ca10f7ed57a020679899b26dd5ac5a1079265885e2a6477f527b6a3f02b5ca02207b550eb3e7b69360aff977f7f6afac99c3f28266b6c5338ce373f6b59263000a");
+ assert_se(EVP_PKEY_verify(ctx, sig, sig_len, (unsigned char*) msg, strlen(msg)) == 1);
+
+ DEFINE_HEX_PTR(invalid_sig, "1234");
+ assert_se(EVP_PKEY_verify(ctx, invalid_sig, invalid_sig_len, (unsigned char*) msg, strlen(msg)) != 1);
+
+ _cleanup_free_ void *x2 = NULL, *y2 = NULL;
+ size_t x2_size, y2_size;
+ int curveid2;
+ assert_se(ecc_pkey_to_curve_x_y(pkey, &curveid2, &x2, &x2_size, &y2, &y2_size) >= 0);
+ assert_se(curveid == curveid2);
+ assert_se(memcmp_nn(x, x_len, x2, x2_size) == 0);
+ assert_se(memcmp_nn(y, y_len, y2, y2_size) == 0);
+}
+
+TEST(invalid) {
+ _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL;
+
+ DEFINE_HEX_PTR(key, "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d466b7b");
+ assert_se(openssl_pkey_from_pem(key, key_len, &pkey) == -EIO);
+ assert_se(pkey == NULL);
+}
+
+static const struct {
+ const char *alg;
+ size_t size;
+} digest_size_table[] = {
+ /* SHA1 "family" */
+ { "sha1", 20, },
+#if OPENSSL_VERSION_MAJOR >= 3
+ { "sha-1", 20, },
+#endif
+ /* SHA2 family */
+ { "sha224", 28, },
+ { "sha256", 32, },
+ { "sha384", 48, },
+ { "sha512", 64, },
+#if OPENSSL_VERSION_MAJOR >= 3
+ { "sha-224", 28, },
+ { "sha2-224", 28, },
+ { "sha-256", 32, },
+ { "sha2-256", 32, },
+ { "sha-384", 48, },
+ { "sha2-384", 48, },
+ { "sha-512", 64, },
+ { "sha2-512", 64, },
+#endif
+ /* SHA3 family */
+ { "sha3-224", 28, },
+ { "sha3-256", 32, },
+ { "sha3-384", 48, },
+ { "sha3-512", 64, },
+ /* SM3 family */
+ { "sm3", 32, },
+ /* MD5 family */
+ { "md5", 16, },
+};
+
+TEST(digest_size) {
+ size_t size;
+
+ FOREACH_ARRAY(t, digest_size_table, ELEMENTSOF(digest_size_table)) {
+ assert(openssl_digest_size(t->alg, &size) >= 0);
+ assert_se(size == t->size);
+
+ _cleanup_free_ char *uppercase_alg = strdup(t->alg);
+ assert_se(uppercase_alg);
+ assert_se(openssl_digest_size(ascii_strupper(uppercase_alg), &size) >= 0);
+ assert_se(size == t->size);
+ }
+
+ assert_se(openssl_digest_size("invalid.alg", &size) == -EOPNOTSUPP);
+}
+
+static void verify_digest(const char *digest_alg, const struct iovec *data, size_t n_data, const char *expect) {
+ _cleanup_free_ void *digest = NULL;
+ size_t digest_size;
+ int r;
+
+ r = openssl_digest_many(digest_alg, data, n_data, &digest, &digest_size);
+ if (r == -EOPNOTSUPP)
+ return;
+ assert_se(r >= 0);
+
+ DEFINE_HEX_PTR(e, expect);
+ assert_se(memcmp_nn(e, e_len, digest, digest_size) == 0);
+}
+
+#define _DEFINE_DIGEST_TEST(uniq, alg, expect, ...) \
+ const struct iovec UNIQ_T(i, uniq)[] = { __VA_ARGS__ }; \
+ verify_digest(alg, \
+ UNIQ_T(i, uniq), \
+ ELEMENTSOF(UNIQ_T(i, uniq)), \
+ expect);
+#define DEFINE_DIGEST_TEST(alg, expect, ...) _DEFINE_DIGEST_TEST(UNIQ, alg, expect, __VA_ARGS__)
+#define DEFINE_SHA1_TEST(expect, ...) DEFINE_DIGEST_TEST("SHA1", expect, __VA_ARGS__)
+#define DEFINE_SHA256_TEST(expect, ...) DEFINE_DIGEST_TEST("SHA256", expect, __VA_ARGS__)
+#define DEFINE_SHA384_TEST(expect, ...) DEFINE_DIGEST_TEST("SHA384", expect, __VA_ARGS__)
+#define DEFINE_SHA512_TEST(expect, ...) DEFINE_DIGEST_TEST("SHA512", expect, __VA_ARGS__)
+
+TEST(digest_many) {
+ const struct iovec test = IOVEC_MAKE_STRING("test");
+
+ /* Empty digests */
+ DEFINE_SHA1_TEST("da39a3ee5e6b4b0d3255bfef95601890afd80709");
+ DEFINE_SHA256_TEST("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
+ DEFINE_SHA384_TEST("38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b");
+ DEFINE_SHA512_TEST("cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e");
+
+ DEFINE_SHA1_TEST("a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", test);
+ DEFINE_SHA256_TEST("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", test);
+ DEFINE_SHA384_TEST("768412320f7b0aa5812fce428dc4706b3cae50e02a64caa16a782249bfe8efc4b7ef1ccb126255d196047dfedf17a0a9", test);
+ DEFINE_SHA512_TEST("ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff", test);
+
+ DEFINE_HEX_PTR(h1, "e9ff2b6dfbc03b8dd0471a0f23840334e3ef51c64a325945524563c0375284a092751eca8d084fae22f74a104559a0ee8339d1845538481e674e6d31d4f63089");
+ DEFINE_HEX_PTR(h2, "5b6e809933a1b8d5a4a6bb62e20b36ae82d9408141e7479d0aa067273bd2d04007fb1977bad549d54330a49ed98f82b495ba");
+ DEFINE_HEX_PTR(h3, "d2aeef94d7ba2a");
+ DEFINE_HEX_PTR(h4, "1557db45ded3e38c79b5bb25c83ade42fa7d13047ef1b9a0b21a3c2ab2d4eee5c75e2927ce643163addbda65331035850a436c0acffc723f419e1d1cbf04c9064e6d850580c0732a12600f9feb");
+
+ const struct iovec i1 = IOVEC_MAKE(h1, h1_len);
+ const struct iovec i2 = IOVEC_MAKE(h2, h2_len);
+ const struct iovec i3 = IOVEC_MAKE(h3, h3_len);
+ const struct iovec i4 = IOVEC_MAKE(h4, h4_len);
+
+ DEFINE_SHA1_TEST("8e7c659a6331508b06adf98b430759dafb92fc43", i1, i2, i3, i4);
+ DEFINE_SHA256_TEST("4d6be38798786a5500651c1a02d96aa010e9d7b2bece1695294cd396d456cde8", i1, i2, i3, i4);
+ DEFINE_SHA384_TEST("82e6ec14f8d90f1ae1fd4fb7f415ea6fdb674515b13092e3e548a8d37a8faed30cda8ea613ec2a015a51bc578dacc995", i1, i2, i3, i4);
+ DEFINE_SHA512_TEST("21fe5beb15927257a9143ff59010e51d4c65c7c5237b0cd9a8db3c3fabe429be3a0759f9ace3cdd70f6ea543f998bec9bc3308833d70aa1bd380364de872a62c", i1, i2, i3, i4);
+
+ DEFINE_SHA256_TEST("0e0ed67d6717dc08dd6f472f6c35107a92b8c2695dcba344b884436f97a9eb4d", i1, i1, i1, i4);
+
+ DEFINE_SHA256_TEST("8fe8b8d1899c44bfb82e1edc4ff92642db5b2cb25c4210ea06c3846c757525a8", i1, i1, i1, i4, i4, i4, i4, i3, i3, i2);
+}
+
+static void verify_hmac(
+ const char *digest_alg,
+ const char *key,
+ const struct iovec *data,
+ size_t n_data,
+ const char *expect) {
+
+ DEFINE_HEX_PTR(k, key);
+ DEFINE_HEX_PTR(e, expect);
+ _cleanup_free_ void *digest = NULL;
+ size_t digest_size;
+
+ if (n_data == 0) {
+ assert_se(openssl_hmac(digest_alg, k, k_len, NULL, 0, &digest, &digest_size) == 0);
+ assert_se(memcmp_nn(e, e_len, digest, digest_size) == 0);
+ digest = mfree(digest);
+ } else if(n_data == 1) {
+ assert_se(openssl_hmac(digest_alg, k, k_len, data[0].iov_base, data[0].iov_len, &digest, &digest_size) == 0);
+ assert_se(memcmp_nn(e, e_len, digest, digest_size) == 0);
+ digest = mfree(digest);
+ }
+
+ assert_se(openssl_hmac_many(digest_alg, k, k_len, data, n_data, &digest, &digest_size) == 0);
+ assert_se(memcmp_nn(e, e_len, digest, digest_size) == 0);
+}
+
+#define _DEFINE_HMAC_TEST(uniq, alg, key, expect, ...) \
+ const struct iovec UNIQ_T(i, uniq)[] = { __VA_ARGS__ }; \
+ verify_hmac(alg, \
+ key, \
+ UNIQ_T(i, uniq), \
+ ELEMENTSOF(UNIQ_T(i, uniq)), \
+ expect);
+#define DEFINE_HMAC_TEST(alg, key, expect, ...) _DEFINE_HMAC_TEST(UNIQ, alg, key, expect, __VA_ARGS__)
+#define DEFINE_HMAC_SHA1_TEST(key, expect, ...) DEFINE_HMAC_TEST("SHA1", key, expect, __VA_ARGS__)
+#define DEFINE_HMAC_SHA256_TEST(key, expect, ...) DEFINE_HMAC_TEST("SHA256", key, expect, __VA_ARGS__)
+#define DEFINE_HMAC_SHA384_TEST(key, expect, ...) DEFINE_HMAC_TEST("SHA384", key, expect, __VA_ARGS__)
+#define DEFINE_HMAC_SHA512_TEST(key, expect, ...) DEFINE_HMAC_TEST("SHA512", key, expect, __VA_ARGS__)
+
+TEST(hmac_many) {
+ const char *key1 = "760eb6845073862c1914c6d188bf8214",
+ *key2 = "0628d1a5f83fce99779e12e2336d87046d42d74b755f00d9f72350668860fd00",
+ *key3 = "b61158912b76348c54f104629924be4178b8a9c9459c3a6e9daa1885445a61fccc1aa0f749c31f3ade4e227f64dd0e86a94b25c2e181f044af22d0a8c07074c3";
+ const struct iovec test = IOVEC_MAKE_STRING("test");
+
+ /* Empty digests */
+ DEFINE_HMAC_SHA1_TEST(key1, "EB9725FC9A99A652C3171E0863984AC42461F88B");
+ DEFINE_HMAC_SHA256_TEST(key1, "82A15D4DD5F583CF8F06D3E447DF0FDFF95A24E29229934B48BD0A5B4E0ADC85");
+ DEFINE_HMAC_SHA384_TEST(key1, "C60F15C4E18736750D91095ADA148C4179825A487CCA3AE047A2FB94F85A5587AB6AF57678AA79715FEF848129C108C3");
+ DEFINE_HMAC_SHA512_TEST(key1, "2B10DC9BFC0349400F8965482EA149C1C51C865BB7B16097623F41C14CF6C8A678724BFAE0CE842EED899C12CC17B5D8C4287F72BE788532FE7CF0BE2EBCD447");
+
+ DEFINE_HMAC_SHA1_TEST(key2, "F9AA74F129681E91807EB264EA6E1B5C5F9B4CFD");
+ DEFINE_HMAC_SHA256_TEST(key2, "B4ADEBF8B3044A5B0668B742C0A49B61D8380F89938C84794C92567F5A33CC7D");
+ DEFINE_HMAC_SHA384_TEST(key2, "E5EACAB7A13CF5BE60FA228D771E183CD6E57536BB9EAFC34A6BB52B1B1324BD6FB8A1713F91EC040790AE97F5672D53");
+ DEFINE_HMAC_SHA512_TEST(key2, "75A597D83A6270FC3204DE741E76DEFCF42D3E1812C71E41EEA8C0F23C07315822E83BE8B54705CB00FEF4CE1BAF80E3975414925C83BF3719CEBC27DD133F7D");
+
+ DEFINE_HMAC_SHA1_TEST(key3, "4B8EACB3C3935ACC8C58995C89F16020FC993569");
+ DEFINE_HMAC_SHA256_TEST(key3, "520E8C0323A1994D58EF5456611BCB6CD701399B24F8FBA0B5A3CD3186780E8E");
+ DEFINE_HMAC_SHA384_TEST(key3, "52ADAF691EFDC377B7349EAA45EE1BFAFA27CAC1FFE08B942C80426D1CA9F3464E3A71D611DA0B415435E82D6EE9F34A");
+ DEFINE_HMAC_SHA512_TEST(key3, "22D8C17BAF591E07CD2BD58A1B3D76D5904EC45C9099F0171A243F07611E25208A395833BC3F9BBD425636FD8D574BE1A1A367DCB6C40AD3C06E2B57E8FD2729");
+
+ /* test message */
+ DEFINE_HMAC_SHA1_TEST(key2, "DEE6313BE6391523D0B2B326890F13A65F3965B2", test);
+ DEFINE_HMAC_SHA256_TEST(key2, "496FF3E9DA52B2B490CD5EAE23457F8A33E61AB7B42F6E6374B7629CFBE1FCED", test);
+ DEFINE_HMAC_SHA384_TEST(key2, "F5223F750D671453CA6159C1354242DB13E0189CB79AC73E4964F623181B00C811A596F7CE3408DDE06B96C6D792F41E", test);
+ DEFINE_HMAC_SHA512_TEST(key2, "8755A8B0D85D89AFFE7A15702BBA0F835CDE454334EC952ED777A30035D6BD9407EA5DF8DCB89814C1DF7EE215022EA68D9D2BC4E4B299CD6F55CD60C269A706", test);
+
+ DEFINE_HEX_PTR(h1, "e9ff2b6dfbc03b8dd0471a0f23840334e3ef51c64a325945524563c0375284a092751eca8d084fae22f74a104559a0ee8339d1845538481e674e6d31d4f63089");
+ DEFINE_HEX_PTR(h2, "5b6e809933a1b8d5a4a6bb62e20b36ae82d9408141e7479d0aa067273bd2d04007fb1977bad549d54330a49ed98f82b495ba");
+ DEFINE_HEX_PTR(h3, "d2aeef94d7ba2a");
+ DEFINE_HEX_PTR(h4, "1557db45ded3e38c79b5bb25c83ade42fa7d13047ef1b9a0b21a3c2ab2d4eee5c75e2927ce643163addbda65331035850a436c0acffc723f419e1d1cbf04c9064e6d850580c0732a12600f9feb");
+
+ const struct iovec i1 = IOVEC_MAKE(h1, h1_len);
+ const struct iovec i2 = IOVEC_MAKE(h2, h2_len);
+ const struct iovec i3 = IOVEC_MAKE(h3, h3_len);
+ const struct iovec i4 = IOVEC_MAKE(h4, h4_len);
+
+ DEFINE_HMAC_SHA1_TEST(key2, "28C041532012BFF1B7C87B2A15A8C43EB8037D27", i1, i2, i3, i4);
+ DEFINE_HMAC_SHA256_TEST(key2, "F8A1FBDEE3CD383EA2B4940A3C8E72F443DB5B247016C9F84E2D2FEF3C5A0A23", i1, i2, i3, i4);
+ DEFINE_HMAC_SHA384_TEST(key2, "4D2AB0516F1F5C73BD0761407E0AF42361C1CAE761685FC65D1199598315EE3DCA4DB88E4D96FB06C2DA215A33FA9CE9", i1, i2, i3, i4);
+ DEFINE_HMAC_SHA512_TEST(key2, "E9BF8FC6FDE75FD5E4EF2DF399EE675C57B60C59A7B331F30535FDE68D8072185552E9A8BFA2008C52437F1BCC1472D16FBCF2A77C37339752938E42D2642150", i1, i2, i3, i4);
+
+ DEFINE_HMAC_SHA256_TEST(key3, "94D4E4B55368A533F6A7FDCC3B93E1F283BB1CA387BB5D14FAFF44A009EDF040", i1, i1, i1, i4);
+
+ DEFINE_HMAC_SHA256_TEST(key3, "5BE1F4D9C2AFAA2BB3F58FCE967BC7D3084BB8F512659875BDA634991145B0F0", i1, i1, i1, i4, i4, i4, i4, i3, i3, i2);
+}
+
+TEST(kdf_kb_hmac_derive) {
+#if OPENSSL_VERSION_MAJOR >= 3
+ _cleanup_free_ void *derived_key = NULL;
+
+ DEFINE_HEX_PTR(key, "d7ac57124f28371eacaec475b74869d26b4cd64586412a607ce0a9e0c63d468c");
+ const char *salt = "salty chocolate";
+ DEFINE_HEX_PTR(info, "6721a2012d9554f5a64593ed3eaa8fe15e6a21e1c8c8736ea4d234eb55b9e31a");
+ DEFINE_HEX_PTR(expected_derived_key, "A9DA9CEEB9578DBE7DD2862F82898B086E85FF2D10C4E8EC5BD99D0D7F003A2DE1574EB4BD789C03EF5235259BCB3A009DA303EA4DB4CA6BF507DB7C5A063279");
+
+ assert_se(kdf_kb_hmac_derive("COUNTER", "SHA256", key, key_len, salt, strlen(salt), info, info_len, /* seed= */ NULL, /* seed_size= */ 0, 64, &derived_key) >= 0);
+ assert_se(memcmp_nn(derived_key, 64, expected_derived_key, expected_derived_key_len) == 0);
+#else
+ log_tests_skipped("KDF-KB requires OpenSSL >= 3");
+#endif
+}
+
+#if OPENSSL_VERSION_MAJOR >= 3
+static void check_ss_derive(const char *hex_key, const char *hex_salt, const char *hex_info, const char *hex_expected) {
+ DEFINE_HEX_PTR(key, hex_key);
+ DEFINE_HEX_PTR(salt, hex_salt);
+ DEFINE_HEX_PTR(info, hex_info);
+ DEFINE_HEX_PTR(expected, hex_expected);
+
+ _cleanup_free_ void *derived_key = NULL;
+ assert_se(kdf_ss_derive("SHA256", key, key_len, salt, salt_len, info, info_len, expected_len, &derived_key) >= 0);
+ assert_se(memcmp_nn(derived_key, expected_len, expected, expected_len) == 0);
+}
+#endif
+
+TEST(kdf_ss_derive) {
+#if OPENSSL_VERSION_MAJOR >= 3
+ check_ss_derive(
+ "01166ad6b05d1fad8cdb50d1902170e9",
+ "feea805789dc8d0b57da5d4d61886b1a",
+ "af4cb6d1d0a996e21e3788584165e2ae",
+ "46CECAB4544E11EF986641BA6F843FAFFD111D3974C34E3B9592311E8579C6BD");
+
+ check_ss_derive(
+ "d1c39e37260d79d6e766f1d1412c4b61fc0801db469b97c897b0fbcaebea5178",
+ "b75e3b65d1bb845dee581c7e14cfebc6e882946e90273b77ebe289faaf7de248",
+ "ed25a0043d6c1eb28296da1f9ab138dafee18f4c937bfc43601d4ee6e7634199",
+ "30EB1A1E9DEA7DE4DDB8F3FDF50A01E3");
+ /* Same inputs as above, but derive more bytes */
+ check_ss_derive(
+ "d1c39e37260d79d6e766f1d1412c4b61fc0801db469b97c897b0fbcaebea5178",
+ "b75e3b65d1bb845dee581c7e14cfebc6e882946e90273b77ebe289faaf7de248",
+ "ed25a0043d6c1eb28296da1f9ab138dafee18f4c937bfc43601d4ee6e7634199",
+ "30EB1A1E9DEA7DE4DDB8F3FDF50A01E30581D606C1228D98AFF691DF743AC2EE9D99EFD2AE1946C079AA18C9524877FA65D5065F0DAED058AB3416AF80EB2B73");
+#else
+ log_tests_skipped("KDF-SS requires OpenSSL >= 3");
+#endif
+}
+
+static void check_cipher(
+ const char *alg,
+ size_t bits,
+ const char *mode,
+ const char *hex_key,
+ const char *hex_iv,
+ const struct iovec data[],
+ size_t n_data,
+ const char *hex_expected) {
+
+ _cleanup_free_ void *enc_buf = NULL;
+ size_t enc_buf_len;
+
+ DEFINE_HEX_PTR(key, hex_key);
+ DEFINE_HEX_PTR(iv, hex_iv);
+ DEFINE_HEX_PTR(expected, hex_expected);
+
+ if (n_data == 0) {
+ assert_se(openssl_cipher(alg, bits, mode, key, key_len, iv, iv_len, NULL, 0, &enc_buf, &enc_buf_len) >= 0);
+ assert_se(memcmp_nn(enc_buf, enc_buf_len, expected, expected_len) == 0);
+ enc_buf = mfree(enc_buf);
+ } else if (n_data == 1) {
+ assert_se(openssl_cipher(alg, bits, mode, key, key_len, iv, iv_len, data[0].iov_base, data[0].iov_len, &enc_buf, &enc_buf_len) >= 0);
+ assert_se(memcmp_nn(enc_buf, enc_buf_len, expected, expected_len) == 0);
+ enc_buf = mfree(enc_buf);
+ }
+
+ assert_se(openssl_cipher_many(alg, bits, mode, key, key_len, iv, iv_len, data, n_data, &enc_buf, &enc_buf_len) >= 0);
+ assert_se(memcmp_nn(enc_buf, enc_buf_len, expected, expected_len) == 0);
+}
+
+TEST(openssl_cipher) {
+ struct iovec data[] = {
+ IOVEC_MAKE_STRING("my"),
+ IOVEC_MAKE_STRING(" "),
+ IOVEC_MAKE_STRING("secret"),
+ IOVEC_MAKE_STRING(" "),
+ IOVEC_MAKE_STRING("text"),
+ IOVEC_MAKE_STRING("!"),
+ };
+
+ check_cipher(
+ "aes", 256, "cfb",
+ "32c62bbaeb0decc5c874b8e0148f86475b5bb10a36f7078a75a6f11704c2f06a",
+ /* hex_iv= */ NULL,
+ data, ELEMENTSOF(data),
+ "bd4a46f8762bf4bef4430514aaec5e");
+
+ check_cipher(
+ "aes", 256, "cfb",
+ "32c62bbaeb0decc5c874b8e0148f86475b5bb10a36f7078a75a6f11704c2f06a",
+ "00000000000000000000000000000000",
+ data, ELEMENTSOF(data),
+ "bd4a46f8762bf4bef4430514aaec5e");
+
+ check_cipher(
+ "aes", 256, "cfb",
+ "32c62bbaeb0decc5c874b8e0148f86475b5bb10a36f7078a75a6f11704c2f06a",
+ "9088fd5c4ad9b9419eced86283021a59",
+ data, ELEMENTSOF(data),
+ "6dfbf8dc972f9a462ad7427a1fa41a");
+
+ check_cipher(
+ "aes", 256, "cfb",
+ "32c62bbaeb0decc5c874b8e0148f86475b5bb10a36f7078a75a6f11704c2f06a",
+ /* hex_iv= */ NULL,
+ &data[2], 1,
+ "a35605f9763c");
+
+ check_cipher(
+ "aes", 256, "cfb",
+ "32c62bbaeb0decc5c874b8e0148f86475b5bb10a36f7078a75a6f11704c2f06a",
+ /* hex_iv= */ NULL,
+ /* data= */ NULL, /* n_data= */ 0,
+ /* expected= */ NULL);
+
+ check_cipher(
+ "aes", 128, "cfb",
+ "b8fe4b89f6f25dd58cadceb68c99d508",
+ /* hex_iv= */ NULL,
+ data, ELEMENTSOF(data),
+ "9c0fe3abb904ab419d950ae00c93a1");
+
+ check_cipher(
+ "aes", 128, "cfb",
+ "b8fe4b89f6f25dd58cadceb68c99d508",
+ "00000000000000000000000000000000",
+ data, ELEMENTSOF(data),
+ "9c0fe3abb904ab419d950ae00c93a1");
+
+ check_cipher(
+ "aes", 128, "cfb",
+ "b8fe4b89f6f25dd58cadceb68c99d508",
+ "9088fd5c4ad9b9419eced86283021a59",
+ data, ELEMENTSOF(data),
+ "e765617aceb1326f5309008c14f4e1");
+
+ check_cipher(
+ "aes", 128, "cfb",
+ "b8fe4b89f6f25dd58cadceb68c99d508",
+ /* hex_iv= */ NULL,
+ /* data= */ NULL, /* n_data= */ 0,
+ /* expected= */ NULL);
+
+ check_cipher(
+ "aes", 128, "cfb",
+ "b8fe4b89f6f25dd58cadceb68c99d508",
+ "00000000000000000000000000000000",
+ /* data= */ NULL, /* n_data= */ 0,
+ /* expected= */ NULL);
+}
+
+TEST(ecc_ecdh) {
+ _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkeyA = NULL, *pkeyB = NULL, *pkeyC = NULL;
+ _cleanup_free_ void *secretAB = NULL, *secretBA = NULL, *secretAC = NULL, *secretCA = NULL;
+ size_t secretAB_size, secretBA_size, secretAC_size, secretCA_size;
+
+ assert_se(ecc_pkey_new(NID_X9_62_prime256v1, &pkeyA) >= 0);
+ assert_se(ecc_pkey_new(NID_X9_62_prime256v1, &pkeyB) >= 0);
+ assert_se(ecc_pkey_new(NID_X9_62_prime256v1, &pkeyC) >= 0);
+
+ assert_se(ecc_ecdh(pkeyA, pkeyB, &secretAB, &secretAB_size) >= 0);
+ assert_se(ecc_ecdh(pkeyB, pkeyA, &secretBA, &secretBA_size) >= 0);
+ assert_se(ecc_ecdh(pkeyA, pkeyC, &secretAC, &secretAC_size) >= 0);
+ assert_se(ecc_ecdh(pkeyC, pkeyA, &secretCA, &secretCA_size) >= 0);
+
+ assert_se(memcmp_nn(secretAB, secretAB_size, secretBA, secretBA_size) == 0);
+ assert_se(memcmp_nn(secretAC, secretAC_size, secretCA, secretCA_size) == 0);
+ assert_se(memcmp_nn(secretAC, secretAC_size, secretAB, secretAB_size) != 0);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-ordered-set.c b/src/test/test-ordered-set.c
new file mode 100644
index 0000000..c055411
--- /dev/null
+++ b/src/test/test-ordered-set.c
@@ -0,0 +1,112 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+
+#include "ordered-set.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+
+TEST(set_steal_first) {
+ _cleanup_ordered_set_free_ OrderedSet *m = NULL;
+ int seen[3] = {};
+ char *val;
+
+ m = ordered_set_new(&string_hash_ops);
+ assert_se(m);
+
+ assert_se(ordered_set_put(m, (void*) "1") == 1);
+ assert_se(ordered_set_put(m, (void*) "22") == 1);
+ assert_se(ordered_set_put(m, (void*) "333") == 1);
+
+ ordered_set_print(stdout, "SET=", m);
+
+ while ((val = ordered_set_steal_first(m)))
+ seen[strlen(val) - 1]++;
+
+ assert_se(seen[0] == 1 && seen[1] == 1 && seen[2] == 1);
+
+ assert_se(ordered_set_isempty(m));
+
+ ordered_set_print(stdout, "SET=", m);
+}
+
+typedef struct Item {
+ int seen;
+} Item;
+static void item_seen(Item *item) {
+ item->seen++;
+}
+
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(item_hash_ops, void, trivial_hash_func, trivial_compare_func, Item, item_seen);
+
+TEST(set_free_with_hash_ops) {
+ OrderedSet *m;
+ struct Item items[4] = {};
+
+ assert_se(m = ordered_set_new(&item_hash_ops));
+
+ for (size_t i = 0; i < ELEMENTSOF(items) - 1; i++)
+ assert_se(ordered_set_put(m, items + i) == 1);
+
+ for (size_t i = 0; i < ELEMENTSOF(items) - 1; i++)
+ assert_se(ordered_set_put(m, items + i) == 0); /* We get 0 here, because we use trivial hash
+ * ops. Also see below... */
+
+ m = ordered_set_free(m);
+ assert_se(items[0].seen == 1);
+ assert_se(items[1].seen == 1);
+ assert_se(items[2].seen == 1);
+ assert_se(items[3].seen == 0);
+}
+
+TEST(set_put) {
+ _cleanup_ordered_set_free_ OrderedSet *m = NULL;
+ _cleanup_free_ char **t = NULL, *str = NULL;
+
+ m = ordered_set_new(&string_hash_ops);
+ assert_se(m);
+
+ assert_se(ordered_set_put(m, (void*) "1") == 1);
+ assert_se(ordered_set_put(m, (void*) "22") == 1);
+ assert_se(ordered_set_put(m, (void*) "333") == 1);
+ assert_se(ordered_set_put(m, (void*) "333") == 0);
+ assert_se(ordered_set_remove(m, (void*) "333"));
+ assert_se(ordered_set_put(m, (void*) "333") == 1);
+ assert_se(ordered_set_put(m, (void*) "333") == 0);
+ assert_se(ordered_set_put(m, (void*) "22") == 0);
+
+ assert_se(str = strdup("333"));
+ assert_se(ordered_set_put(m, str) == -EEXIST); /* ... and we get -EEXIST here, because we use
+ * non-trivial hash ops. */
+
+ assert_se(t = ordered_set_get_strv(m));
+ assert_se(streq(t[0], "1"));
+ assert_se(streq(t[1], "22"));
+ assert_se(streq(t[2], "333"));
+ assert_se(!t[3]);
+
+ ordered_set_print(stdout, "FOO=", m);
+}
+
+TEST(set_put_string_set) {
+ _cleanup_ordered_set_free_ OrderedSet *m = NULL, *q = NULL;
+ _cleanup_free_ char **final = NULL; /* "just free" because the strings are in the set */
+
+ assert_se(ordered_set_put_strdup(&m, "1") == 1);
+ assert_se(ordered_set_put_strdup(&m, "22") == 1);
+ assert_se(ordered_set_put_strdup(&m, "333") == 1);
+
+ assert_se(ordered_set_put_strdup(&q, "11") == 1);
+ assert_se(ordered_set_put_strdup(&q, "22") == 1);
+ assert_se(ordered_set_put_strdup(&q, "33") == 1);
+
+ assert_se(ordered_set_put_string_set(&m, q) == 2);
+
+ assert_se(final = ordered_set_get_strv(m));
+ assert_se(strv_equal(final, STRV_MAKE("1", "22", "333", "11", "33")));
+
+ ordered_set_print(stdout, "BAR=", m);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-os-util.c b/src/test/test-os-util.c
new file mode 100644
index 0000000..84e55e1
--- /dev/null
+++ b/src/test/test-os-util.c
@@ -0,0 +1,135 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include "fileio.h"
+#include "fs-util.h"
+#include "log.h"
+#include "mkdir.h"
+#include "os-util.h"
+#include "path-util.h"
+#include "rm-rf.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+TEST(path_is_os_tree) {
+ assert_se(path_is_os_tree("/") > 0);
+ assert_se(path_is_os_tree("/etc") == 0);
+ assert_se(path_is_os_tree("/idontexist") == -ENOENT);
+}
+
+TEST(parse_os_release) {
+ /* Let's assume that we're running in a valid system, so os-release is available */
+ _cleanup_free_ char *id = NULL, *id2 = NULL, *name = NULL, *foobar = NULL;
+ assert_se(parse_os_release(NULL, "ID", &id) == 0);
+ log_info("ID: %s", id);
+
+ assert_se(setenv("SYSTEMD_OS_RELEASE", "/dev/null", 1) == 0);
+ assert_se(parse_os_release(NULL, "ID", &id2) == 0);
+ log_info("ID: %s", strnull(id2));
+
+ _cleanup_(unlink_tempfilep) char tmpfile[] = "/tmp/test-os-util.XXXXXX";
+ assert_se(write_tmpfile(tmpfile,
+ "ID=the-id \n"
+ "NAME=the-name") == 0);
+
+ assert_se(setenv("SYSTEMD_OS_RELEASE", tmpfile, 1) == 0);
+ assert_se(parse_os_release(NULL, "ID", &id, "NAME", &name) == 0);
+ log_info("ID: %s NAME: %s", id, name);
+ assert_se(streq(id, "the-id"));
+ assert_se(streq(name, "the-name"));
+
+ _cleanup_(unlink_tempfilep) char tmpfile2[] = "/tmp/test-os-util.XXXXXX";
+ assert_se(write_tmpfile(tmpfile2,
+ "ID=\"ignored\" \n"
+ "ID=\"the-id\" \n"
+ "NAME='the-name'") == 0);
+
+ assert_se(setenv("SYSTEMD_OS_RELEASE", tmpfile2, 1) == 0);
+ assert_se(parse_os_release(NULL, "ID", &id, "NAME", &name) == 0);
+ log_info("ID: %s NAME: %s", id, name);
+ assert_se(streq(id, "the-id"));
+ assert_se(streq(name, "the-name"));
+
+ assert_se(parse_os_release(NULL, "FOOBAR", &foobar) == 0);
+ log_info("FOOBAR: %s", strnull(foobar));
+ assert_se(foobar == NULL);
+
+ assert_se(unsetenv("SYSTEMD_OS_RELEASE") == 0);
+}
+
+TEST(parse_extension_release) {
+ /* Let's assume that we have a valid extension image */
+ _cleanup_free_ char *id = NULL, *version_id = NULL, *foobar = NULL, *a = NULL, *b = NULL;
+ _cleanup_(rm_rf_physical_and_freep) char *tempdir = NULL;
+
+ int r = mkdtemp_malloc("/tmp/test-os-util.XXXXXX", &tempdir);
+ if (r < 0)
+ log_error_errno(r, "Failed to setup working directory: %m");
+
+ assert_se(a = path_join(tempdir, "/usr/lib/extension-release.d/extension-release.test"));
+ assert_se(mkdir_parents(a, 0777) >= 0);
+
+ r = write_string_file(a, "ID=the-id \n VERSION_ID=the-version-id", WRITE_STRING_FILE_CREATE);
+ if (r < 0)
+ log_error_errno(r, "Failed to write file: %m");
+
+ assert_se(parse_extension_release(tempdir, IMAGE_SYSEXT, "test", false, "ID", &id, "VERSION_ID", &version_id) == 0);
+ log_info("ID: %s VERSION_ID: %s", id, version_id);
+ assert_se(streq(id, "the-id"));
+ assert_se(streq(version_id, "the-version-id"));
+
+ assert_se(b = path_join(tempdir, "/etc/extension-release.d/extension-release.tester"));
+ assert_se(mkdir_parents(b, 0777) >= 0);
+
+ r = write_string_file(b, "ID=\"ignored\" \n ID=\"the-id\" \n VERSION_ID='the-version-id'", WRITE_STRING_FILE_CREATE);
+ if (r < 0)
+ log_error_errno(r, "Failed to write file: %m");
+
+ assert_se(parse_extension_release(tempdir, IMAGE_CONFEXT, "tester", false, "ID", &id, "VERSION_ID", &version_id) == 0);
+ log_info("ID: %s VERSION_ID: %s", id, version_id);
+ assert_se(streq(id, "the-id"));
+ assert_se(streq(version_id, "the-version-id"));
+
+ assert_se(parse_extension_release(tempdir, IMAGE_CONFEXT, "tester", false, "FOOBAR", &foobar) == 0);
+ log_info("FOOBAR: %s", strnull(foobar));
+ assert_se(foobar == NULL);
+
+ assert_se(parse_extension_release(tempdir, IMAGE_SYSEXT, "test", false, "FOOBAR", &foobar) == 0);
+ log_info("FOOBAR: %s", strnull(foobar));
+ assert_se(foobar == NULL);
+}
+
+TEST(load_os_release_pairs) {
+ _cleanup_(unlink_tempfilep) char tmpfile[] = "/tmp/test-os-util.XXXXXX";
+ assert_se(write_tmpfile(tmpfile,
+ "ID=\"ignored\" \n"
+ "ID=\"the-id\" \n"
+ "NAME='the-name'") == 0);
+
+ assert_se(setenv("SYSTEMD_OS_RELEASE", tmpfile, 1) == 0);
+
+ _cleanup_strv_free_ char **pairs = NULL;
+ assert_se(load_os_release_pairs(NULL, &pairs) == 0);
+ assert_se(strv_equal(pairs, STRV_MAKE("ID", "the-id",
+ "NAME", "the-name")));
+
+ assert_se(unsetenv("SYSTEMD_OS_RELEASE") == 0);
+}
+
+TEST(os_release_support_ended) {
+ int r;
+
+ assert_se(os_release_support_ended("1999-01-01", false, NULL) == true);
+ assert_se(os_release_support_ended("2037-12-31", false, NULL) == false);
+ assert_se(os_release_support_ended("-1-1-1", true, NULL) == -EINVAL);
+
+ r = os_release_support_ended(NULL, false, NULL);
+ if (r < 0)
+ log_info_errno(r, "Failed to check host: %m");
+ else
+ log_info_errno(r, "Host is supported: %s", yes_no(!r));
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-parse-argument.c b/src/test/test-parse-argument.c
new file mode 100644
index 0000000..cf3d542
--- /dev/null
+++ b/src/test/test-parse-argument.c
@@ -0,0 +1,53 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <signal.h>
+
+#include "parse-argument.h"
+#include "stdio-util.h"
+#include "tests.h"
+
+TEST(parse_json_argument) {
+ JsonFormatFlags flags = JSON_FORMAT_PRETTY;
+
+ assert_se(parse_json_argument("help", &flags) == 0);
+ assert_se(flags == JSON_FORMAT_PRETTY);
+
+ assert_se(parse_json_argument("off", &flags) == 1);
+ assert_se(flags == JSON_FORMAT_OFF);
+}
+
+TEST(parse_path_argument) {
+ _cleanup_free_ char *path = NULL;
+
+ assert_se(parse_path_argument("help", false, &path) == 0);
+ assert_se(streq(basename(path), "help"));
+
+ assert_se(parse_path_argument("/", false, &path) == 0);
+ assert_se(streq(path, "/"));
+
+ assert_se(parse_path_argument("/", true, &path) == 0);
+ assert_se(path == NULL);
+}
+
+TEST(parse_signal_argument) {
+ int signal = -1;
+
+ assert_se(parse_signal_argument("help", &signal) == 0);
+ assert_se(signal == -1);
+
+ assert_se(parse_signal_argument("list", &signal) == 0);
+ assert_se(signal == -1);
+
+ assert_se(parse_signal_argument("SIGABRT", &signal) == 1);
+ assert_se(signal == SIGABRT);
+
+ assert_se(parse_signal_argument("ABRT", &signal) == 1);
+ assert_se(signal == SIGABRT);
+
+ char buf[DECIMAL_STR_MAX(int)];
+ xsprintf(buf, "%d", SIGABRT);
+ assert_se(parse_signal_argument(buf, &signal) == 1);
+ assert_se(signal == SIGABRT);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-parse-helpers.c b/src/test/test-parse-helpers.c
new file mode 100644
index 0000000..052e251
--- /dev/null
+++ b/src/test/test-parse-helpers.c
@@ -0,0 +1,95 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <linux/in.h>
+#include <sys/socket.h>
+#include <stdio.h>
+
+#include "macro.h"
+#include "parse-helpers.h"
+#include "tests.h"
+
+static void test_valid_item(
+ const char *str,
+ int expected_af,
+ int expected_ip_protocol,
+ uint16_t expected_nr_ports,
+ uint16_t expected_port_min) {
+ uint16_t nr_ports, port_min;
+ int af, ip_protocol;
+
+ assert_se(parse_socket_bind_item(str, &af, &ip_protocol, &nr_ports, &port_min) >= 0);
+ assert_se(af == expected_af);
+ assert_se(ip_protocol == expected_ip_protocol);
+ assert_se(nr_ports == expected_nr_ports);
+ assert_se(port_min == expected_port_min);
+
+ log_info("%s: \"%s\" ok", __func__, str);
+}
+
+static void test_invalid_item(const char *str) {
+ uint16_t nr_ports, port_min;
+ int af, ip_protocol;
+
+ assert_se(parse_socket_bind_item(str, &af, &ip_protocol, &nr_ports, &port_min) == -EINVAL);
+
+ log_info("%s: \"%s\" ok", __func__, str);
+}
+
+TEST(valid_items) {
+ test_valid_item("any", AF_UNSPEC, 0, 0, 0);
+ test_valid_item("ipv4", AF_INET, 0, 0, 0);
+ test_valid_item("ipv6", AF_INET6, 0, 0, 0);
+ test_valid_item("ipv4:any", AF_INET, 0, 0, 0);
+ test_valid_item("ipv6:any", AF_INET6, 0, 0, 0);
+ test_valid_item("tcp", AF_UNSPEC, IPPROTO_TCP, 0, 0);
+ test_valid_item("udp", AF_UNSPEC, IPPROTO_UDP, 0, 0);
+ test_valid_item("tcp:any", AF_UNSPEC, IPPROTO_TCP, 0, 0);
+ test_valid_item("udp:any", AF_UNSPEC, IPPROTO_UDP, 0, 0);
+ test_valid_item("6666", AF_UNSPEC, 0, 1, 6666);
+ test_valid_item("6666-6667", AF_UNSPEC, 0, 2, 6666);
+ test_valid_item("65535", AF_UNSPEC, 0, 1, 65535);
+ test_valid_item("1-65535", AF_UNSPEC, 0, 65535, 1);
+ test_valid_item("ipv4:tcp", AF_INET, IPPROTO_TCP, 0, 0);
+ test_valid_item("ipv4:udp", AF_INET, IPPROTO_UDP, 0, 0);
+ test_valid_item("ipv6:tcp", AF_INET6, IPPROTO_TCP, 0, 0);
+ test_valid_item("ipv6:udp", AF_INET6, IPPROTO_UDP, 0, 0);
+ test_valid_item("ipv4:6666", AF_INET, 0, 1, 6666);
+ test_valid_item("ipv6:6666", AF_INET6, 0, 1, 6666);
+ test_valid_item("tcp:6666", AF_UNSPEC, IPPROTO_TCP, 1, 6666);
+ test_valid_item("udp:6666", AF_UNSPEC, IPPROTO_UDP, 1, 6666);
+ test_valid_item("ipv4:tcp:6666", AF_INET, IPPROTO_TCP, 1, 6666);
+ test_valid_item("ipv6:tcp:6666", AF_INET6, IPPROTO_TCP, 1, 6666);
+ test_valid_item("ipv6:udp:6666-6667", AF_INET6, IPPROTO_UDP, 2, 6666);
+ test_valid_item("ipv6:tcp:any", AF_INET6, IPPROTO_TCP, 0, 0);
+}
+
+TEST(invalid_items) {
+ test_invalid_item("");
+ test_invalid_item(":");
+ test_invalid_item("::");
+ test_invalid_item("any:");
+ test_invalid_item("meh");
+ test_invalid_item("zupa:meh");
+ test_invalid_item("zupa:meh:eh");
+ test_invalid_item("ip");
+ test_invalid_item("dccp");
+ test_invalid_item("ipv6meh");
+ test_invalid_item("ipv6::");
+ test_invalid_item("ipv6:ipv6");
+ test_invalid_item("ipv6:icmp");
+ test_invalid_item("ipv6:tcp:0");
+ test_invalid_item("65536");
+ test_invalid_item("0-65535");
+ test_invalid_item("ipv6:tcp:6666-6665");
+ test_invalid_item("ipv6:tcp:6666-100000");
+ test_invalid_item("ipv6::6666");
+ test_invalid_item("ipv6:tcp:any:");
+ test_invalid_item("ipv6:tcp:any:ipv6");
+ test_invalid_item("ipv6:tcp:6666:zupa");
+ test_invalid_item("ipv6:tcp:6666:any");
+ test_invalid_item("ipv6:tcp:6666 zupa");
+ test_invalid_item("ipv6:tcp:6666: zupa");
+ test_invalid_item("ipv6:tcp:6666\n zupa");
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-parse-util.c b/src/test/test-parse-util.c
new file mode 100644
index 0000000..58d22b6
--- /dev/null
+++ b/src/test/test-parse-util.c
@@ -0,0 +1,979 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <locale.h>
+#include <math.h>
+#include <sys/socket.h>
+
+#include "alloc-util.h"
+#include "errno-list.h"
+#include "log.h"
+#include "parse-util.h"
+#include "string-util.h"
+#include "tests.h"
+
+TEST(parse_boolean) {
+ assert_se(parse_boolean("1") == 1);
+ assert_se(parse_boolean("y") == 1);
+ assert_se(parse_boolean("Y") == 1);
+ assert_se(parse_boolean("yes") == 1);
+ assert_se(parse_boolean("YES") == 1);
+ assert_se(parse_boolean("true") == 1);
+ assert_se(parse_boolean("TRUE") == 1);
+ assert_se(parse_boolean("on") == 1);
+ assert_se(parse_boolean("ON") == 1);
+
+ assert_se(parse_boolean("0") == 0);
+ assert_se(parse_boolean("n") == 0);
+ assert_se(parse_boolean("N") == 0);
+ assert_se(parse_boolean("no") == 0);
+ assert_se(parse_boolean("NO") == 0);
+ assert_se(parse_boolean("false") == 0);
+ assert_se(parse_boolean("FALSE") == 0);
+ assert_se(parse_boolean("off") == 0);
+ assert_se(parse_boolean("OFF") == 0);
+
+ assert_se(parse_boolean("garbage") < 0);
+ assert_se(parse_boolean("") < 0);
+ assert_se(parse_boolean("full") < 0);
+}
+
+TEST(parse_pid) {
+ int r;
+ pid_t pid;
+
+ r = parse_pid("100", &pid);
+ assert_se(r == 0);
+ assert_se(pid == 100);
+
+ r = parse_pid("0x7FFFFFFF", &pid);
+ assert_se(r == 0);
+ assert_se(pid == 2147483647);
+
+ pid = 65; /* pid is left unchanged on ERANGE. Set to known arbitrary value. */
+ r = parse_pid("0", &pid);
+ assert_se(r == -ERANGE);
+ assert_se(pid == 65);
+
+ pid = 65; /* pid is left unchanged on ERANGE. Set to known arbitrary value. */
+ r = parse_pid("-100", &pid);
+ assert_se(r == -ERANGE);
+ assert_se(pid == 65);
+
+ pid = 65; /* pid is left unchanged on ERANGE. Set to known arbitrary value. */
+ r = parse_pid("0xFFFFFFFFFFFFFFFFF", &pid);
+ assert_se(r == -ERANGE);
+ assert_se(pid == 65);
+
+ r = parse_pid("junk", &pid);
+ assert_se(r == -EINVAL);
+
+ r = parse_pid("", &pid);
+ assert_se(r == -EINVAL);
+}
+
+TEST(parse_mode) {
+ mode_t m;
+
+ assert_se(parse_mode("-1", &m) < 0);
+ assert_se(parse_mode("+1", &m) < 0);
+ assert_se(parse_mode("", &m) < 0);
+ assert_se(parse_mode("888", &m) < 0);
+ assert_se(parse_mode("77777", &m) < 0);
+
+ assert_se(parse_mode("544", &m) >= 0 && m == 0544);
+ assert_se(parse_mode("0544", &m) >= 0 && m == 0544);
+ assert_se(parse_mode("00544", &m) >= 0 && m == 0544);
+ assert_se(parse_mode("777", &m) >= 0 && m == 0777);
+ assert_se(parse_mode("0777", &m) >= 0 && m == 0777);
+ assert_se(parse_mode("00777", &m) >= 0 && m == 0777);
+ assert_se(parse_mode("7777", &m) >= 0 && m == 07777);
+ assert_se(parse_mode("07777", &m) >= 0 && m == 07777);
+ assert_se(parse_mode("007777", &m) >= 0 && m == 07777);
+ assert_se(parse_mode("0", &m) >= 0 && m == 0);
+ assert_se(parse_mode(" 1", &m) >= 0 && m == 1);
+}
+
+TEST(parse_size_iec) {
+ uint64_t bytes;
+
+ assert_se(parse_size("", 1024, &bytes) == -EINVAL);
+
+ assert_se(parse_size("111", 1024, &bytes) == 0);
+ assert_se(bytes == 111);
+
+ assert_se(parse_size("111.4", 1024, &bytes) == 0);
+ assert_se(bytes == 111);
+
+ assert_se(parse_size(" 112 B", 1024, &bytes) == 0);
+ assert_se(bytes == 112);
+
+ assert_se(parse_size(" 112.6 B", 1024, &bytes) == 0);
+ assert_se(bytes == 112);
+
+ assert_se(parse_size("3.5 K", 1024, &bytes) == 0);
+ assert_se(bytes == 3*1024 + 512);
+
+ assert_se(parse_size("3. K", 1024, &bytes) == 0);
+ assert_se(bytes == 3*1024);
+
+ assert_se(parse_size("3.0 K", 1024, &bytes) == 0);
+ assert_se(bytes == 3*1024);
+
+ assert_se(parse_size("3. 0 K", 1024, &bytes) == -EINVAL);
+
+ assert_se(parse_size(" 4 M 11.5K", 1024, &bytes) == 0);
+ assert_se(bytes == 4*1024*1024 + 11 * 1024 + 512);
+
+ assert_se(parse_size("3B3.5G", 1024, &bytes) == -EINVAL);
+
+ assert_se(parse_size("3.5G3B", 1024, &bytes) == 0);
+ assert_se(bytes == 3ULL*1024*1024*1024 + 512*1024*1024 + 3);
+
+ assert_se(parse_size("3.5G 4B", 1024, &bytes) == 0);
+ assert_se(bytes == 3ULL*1024*1024*1024 + 512*1024*1024 + 4);
+
+ assert_se(parse_size("3B3G4T", 1024, &bytes) == -EINVAL);
+
+ assert_se(parse_size("4T3G3B", 1024, &bytes) == 0);
+ assert_se(bytes == (4ULL*1024 + 3)*1024*1024*1024 + 3);
+
+ assert_se(parse_size(" 4 T 3 G 3 B", 1024, &bytes) == 0);
+ assert_se(bytes == (4ULL*1024 + 3)*1024*1024*1024 + 3);
+
+ assert_se(parse_size("12P", 1024, &bytes) == 0);
+ assert_se(bytes == 12ULL * 1024*1024*1024*1024*1024);
+
+ assert_se(parse_size("12P12P", 1024, &bytes) == -EINVAL);
+
+ assert_se(parse_size("3E 2P", 1024, &bytes) == 0);
+ assert_se(bytes == (3 * 1024 + 2ULL) * 1024*1024*1024*1024*1024);
+
+ assert_se(parse_size("12X", 1024, &bytes) == -EINVAL);
+
+ assert_se(parse_size("12.5X", 1024, &bytes) == -EINVAL);
+
+ assert_se(parse_size("12.5e3", 1024, &bytes) == -EINVAL);
+
+ assert_se(parse_size("1024E", 1024, &bytes) == -ERANGE);
+ assert_se(parse_size("-1", 1024, &bytes) == -ERANGE);
+ assert_se(parse_size("-1024E", 1024, &bytes) == -ERANGE);
+
+ assert_se(parse_size("-1024P", 1024, &bytes) == -ERANGE);
+
+ assert_se(parse_size("-10B 20K", 1024, &bytes) == -ERANGE);
+}
+
+TEST(parse_size_si) {
+ uint64_t bytes;
+
+ assert_se(parse_size("", 1000, &bytes) == -EINVAL);
+
+ assert_se(parse_size("111", 1000, &bytes) == 0);
+ assert_se(bytes == 111);
+
+ assert_se(parse_size("111.4", 1000, &bytes) == 0);
+ assert_se(bytes == 111);
+
+ assert_se(parse_size(" 112 B", 1000, &bytes) == 0);
+ assert_se(bytes == 112);
+
+ assert_se(parse_size(" 112.6 B", 1000, &bytes) == 0);
+ assert_se(bytes == 112);
+
+ assert_se(parse_size("3.5 K", 1000, &bytes) == 0);
+ assert_se(bytes == 3*1000 + 500);
+
+ assert_se(parse_size("3. K", 1000, &bytes) == 0);
+ assert_se(bytes == 3*1000);
+
+ assert_se(parse_size("3.0 K", 1000, &bytes) == 0);
+ assert_se(bytes == 3*1000);
+
+ assert_se(parse_size("3. 0 K", 1000, &bytes) == -EINVAL);
+
+ assert_se(parse_size(" 4 M 11.5K", 1000, &bytes) == 0);
+ assert_se(bytes == 4*1000*1000 + 11 * 1000 + 500);
+
+ assert_se(parse_size("3B3.5G", 1000, &bytes) == -EINVAL);
+
+ assert_se(parse_size("3.5G3B", 1000, &bytes) == 0);
+ assert_se(bytes == 3ULL*1000*1000*1000 + 500*1000*1000 + 3);
+
+ assert_se(parse_size("3.5G 4B", 1000, &bytes) == 0);
+ assert_se(bytes == 3ULL*1000*1000*1000 + 500*1000*1000 + 4);
+
+ assert_se(parse_size("3B3G4T", 1000, &bytes) == -EINVAL);
+
+ assert_se(parse_size("4T3G3B", 1000, &bytes) == 0);
+ assert_se(bytes == (4ULL*1000 + 3)*1000*1000*1000 + 3);
+
+ assert_se(parse_size(" 4 T 3 G 3 B", 1000, &bytes) == 0);
+ assert_se(bytes == (4ULL*1000 + 3)*1000*1000*1000 + 3);
+
+ assert_se(parse_size("12P", 1000, &bytes) == 0);
+ assert_se(bytes == 12ULL * 1000*1000*1000*1000*1000);
+
+ assert_se(parse_size("12P12P", 1000, &bytes) == -EINVAL);
+
+ assert_se(parse_size("3E 2P", 1000, &bytes) == 0);
+ assert_se(bytes == (3 * 1000 + 2ULL) * 1000*1000*1000*1000*1000);
+
+ assert_se(parse_size("12X", 1000, &bytes) == -EINVAL);
+
+ assert_se(parse_size("12.5X", 1000, &bytes) == -EINVAL);
+
+ assert_se(parse_size("12.5e3", 1000, &bytes) == -EINVAL);
+
+ assert_se(parse_size("1000E", 1000, &bytes) == -ERANGE);
+ assert_se(parse_size("-1", 1000, &bytes) == -ERANGE);
+ assert_se(parse_size("-1000E", 1000, &bytes) == -ERANGE);
+
+ assert_se(parse_size("-1000P", 1000, &bytes) == -ERANGE);
+
+ assert_se(parse_size("-10B 20K", 1000, &bytes) == -ERANGE);
+}
+
+TEST(parse_range) {
+ unsigned lower, upper;
+
+ /* Successful cases */
+ assert_se(parse_range("111", &lower, &upper) == 0);
+ assert_se(lower == 111);
+ assert_se(upper == 111);
+
+ assert_se(parse_range("111-123", &lower, &upper) == 0);
+ assert_se(lower == 111);
+ assert_se(upper == 123);
+
+ assert_se(parse_range("123-111", &lower, &upper) == 0);
+ assert_se(lower == 123);
+ assert_se(upper == 111);
+
+ assert_se(parse_range("123-123", &lower, &upper) == 0);
+ assert_se(lower == 123);
+ assert_se(upper == 123);
+
+ assert_se(parse_range("0", &lower, &upper) == 0);
+ assert_se(lower == 0);
+ assert_se(upper == 0);
+
+ assert_se(parse_range("0-15", &lower, &upper) == 0);
+ assert_se(lower == 0);
+ assert_se(upper == 15);
+
+ assert_se(parse_range("15-0", &lower, &upper) == 0);
+ assert_se(lower == 15);
+ assert_se(upper == 0);
+
+ assert_se(parse_range("128-65535", &lower, &upper) == 0);
+ assert_se(lower == 128);
+ assert_se(upper == 65535);
+
+ assert_se(parse_range("1024-4294967295", &lower, &upper) == 0);
+ assert_se(lower == 1024);
+ assert_se(upper == 4294967295);
+
+ /* Leading whitespace is acceptable */
+ assert_se(parse_range(" 111", &lower, &upper) == 0);
+ assert_se(lower == 111);
+ assert_se(upper == 111);
+
+ assert_se(parse_range(" 111-123", &lower, &upper) == 0);
+ assert_se(lower == 111);
+ assert_se(upper == 123);
+
+ assert_se(parse_range("111- 123", &lower, &upper) == 0);
+ assert_se(lower == 111);
+ assert_se(upper == 123);
+
+ assert_se(parse_range("\t111-\t123", &lower, &upper) == 0);
+ assert_se(lower == 111);
+ assert_se(upper == 123);
+
+ assert_se(parse_range(" \t 111- \t 123", &lower, &upper) == 0);
+ assert_se(lower == 111);
+ assert_se(upper == 123);
+
+ /* Error cases, make sure they fail as expected */
+ lower = upper = 9999;
+ assert_se(parse_range("111garbage", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("garbage111", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("garbage", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("111-123garbage", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("111garbage-123", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ /* Empty string */
+ lower = upper = 9999;
+ assert_se(parse_range("", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ /* 111--123 will pass -123 to safe_atou which returns -ERANGE for negative */
+ assert_se(parse_range("111--123", &lower, &upper) == -ERANGE);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("-123", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("-111-123", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("111-123-", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("111.4-123", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("111-123.4", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("111,4-123", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("111-123,4", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ /* Error on trailing dash */
+ assert_se(parse_range("111-", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("111-123-", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("111--", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("111- ", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ /* Whitespace is not a separator */
+ assert_se(parse_range("111 123", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("111\t123", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("111 \t 123", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ /* Trailing whitespace is invalid (from safe_atou) */
+ assert_se(parse_range("111 ", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("111-123 ", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("111 -123", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("111 -123 ", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("111\t-123\t", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ assert_se(parse_range("111 \t -123 \t ", &lower, &upper) == -EINVAL);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+
+ /* Out of the "unsigned" range, this is 1<<64 */
+ assert_se(parse_range("0-18446744073709551616", &lower, &upper) == -ERANGE);
+ assert_se(lower == 9999);
+ assert_se(upper == 9999);
+}
+
+TEST(safe_atou_bounded) {
+ int r;
+ unsigned x;
+
+ r = safe_atou_bounded("12345", 12, 20000, &x);
+ assert_se(r == 0);
+ assert_se(x == 12345);
+
+ r = safe_atou_bounded("12", 12, 20000, &x);
+ assert_se(r == 0);
+ assert_se(x == 12);
+
+ r = safe_atou_bounded("20000", 12, 20000, &x);
+ assert_se(r == 0);
+ assert_se(x == 20000);
+
+ r = safe_atou_bounded("-1", 12, 20000, &x);
+ assert_se(r == -ERANGE);
+
+ r = safe_atou_bounded("11", 12, 20000, &x);
+ assert_se(r == -ERANGE);
+
+ r = safe_atou_bounded("20001", 12, 20000, &x);
+ assert_se(r == -ERANGE);
+}
+
+TEST(safe_atolli) {
+ int r;
+ long long l;
+
+ r = safe_atolli("12345", &l);
+ assert_se(r == 0);
+ assert_se(l == 12345);
+
+ r = safe_atolli(" 12345", &l);
+ assert_se(r == 0);
+ assert_se(l == 12345);
+
+ r = safe_atolli("-12345", &l);
+ assert_se(r == 0);
+ assert_se(l == -12345);
+
+ r = safe_atolli(" -12345", &l);
+ assert_se(r == 0);
+ assert_se(l == -12345);
+
+ r = safe_atolli("0x5", &l);
+ assert_se(r == 0);
+ assert_se(l == 5);
+
+ r = safe_atolli("0o6", &l);
+ assert_se(r == 0);
+ assert_se(l == 6);
+
+ r = safe_atolli("0B101", &l);
+ assert_se(r == 0);
+ assert_se(l == 5);
+
+ r = safe_atolli("12345678901234567890", &l);
+ assert_se(r == -ERANGE);
+
+ r = safe_atolli("-12345678901234567890", &l);
+ assert_se(r == -ERANGE);
+
+ r = safe_atolli("junk", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atolli("123x", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atolli("12.3", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atolli("", &l);
+ assert_se(r == -EINVAL);
+}
+
+TEST(safe_atou16) {
+ int r;
+ uint16_t l;
+
+ r = safe_atou16("12345", &l);
+ assert_se(r == 0);
+ assert_se(l == 12345);
+
+ r = safe_atou16(" 12345", &l);
+ assert_se(r == 0);
+ assert_se(l == 12345);
+
+ r = safe_atou16("+12345", &l);
+ assert_se(r == 0);
+ assert_se(l == 12345);
+
+ r = safe_atou16(" +12345", &l);
+ assert_se(r == 0);
+ assert_se(l == 12345);
+
+ r = safe_atou16("123456", &l);
+ assert_se(r == -ERANGE);
+
+ r = safe_atou16("-1", &l);
+ assert_se(r == -ERANGE);
+
+ r = safe_atou16(" -1", &l);
+ assert_se(r == -ERANGE);
+
+ r = safe_atou16("junk", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atou16("123x", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atou16("12.3", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atou16("", &l);
+ assert_se(r == -EINVAL);
+}
+
+TEST(safe_atoi16) {
+ int r;
+ int16_t l;
+
+ r = safe_atoi16("-12345", &l);
+ assert_se(r == 0);
+ assert_se(l == -12345);
+
+ r = safe_atoi16(" -12345", &l);
+ assert_se(r == 0);
+ assert_se(l == -12345);
+
+ r = safe_atoi16("+12345", &l);
+ assert_se(r == 0);
+ assert_se(l == 12345);
+
+ r = safe_atoi16(" +12345", &l);
+ assert_se(r == 0);
+ assert_se(l == 12345);
+
+ r = safe_atoi16("32767", &l);
+ assert_se(r == 0);
+ assert_se(l == 32767);
+
+ r = safe_atoi16(" 32767", &l);
+ assert_se(r == 0);
+ assert_se(l == 32767);
+
+ r = safe_atoi16("0o11", &l);
+ assert_se(r == 0);
+ assert_se(l == 9);
+
+ r = safe_atoi16("0B110", &l);
+ assert_se(r == 0);
+ assert_se(l == 6);
+
+ r = safe_atoi16("36536", &l);
+ assert_se(r == -ERANGE);
+
+ r = safe_atoi16("-32769", &l);
+ assert_se(r == -ERANGE);
+
+ r = safe_atoi16("junk", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atoi16("123x", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atoi16("12.3", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atoi16("", &l);
+ assert_se(r == -EINVAL);
+}
+
+TEST(safe_atoux16) {
+ int r;
+ uint16_t l;
+
+ r = safe_atoux16("1234", &l);
+ assert_se(r == 0);
+ assert_se(l == 0x1234);
+
+ r = safe_atoux16("abcd", &l);
+ assert_se(r == 0);
+ assert_se(l == 0xabcd);
+
+ r = safe_atoux16(" 1234", &l);
+ assert_se(r == 0);
+ assert_se(l == 0x1234);
+
+ r = safe_atoux16("12345", &l);
+ assert_se(r == -ERANGE);
+
+ r = safe_atoux16("-1", &l);
+ assert_se(r == -ERANGE);
+
+ r = safe_atoux16(" -1", &l);
+ assert_se(r == -ERANGE);
+
+ r = safe_atoux16("0b1", &l);
+ assert_se(r == 0);
+ assert_se(l == 177);
+
+ r = safe_atoux16("0o70", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atoux16("junk", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atoux16("123x", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atoux16("12.3", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atoux16("", &l);
+ assert_se(r == -EINVAL);
+}
+
+TEST(safe_atou64) {
+ int r;
+ uint64_t l;
+
+ r = safe_atou64("12345", &l);
+ assert_se(r == 0);
+ assert_se(l == 12345);
+
+ r = safe_atou64(" 12345", &l);
+ assert_se(r == 0);
+ assert_se(l == 12345);
+
+ r = safe_atou64("0o11", &l);
+ assert_se(r == 0);
+ assert_se(l == 9);
+
+ r = safe_atou64("0b11", &l);
+ assert_se(r == 0);
+ assert_se(l == 3);
+
+ r = safe_atou64("18446744073709551617", &l);
+ assert_se(r == -ERANGE);
+
+ r = safe_atou64("-1", &l);
+ assert_se(r == -ERANGE);
+
+ r = safe_atou64(" -1", &l);
+ assert_se(r == -ERANGE);
+
+ r = safe_atou64("junk", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atou64("123x", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atou64("12.3", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atou64("", &l);
+ assert_se(r == -EINVAL);
+}
+
+TEST(safe_atoi64) {
+ int r;
+ int64_t l;
+
+ r = safe_atoi64("-12345", &l);
+ assert_se(r == 0);
+ assert_se(l == -12345);
+
+ r = safe_atoi64(" -12345", &l);
+ assert_se(r == 0);
+ assert_se(l == -12345);
+
+ r = safe_atoi64("32767", &l);
+ assert_se(r == 0);
+ assert_se(l == 32767);
+
+ r = safe_atoi64(" 32767", &l);
+ assert_se(r == 0);
+ assert_se(l == 32767);
+
+ r = safe_atoi64(" 0o20", &l);
+ assert_se(r == 0);
+ assert_se(l == 16);
+
+ r = safe_atoi64(" 0b01010", &l);
+ assert_se(r == 0);
+ assert_se(l == 10);
+
+ r = safe_atoi64("9223372036854775813", &l);
+ assert_se(r == -ERANGE);
+
+ r = safe_atoi64("-9223372036854775813", &l);
+ assert_se(r == -ERANGE);
+
+ r = safe_atoi64("junk", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atoi64("123x", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atoi64("12.3", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atoi64("", &l);
+ assert_se(r == -EINVAL);
+}
+
+TEST(safe_atoux64) {
+ int r;
+ uint64_t l;
+
+ r = safe_atoux64("12345", &l);
+ assert_se(r == 0);
+ assert_se(l == 0x12345);
+
+ r = safe_atoux64(" 12345", &l);
+ assert_se(r == 0);
+ assert_se(l == 0x12345);
+
+ r = safe_atoux64("0x12345", &l);
+ assert_se(r == 0);
+ assert_se(l == 0x12345);
+
+ r = safe_atoux64("0b11011", &l);
+ assert_se(r == 0);
+ assert_se(l == 11603985);
+
+ r = safe_atoux64("+12345", &l);
+ assert_se(r == 0);
+ assert_se(l == 0x12345);
+
+ r = safe_atoux64(" +12345", &l);
+ assert_se(r == 0);
+ assert_se(l == 0x12345);
+
+ r = safe_atoux64("+0x12345", &l);
+ assert_se(r == 0);
+ assert_se(l == 0x12345);
+
+ r = safe_atoux64("+0b11011", &l);
+ assert_se(r == 0);
+ assert_se(l == 11603985);
+
+ r = safe_atoux64("0o11011", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atoux64("18446744073709551617", &l);
+ assert_se(r == -ERANGE);
+
+ r = safe_atoux64("-1", &l);
+ assert_se(r == -ERANGE);
+
+ r = safe_atoux64(" -1", &l);
+ assert_se(r == -ERANGE);
+
+ r = safe_atoux64("junk", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atoux64("123x", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atoux64("12.3", &l);
+ assert_se(r == -EINVAL);
+
+ r = safe_atoux64("", &l);
+ assert_se(r == -EINVAL);
+}
+
+TEST(safe_atod) {
+ int r;
+ double d;
+ char *e;
+
+ r = safe_atod("junk", &d);
+ assert_se(r == -EINVAL);
+
+ r = safe_atod("0.2244", &d);
+ assert_se(r == 0);
+ assert_se(fabs(d - 0.2244) < 0.000001);
+
+ r = safe_atod("0,5", &d);
+ assert_se(r == -EINVAL);
+
+ errno = 0;
+ strtod("0,5", &e);
+ assert_se(*e == ',');
+
+ r = safe_atod("", &d);
+ assert_se(r == -EINVAL);
+
+ /* Check if this really is locale independent */
+ if (setlocale(LC_NUMERIC, "de_DE.utf8")) {
+
+ r = safe_atod("0.2244", &d);
+ assert_se(r == 0);
+ assert_se(fabs(d - 0.2244) < 0.000001);
+
+ r = safe_atod("0,5", &d);
+ assert_se(r == -EINVAL);
+
+ errno = 0;
+ assert_se(fabs(strtod("0,5", &e) - 0.5) < 0.00001);
+
+ r = safe_atod("", &d);
+ assert_se(r == -EINVAL);
+ }
+
+ /* And check again, reset */
+ assert_se(setlocale(LC_NUMERIC, "C"));
+
+ r = safe_atod("0.2244", &d);
+ assert_se(r == 0);
+ assert_se(fabs(d - 0.2244) < 0.000001);
+
+ r = safe_atod("0,5", &d);
+ assert_se(r == -EINVAL);
+
+ errno = 0;
+ strtod("0,5", &e);
+ assert_se(*e == ',');
+
+ r = safe_atod("", &d);
+ assert_se(r == -EINVAL);
+}
+
+TEST(parse_nice) {
+ int n;
+
+ assert_se(parse_nice("0", &n) >= 0 && n == 0);
+ assert_se(parse_nice("+0", &n) >= 0 && n == 0);
+ assert_se(parse_nice("-1", &n) >= 0 && n == -1);
+ assert_se(parse_nice("-2", &n) >= 0 && n == -2);
+ assert_se(parse_nice("1", &n) >= 0 && n == 1);
+ assert_se(parse_nice("2", &n) >= 0 && n == 2);
+ assert_se(parse_nice("+1", &n) >= 0 && n == 1);
+ assert_se(parse_nice("+2", &n) >= 0 && n == 2);
+ assert_se(parse_nice("-20", &n) >= 0 && n == -20);
+ assert_se(parse_nice("19", &n) >= 0 && n == 19);
+ assert_se(parse_nice("+19", &n) >= 0 && n == 19);
+
+ assert_se(parse_nice("", &n) == -EINVAL);
+ assert_se(parse_nice("-", &n) == -EINVAL);
+ assert_se(parse_nice("+", &n) == -EINVAL);
+ assert_se(parse_nice("xx", &n) == -EINVAL);
+ assert_se(parse_nice("-50", &n) == -ERANGE);
+ assert_se(parse_nice("50", &n) == -ERANGE);
+ assert_se(parse_nice("+50", &n) == -ERANGE);
+ assert_se(parse_nice("-21", &n) == -ERANGE);
+ assert_se(parse_nice("20", &n) == -ERANGE);
+ assert_se(parse_nice("+20", &n) == -ERANGE);
+}
+
+TEST(parse_errno) {
+ assert_se(parse_errno("EILSEQ") == EILSEQ);
+ assert_se(parse_errno("EINVAL") == EINVAL);
+ assert_se(parse_errno("0") == 0);
+ assert_se(parse_errno("1") == 1);
+ assert_se(parse_errno("4095") == 4095);
+
+ assert_se(parse_errno("-1") == -ERANGE);
+ assert_se(parse_errno("-3") == -ERANGE);
+ assert_se(parse_errno("4096") == -ERANGE);
+
+ assert_se(parse_errno("") == -EINVAL);
+ assert_se(parse_errno("12.3") == -EINVAL);
+ assert_se(parse_errno("123junk") == -EINVAL);
+ assert_se(parse_errno("junk123") == -EINVAL);
+ assert_se(parse_errno("255EILSEQ") == -EINVAL);
+ assert_se(parse_errno("EINVAL12") == -EINVAL);
+ assert_se(parse_errno("-EINVAL") == -EINVAL);
+ assert_se(parse_errno("EINVALaaa") == -EINVAL);
+}
+
+TEST(parse_fd) {
+ assert_se(parse_fd("0") == 0);
+ assert_se(parse_fd("1") == 1);
+
+ assert_se(parse_fd("-1") == -EBADF);
+ assert_se(parse_fd("-3") == -EBADF);
+
+ assert_se(parse_fd("") == -EINVAL);
+ assert_se(parse_fd("12.3") == -EINVAL);
+ assert_se(parse_fd("123junk") == -EINVAL);
+ assert_se(parse_fd("junk123") == -EINVAL);
+}
+
+TEST(parse_mtu) {
+ uint32_t mtu = 0;
+
+ assert_se(parse_mtu(AF_UNSPEC, "1500", &mtu) >= 0 && mtu == 1500);
+ assert_se(parse_mtu(AF_UNSPEC, "1400", &mtu) >= 0 && mtu == 1400);
+ assert_se(parse_mtu(AF_UNSPEC, "65535", &mtu) >= 0 && mtu == 65535);
+ assert_se(parse_mtu(AF_UNSPEC, "65536", &mtu) >= 0 && mtu == 65536);
+ assert_se(parse_mtu(AF_UNSPEC, "4294967295", &mtu) >= 0 && mtu == 4294967295);
+ assert_se(parse_mtu(AF_UNSPEC, "500", &mtu) >= 0 && mtu == 500);
+ assert_se(parse_mtu(AF_UNSPEC, "1280", &mtu) >= 0 && mtu == 1280);
+ assert_se(parse_mtu(AF_UNSPEC, "4294967296", &mtu) == -ERANGE);
+ assert_se(parse_mtu(AF_UNSPEC, "68", &mtu) >= 0 && mtu == 68);
+ assert_se(parse_mtu(AF_UNSPEC, "67", &mtu) >= 0 && mtu == 67);
+ assert_se(parse_mtu(AF_UNSPEC, "0", &mtu) >= 0 && mtu == 0);
+ assert_se(parse_mtu(AF_UNSPEC, "", &mtu) == -EINVAL);
+
+ assert_se(parse_mtu(AF_INET, "1500", &mtu) >= 0 && mtu == 1500);
+ assert_se(parse_mtu(AF_INET, "1400", &mtu) >= 0 && mtu == 1400);
+ assert_se(parse_mtu(AF_INET, "65535", &mtu) >= 0 && mtu == 65535);
+ assert_se(parse_mtu(AF_INET, "65536", &mtu) >= 0 && mtu == 65536);
+ assert_se(parse_mtu(AF_INET, "4294967295", &mtu) >= 0 && mtu == 4294967295);
+ assert_se(parse_mtu(AF_INET, "500", &mtu) >= 0 && mtu == 500);
+ assert_se(parse_mtu(AF_INET, "1280", &mtu) >= 0 && mtu == 1280);
+ assert_se(parse_mtu(AF_INET, "4294967296", &mtu) == -ERANGE);
+ assert_se(parse_mtu(AF_INET, "68", &mtu) >= 0 && mtu == 68);
+ assert_se(parse_mtu(AF_INET, "67", &mtu) == -ERANGE);
+ assert_se(parse_mtu(AF_INET, "0", &mtu) == -ERANGE);
+ assert_se(parse_mtu(AF_INET, "", &mtu) == -EINVAL);
+
+ assert_se(parse_mtu(AF_INET6, "1280", &mtu) >= 0 && mtu == 1280);
+ assert_se(parse_mtu(AF_INET6, "1279", &mtu) == -ERANGE);
+ assert_se(parse_mtu(AF_INET6, "4294967296", &mtu) == -ERANGE);
+ assert_se(parse_mtu(AF_INET6, "68", &mtu) == -ERANGE);
+ assert_se(parse_mtu(AF_INET6, "", &mtu) == -EINVAL);
+}
+
+TEST(parse_loadavg_fixed_point) {
+ loadavg_t fp;
+
+ assert_se(parse_loadavg_fixed_point("1.23", &fp) == 0);
+ assert_se(LOADAVG_INT_SIDE(fp) == 1);
+ assert_se(LOADAVG_DECIMAL_SIDE(fp) == 23);
+
+ assert_se(parse_loadavg_fixed_point("1.80", &fp) == 0);
+ assert_se(LOADAVG_INT_SIDE(fp) == 1);
+ assert_se(LOADAVG_DECIMAL_SIDE(fp) == 80);
+
+ assert_se(parse_loadavg_fixed_point("0.07", &fp) == 0);
+ assert_se(LOADAVG_INT_SIDE(fp) == 0);
+ assert_se(LOADAVG_DECIMAL_SIDE(fp) == 7);
+
+ assert_se(parse_loadavg_fixed_point("0.00", &fp) == 0);
+ assert_se(LOADAVG_INT_SIDE(fp) == 0);
+ assert_se(LOADAVG_DECIMAL_SIDE(fp) == 0);
+
+ assert_se(parse_loadavg_fixed_point("4096.57", &fp) == 0);
+ assert_se(LOADAVG_INT_SIDE(fp) == 4096);
+ assert_se(LOADAVG_DECIMAL_SIDE(fp) == 57);
+
+ /* Caps out at 2 digit fracs */
+ assert_se(parse_loadavg_fixed_point("1.100", &fp) == -ERANGE);
+
+ assert_se(parse_loadavg_fixed_point("4096.4096", &fp) == -ERANGE);
+ assert_se(parse_loadavg_fixed_point("-4000.5", &fp) == -ERANGE);
+ assert_se(parse_loadavg_fixed_point("18446744073709551615.5", &fp) == -ERANGE);
+ assert_se(parse_loadavg_fixed_point("foobar", &fp) == -EINVAL);
+ assert_se(parse_loadavg_fixed_point("3333", &fp) == -EINVAL);
+ assert_se(parse_loadavg_fixed_point("1.2.3", &fp) == -EINVAL);
+ assert_se(parse_loadavg_fixed_point(".", &fp) == -EINVAL);
+ assert_se(parse_loadavg_fixed_point("", &fp) == -EINVAL);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-path-lookup.c b/src/test/test-path-lookup.c
new file mode 100644
index 0000000..431a859
--- /dev/null
+++ b/src/test/test-path-lookup.c
@@ -0,0 +1,126 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdlib.h>
+#include <sys/stat.h>
+
+#include "log.h"
+#include "path-lookup.h"
+#include "rm-rf.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+static void test_paths_one(RuntimeScope scope) {
+ _cleanup_(rm_rf_physical_and_freep) char *tmp = NULL;
+ _cleanup_(lookup_paths_free) LookupPaths lp_without_env = {};
+ _cleanup_(lookup_paths_free) LookupPaths lp_with_env = {};
+ char *systemd_unit_path;
+
+ assert_se(mkdtemp_malloc("/tmp/test-path-lookup.XXXXXXX", &tmp) >= 0);
+
+ assert_se(unsetenv("SYSTEMD_UNIT_PATH") == 0);
+ assert_se(lookup_paths_init(&lp_without_env, scope, 0, NULL) >= 0);
+ assert_se(!strv_isempty(lp_without_env.search_path));
+ lookup_paths_log(&lp_without_env);
+
+ systemd_unit_path = strjoina(tmp, "/systemd-unit-path");
+ assert_se(setenv("SYSTEMD_UNIT_PATH", systemd_unit_path, 1) == 0);
+ assert_se(lookup_paths_init(&lp_with_env, scope, 0, NULL) == 0);
+ assert_se(strv_length(lp_with_env.search_path) == 1);
+ assert_se(streq(lp_with_env.search_path[0], systemd_unit_path));
+ lookup_paths_log(&lp_with_env);
+ assert_se(strv_equal(lp_with_env.search_path, STRV_MAKE(systemd_unit_path)));
+}
+
+TEST(paths) {
+ test_paths_one(RUNTIME_SCOPE_SYSTEM);
+ test_paths_one(RUNTIME_SCOPE_USER);
+ test_paths_one(RUNTIME_SCOPE_GLOBAL);
+}
+
+TEST(user_and_global_paths) {
+ _cleanup_(lookup_paths_free) LookupPaths lp_global = {}, lp_user = {};
+ char **u, **g;
+ unsigned k = 0;
+
+ assert_se(unsetenv("SYSTEMD_UNIT_PATH") == 0);
+ assert_se(unsetenv("XDG_DATA_DIRS") == 0);
+ assert_se(unsetenv("XDG_CONFIG_DIRS") == 0);
+
+ assert_se(lookup_paths_init(&lp_global, RUNTIME_SCOPE_GLOBAL, 0, NULL) == 0);
+ assert_se(lookup_paths_init(&lp_user, RUNTIME_SCOPE_USER, 0, NULL) == 0);
+ g = lp_global.search_path;
+ u = lp_user.search_path;
+
+ /* Go over all entries in global search path, and verify
+ * that they also exist in the user search path. Skip any
+ * entries in user search path which don't exist in the global
+ * one, but not vice versa. */
+ STRV_FOREACH(p, g) {
+ while (u[k] && !streq(*p, u[k])) {
+ log_info("+ %s", u[k]);
+ k++;
+ }
+ log_info(" %s", *p);
+ assert_se(u[k]); /* If NULL, we didn't find a matching entry */
+ k++;
+ }
+ STRV_FOREACH(p, u + k)
+ log_info("+ %s", *p);
+}
+
+static void test_generator_binary_paths_one(RuntimeScope scope) {
+ _cleanup_(rm_rf_physical_and_freep) char *tmp = NULL;
+ _cleanup_strv_free_ char **gp_without_env = NULL;
+ _cleanup_strv_free_ char **env_gp_without_env = NULL;
+ _cleanup_strv_free_ char **gp_with_env = NULL;
+ _cleanup_strv_free_ char **env_gp_with_env = NULL;
+ char *systemd_generator_path = NULL;
+ char *systemd_env_generator_path = NULL;
+
+ assert_se(mkdtemp_malloc("/tmp/test-path-lookup.XXXXXXX", &tmp) >= 0);
+
+ assert_se(unsetenv("SYSTEMD_GENERATOR_PATH") == 0);
+ assert_se(unsetenv("SYSTEMD_ENVIRONMENT_GENERATOR_PATH") == 0);
+
+ gp_without_env = generator_binary_paths(scope);
+ env_gp_without_env = env_generator_binary_paths(scope);
+
+ log_info("Generators dirs (%s):", runtime_scope_to_string(scope));
+ STRV_FOREACH(dir, gp_without_env)
+ log_info(" %s", *dir);
+
+ log_info("Environment generators dirs (%s):", runtime_scope_to_string(scope));
+ STRV_FOREACH(dir, env_gp_without_env)
+ log_info(" %s", *dir);
+
+ assert_se(!strv_isempty(gp_without_env));
+ assert_se(!strv_isempty(env_gp_without_env));
+
+ systemd_generator_path = strjoina(tmp, "/systemd-generator-path");
+ systemd_env_generator_path = strjoina(tmp, "/systemd-environment-generator-path");
+ assert_se(setenv("SYSTEMD_GENERATOR_PATH", systemd_generator_path, 1) == 0);
+ assert_se(setenv("SYSTEMD_ENVIRONMENT_GENERATOR_PATH", systemd_env_generator_path, 1) == 0);
+
+ gp_with_env = generator_binary_paths(scope);
+ env_gp_with_env = env_generator_binary_paths(scope);
+
+ log_info("Generators dirs (%s):", runtime_scope_to_string(scope));
+ STRV_FOREACH(dir, gp_with_env)
+ log_info(" %s", *dir);
+
+ log_info("Environment generators dirs (%s):", runtime_scope_to_string(scope));
+ STRV_FOREACH(dir, env_gp_with_env)
+ log_info(" %s", *dir);
+
+ assert_se(strv_equal(gp_with_env, STRV_MAKE(systemd_generator_path)));
+ assert_se(strv_equal(env_gp_with_env, STRV_MAKE(systemd_env_generator_path)));
+}
+
+TEST(generator_binary_paths) {
+ test_generator_binary_paths_one(RUNTIME_SCOPE_SYSTEM);
+ test_generator_binary_paths_one(RUNTIME_SCOPE_USER);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-path-util.c b/src/test/test-path-util.c
new file mode 100644
index 0000000..f5a4256
--- /dev/null
+++ b/src/test/test-path-util.c
@@ -0,0 +1,1308 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "exec-util.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "macro.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "rm-rf.h"
+#include "stat-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+TEST(print_paths) {
+ log_info("DEFAULT_PATH=%s", DEFAULT_PATH);
+ log_info("DEFAULT_USER_PATH=%s", DEFAULT_USER_PATH);
+}
+
+TEST(path) {
+ assert_se(path_is_absolute("/"));
+ assert_se(!path_is_absolute("./"));
+
+ assert_se(streq(basename("./aa/bb/../file.da."), "file.da."));
+ assert_se(streq(basename("/aa///.file"), ".file"));
+ assert_se(streq(basename("/aa///file..."), "file..."));
+ assert_se(streq(basename("file.../"), ""));
+
+ assert_se(PATH_IN_SET("/bin", "/", "/bin", "/foo"));
+ assert_se(PATH_IN_SET("/bin", "/bin"));
+ assert_se(PATH_IN_SET("/bin", "/foo/bar", "/bin"));
+ assert_se(PATH_IN_SET("/", "/", "/", "/foo/bar"));
+ assert_se(!PATH_IN_SET("/", "/abc", "/def"));
+
+ assert_se(path_equal_ptr(NULL, NULL));
+ assert_se(path_equal_ptr("/a", "/a"));
+ assert_se(!path_equal_ptr("/a", "/b"));
+ assert_se(!path_equal_ptr("/a", NULL));
+ assert_se(!path_equal_ptr(NULL, "/a"));
+}
+
+TEST(is_path) {
+ assert_se(!is_path("foo"));
+ assert_se(!is_path("dos.ext"));
+ assert_se( is_path("/dir"));
+ assert_se( is_path("a/b"));
+ assert_se( is_path("a/b.ext"));
+
+ assert_se(!is_path("."));
+ assert_se(!is_path(""));
+ assert_se(!is_path(".."));
+
+ assert_se( is_path("/dev"));
+ assert_se( is_path("/./dev"));
+ assert_se( is_path("/./dev/."));
+ assert_se( is_path("/./dev."));
+ assert_se( is_path("//dev"));
+ assert_se( is_path("///dev"));
+ assert_se( is_path("/dev/"));
+ assert_se( is_path("///dev/"));
+ assert_se( is_path("/./dev/"));
+ assert_se( is_path("/../dev/"));
+ assert_se( is_path("/dev/sda"));
+ assert_se( is_path("/dev/sda5"));
+ assert_se( is_path("/dev/sda5b3"));
+ assert_se( is_path("/dev/sda5b3/idontexit"));
+ assert_se( is_path("/../dev/sda"));
+ assert_se( is_path("/../../dev/sda5"));
+ assert_se( is_path("/../../../dev/sda5b3"));
+ assert_se( is_path("/.././.././dev/sda5b3/idontexit"));
+ assert_se( is_path("/sys"));
+ assert_se( is_path("/sys/"));
+ assert_se( is_path("/./sys"));
+ assert_se( is_path("/./sys/."));
+ assert_se( is_path("/./sys."));
+ assert_se( is_path("/sys/what"));
+ assert_se( is_path("/sys/something/.."));
+ assert_se( is_path("/sys/something/../"));
+ assert_se( is_path("/sys////"));
+ assert_se( is_path("/sys////."));
+ assert_se( is_path("/sys/.."));
+ assert_se( is_path("/sys/../"));
+ assert_se( is_path("/usr/../dev/sda"));
+}
+
+TEST(is_device_path) {
+ assert_se(!is_device_path("foo"));
+ assert_se(!is_device_path("dos.ext"));
+ assert_se(!is_device_path("/dir"));
+ assert_se(!is_device_path("a/b"));
+ assert_se(!is_device_path("a/b.ext"));
+
+ assert_se(!is_device_path("."));
+ assert_se(!is_device_path(""));
+ assert_se(!is_device_path(".."));
+
+ assert_se(!is_device_path("/dev"));
+ assert_se(!is_device_path("/./dev"));
+ assert_se(!is_device_path("/./dev/."));
+ assert_se(!is_device_path("/./dev."));
+ assert_se( is_device_path("/./dev/foo"));
+ assert_se( is_device_path("/./dev/./foo"));
+ assert_se(!is_device_path("/./dev./foo"));
+ assert_se(!is_device_path("//dev"));
+ assert_se(!is_device_path("///dev"));
+ assert_se(!is_device_path("/dev/"));
+ assert_se(!is_device_path("///dev/"));
+ assert_se(!is_device_path("/./dev/"));
+ assert_se(!is_device_path("/../dev/"));
+ assert_se( is_device_path("/dev/sda"));
+ assert_se( is_device_path("/dev/sda5"));
+ assert_se( is_device_path("/dev/sda5b3"));
+ assert_se( is_device_path("/dev/sda5b3/idontexit"));
+ assert_se(!is_device_path("/../dev/sda"));
+ assert_se(!is_device_path("/../../dev/sda5"));
+ assert_se(!is_device_path("/../../../dev/sda5b3"));
+ assert_se(!is_device_path("/.././.././dev/sda5b3/idontexit"));
+ assert_se(!is_device_path("/sys"));
+ assert_se(!is_device_path("/sys/"));
+ assert_se(!is_device_path("/./sys"));
+ assert_se(!is_device_path("/./sys/."));
+ assert_se(!is_device_path("/./sys."));
+ assert_se( is_device_path("/./sys/foo"));
+ assert_se( is_device_path("/./sys/./foo"));
+ assert_se(!is_device_path("/./sys./foo"));
+ assert_se( is_device_path("/sys/what"));
+ assert_se( is_device_path("/sys/something/.."));
+ assert_se( is_device_path("/sys/something/../"));
+ assert_se(!is_device_path("/sys////"));
+ assert_se(!is_device_path("/sys////."));
+ assert_se( is_device_path("/sys/.."));
+ assert_se( is_device_path("/sys/../"));
+ assert_se(!is_device_path("/usr/../dev/sda"));
+}
+
+static void test_path_simplify_one(const char *in, const char *out, PathSimplifyFlags flags) {
+ char *p;
+
+ p = strdupa_safe(in);
+ path_simplify_full(p, flags);
+ log_debug("/* test_path_simplify(%s) → %s (expected: %s) */", in, p, out);
+ assert_se(streq(p, out));
+}
+
+TEST(path_simplify) {
+ _cleanup_free_ char *hoge = NULL, *hoge_out = NULL;
+ char foo[NAME_MAX * 2];
+
+ test_path_simplify_one("", "", 0);
+ test_path_simplify_one("aaa/bbb////ccc", "aaa/bbb/ccc", 0);
+ test_path_simplify_one("//aaa/.////ccc", "/aaa/ccc", 0);
+ test_path_simplify_one("///", "/", 0);
+ test_path_simplify_one("///", "/", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("///.//", "/", 0);
+ test_path_simplify_one("///.//.///", "/", 0);
+ test_path_simplify_one("////.././///../.", "/", 0);
+ test_path_simplify_one(".", ".", 0);
+ test_path_simplify_one("./", ".", 0);
+ test_path_simplify_one("./", "./", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one(".///.//./.", ".", 0);
+ test_path_simplify_one(".///.//././/", ".", 0);
+ test_path_simplify_one("//./aaa///.//./.bbb/..///c.//d.dd///..eeee/.",
+ "/aaa/.bbb/../c./d.dd/..eeee", 0);
+ test_path_simplify_one("//./aaa///.//./.bbb/..///c.//d.dd///..eeee/..",
+ "/aaa/.bbb/../c./d.dd/..eeee/..", 0);
+ test_path_simplify_one(".//./aaa///.//./.bbb/..///c.//d.dd///..eeee/..",
+ "aaa/.bbb/../c./d.dd/..eeee/..", 0);
+ test_path_simplify_one("..//./aaa///.//./.bbb/..///c.//d.dd///..eeee/..",
+ "../aaa/.bbb/../c./d.dd/..eeee/..", 0);
+ test_path_simplify_one("abc///", "abc/", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+
+ test_path_simplify_one("/../abc", "/abc", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/../abc///", "/abc/", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/../abc///", "/abc", 0);
+ test_path_simplify_one("/../abc", "/abc", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/../abc///..", "/abc/..", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/../abc///../", "/abc/../", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/../abc///../", "/abc/..", 0);
+
+ test_path_simplify_one("/../../abc", "/abc", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/../../abc///", "/abc/", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/../../abc///", "/abc", 0);
+ test_path_simplify_one("/../../abc", "/abc", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/../../abc///../..", "/abc/../..", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/../../abc///../../", "/abc/../../", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/../../abc///../../", "/abc/../..", 0);
+
+ test_path_simplify_one("/.././../abc", "/abc", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/.././../abc///", "/abc/", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/.././../abc///", "/abc", 0);
+ test_path_simplify_one("/.././../abc", "/abc", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/.././../abc///../..", "/abc/../..", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/.././../abc///../../", "/abc/../../", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/.././../abc///../../", "/abc/../..", 0);
+
+ test_path_simplify_one("/./.././../abc", "/abc", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/./.././../abc///", "/abc/", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/./.././../abc///", "/abc", 0);
+ test_path_simplify_one("/./.././../abc", "/abc", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/./.././../abc///../..", "/abc/../..", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/./.././../abc///../../", "/abc/../../", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/./.././../abc///../../", "/abc/../..", 0);
+
+ test_path_simplify_one("/.../abc", "/.../abc", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/.../abc///", "/.../abc/", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/.../abc///", "/.../abc", 0);
+ test_path_simplify_one("/.../abc", "/.../abc", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/.../abc///...", "/.../abc/...", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/.../abc///.../", "/.../abc/.../", PATH_SIMPLIFY_KEEP_TRAILING_SLASH);
+ test_path_simplify_one("/.../abc///.../", "/.../abc/...", 0);
+
+ memset(foo, 'a', sizeof(foo) -1);
+ char_array_0(foo);
+
+ test_path_simplify_one(foo, foo, 0);
+
+ hoge = strjoin("/", foo);
+ assert_se(hoge);
+ test_path_simplify_one(hoge, hoge, 0);
+ hoge = mfree(hoge);
+
+ hoge = strjoin("a////.//././//./b///././/./c/////././//./", foo, "//.//////d/e/.//f/");
+ assert_se(hoge);
+
+ hoge_out = strjoin("a/b/c/", foo, "//.//////d/e/.//f/");
+ assert_se(hoge_out);
+
+ test_path_simplify_one(hoge, hoge_out, 0);
+}
+
+static void test_path_compare_one(const char *a, const char *b, int expected) {
+ int r;
+
+ assert_se(path_compare(a, a) == 0);
+ assert_se(path_compare(b, b) == 0);
+
+ r = path_compare(a, b);
+ assert_se((r > 0) == (expected > 0) && (r < 0) == (expected < 0));
+ r = path_compare(b, a);
+ assert_se((r < 0) == (expected > 0) && (r > 0) == (expected < 0));
+
+ assert_se(path_equal(a, a) == 1);
+ assert_se(path_equal(b, b) == 1);
+ assert_se(path_equal(a, b) == (expected == 0));
+ assert_se(path_equal(b, a) == (expected == 0));
+}
+
+TEST(path_compare) {
+ test_path_compare_one("/goo", "/goo", 0);
+ test_path_compare_one("/goo", "/goo", 0);
+ test_path_compare_one("//goo", "/goo", 0);
+ test_path_compare_one("//goo/////", "/goo", 0);
+ test_path_compare_one("goo/////", "goo", 0);
+ test_path_compare_one("/goo/boo", "/goo//boo", 0);
+ test_path_compare_one("//goo/boo", "/goo/boo//", 0);
+ test_path_compare_one("//goo/././//./boo//././//", "/goo/boo//.", 0);
+ test_path_compare_one("/.", "//.///", 0);
+ test_path_compare_one("/x", "x/", 1);
+ test_path_compare_one("x/", "/", -1);
+ test_path_compare_one("/x/./y", "x/y", 1);
+ test_path_compare_one("/x/./y", "/x/y", 0);
+ test_path_compare_one("/x/./././y", "/x/y/././.", 0);
+ test_path_compare_one("./x/./././y", "./x/y/././.", 0);
+ test_path_compare_one(".", "./.", 0);
+ test_path_compare_one(".", "././.", 0);
+ test_path_compare_one("./..", ".", 1);
+ test_path_compare_one("x/.y", "x/y", -1);
+ test_path_compare_one("foo", "/foo", -1);
+ test_path_compare_one("/foo", "/foo/bar", -1);
+ test_path_compare_one("/foo/aaa", "/foo/b", -1);
+ test_path_compare_one("/foo/aaa", "/foo/b/a", -1);
+ test_path_compare_one("/foo/a", "/foo/aaa", -1);
+ test_path_compare_one("/foo/a/b", "/foo/aaa", -1);
+}
+
+static void test_path_compare_filename_one(const char *a, const char *b, int expected) {
+ int r;
+
+ assert_se(path_compare_filename(a, a) == 0);
+ assert_se(path_compare_filename(b, b) == 0);
+
+ r = path_compare_filename(a, b);
+ assert_se((r > 0) == (expected > 0) && (r < 0) == (expected < 0));
+ r = path_compare_filename(b, a);
+ assert_se((r < 0) == (expected > 0) && (r > 0) == (expected < 0));
+
+ assert_se(path_equal_filename(a, a) == 1);
+ assert_se(path_equal_filename(b, b) == 1);
+ assert_se(path_equal_filename(a, b) == (expected == 0));
+ assert_se(path_equal_filename(b, a) == (expected == 0));
+}
+
+TEST(path_compare_filename) {
+ test_path_compare_filename_one("/goo", "/goo", 0);
+ test_path_compare_filename_one("/goo", "/goo", 0);
+ test_path_compare_filename_one("//goo", "/goo", 0);
+ test_path_compare_filename_one("//goo/////", "/goo", 0);
+ test_path_compare_filename_one("goo/////", "goo", 0);
+ test_path_compare_filename_one("/goo/boo", "/goo//boo", 0);
+ test_path_compare_filename_one("//goo/boo", "/goo/boo//", 0);
+ test_path_compare_filename_one("//goo/././//./boo//././//", "/goo/boo//.", 0);
+ test_path_compare_filename_one("/.", "//.///", -1);
+ test_path_compare_filename_one("/x", "x/", 0);
+ test_path_compare_filename_one("x/", "/", 1);
+ test_path_compare_filename_one("/x/./y", "x/y", 0);
+ test_path_compare_filename_one("/x/./y", "/x/y", 0);
+ test_path_compare_filename_one("/x/./././y", "/x/y/././.", 0);
+ test_path_compare_filename_one("./x/./././y", "./x/y/././.", 0);
+ test_path_compare_filename_one(".", "./.", -1);
+ test_path_compare_filename_one(".", "././.", -1);
+ test_path_compare_filename_one("./..", ".", 1);
+ test_path_compare_filename_one("x/.y", "x/y", -1);
+ test_path_compare_filename_one("foo", "/foo", 0);
+ test_path_compare_filename_one("/foo", "/foo/bar", 1);
+ test_path_compare_filename_one("/foo/aaa", "/foo/b", -1);
+ test_path_compare_filename_one("/foo/aaa", "/foo/b/a", 1);
+ test_path_compare_filename_one("/foo/a", "/foo/aaa", -1);
+ test_path_compare_filename_one("/foo/a/b", "/foo/aaa", 1);
+ test_path_compare_filename_one("/a/c", "/b/c", 0);
+ test_path_compare_filename_one("/a", "/a", 0);
+ test_path_compare_filename_one("/a/b", "/a/c", -1);
+ test_path_compare_filename_one("/b", "/c", -1);
+}
+
+TEST(path_equal_root) {
+ /* Nail down the details of how path_equal("/", ...) works. */
+
+ assert_se(path_equal("/", "/"));
+ assert_se(path_equal("/", "//"));
+
+ assert_se(path_equal("/", "/./"));
+ assert_se(!path_equal("/", "/../"));
+
+ assert_se(!path_equal("/", "/.../"));
+
+ /* Make sure that files_same works as expected. */
+
+ assert_se(inode_same("/", "/", 0) > 0);
+ assert_se(inode_same("/", "/", AT_SYMLINK_NOFOLLOW) > 0);
+ assert_se(inode_same("/", "//", 0) > 0);
+ assert_se(inode_same("/", "//", AT_SYMLINK_NOFOLLOW) > 0);
+
+ assert_se(inode_same("/", "/./", 0) > 0);
+ assert_se(inode_same("/", "/./", AT_SYMLINK_NOFOLLOW) > 0);
+ assert_se(inode_same("/", "/../", 0) > 0);
+ assert_se(inode_same("/", "/../", AT_SYMLINK_NOFOLLOW) > 0);
+
+ assert_se(inode_same("/", "/.../", 0) == -ENOENT);
+ assert_se(inode_same("/", "/.../", AT_SYMLINK_NOFOLLOW) == -ENOENT);
+
+ /* The same for path_equal_or_files_same. */
+
+ assert_se(path_equal_or_inode_same("/", "/", 0));
+ assert_se(path_equal_or_inode_same("/", "/", AT_SYMLINK_NOFOLLOW));
+ assert_se(path_equal_or_inode_same("/", "//", 0));
+ assert_se(path_equal_or_inode_same("/", "//", AT_SYMLINK_NOFOLLOW));
+
+ assert_se(path_equal_or_inode_same("/", "/./", 0));
+ assert_se(path_equal_or_inode_same("/", "/./", AT_SYMLINK_NOFOLLOW));
+ assert_se(path_equal_or_inode_same("/", "/../", 0));
+ assert_se(path_equal_or_inode_same("/", "/../", AT_SYMLINK_NOFOLLOW));
+
+ assert_se(!path_equal_or_inode_same("/", "/.../", 0));
+ assert_se(!path_equal_or_inode_same("/", "/.../", AT_SYMLINK_NOFOLLOW));
+}
+
+TEST(find_executable_full) {
+ char *p;
+ char* test_file_name;
+ _cleanup_close_ int fd = -EBADF;
+ char fn[] = "/tmp/test-XXXXXX";
+
+ assert_se(find_executable_full("sh", NULL, NULL, true, &p, NULL) == 0);
+ puts(p);
+ assert_se(streq(basename(p), "sh"));
+ free(p);
+
+ assert_se(find_executable_full("sh", NULL, NULL, false, &p, NULL) == 0);
+ puts(p);
+ assert_se(streq(basename(p), "sh"));
+ free(p);
+
+ _cleanup_free_ char *oldpath = NULL;
+ p = getenv("PATH");
+ if (p)
+ assert_se(oldpath = strdup(p));
+
+ assert_se(unsetenv("PATH") == 0);
+
+ assert_se(find_executable_full("sh", NULL, NULL, true, &p, NULL) == 0);
+ puts(p);
+ assert_se(streq(basename(p), "sh"));
+ free(p);
+
+ assert_se(find_executable_full("sh", NULL, NULL, false, &p, NULL) == 0);
+ puts(p);
+ assert_se(streq(basename(p), "sh"));
+ free(p);
+
+ if (oldpath)
+ assert_se(setenv("PATH", oldpath, true) >= 0);
+
+ assert_se((fd = mkostemp_safe(fn)) >= 0);
+ assert_se(fchmod(fd, 0755) >= 0);
+
+ test_file_name = basename(fn);
+
+ assert_se(find_executable_full(test_file_name, NULL, STRV_MAKE("/doesnotexist", "/tmp", "/bin"), false, &p, NULL) == 0);
+ puts(p);
+ assert_se(streq(p, fn));
+ free(p);
+
+ (void) unlink(fn);
+ assert_se(find_executable_full(test_file_name, NULL, STRV_MAKE("/doesnotexist", "/tmp", "/bin"), false, &p, NULL) == -ENOENT);
+}
+
+TEST(find_executable) {
+ char *p;
+
+ assert_se(find_executable("/bin/sh", &p) == 0);
+ puts(p);
+ assert_se(path_equal(p, "/bin/sh"));
+ free(p);
+
+ assert_se(find_executable(saved_argv[0], &p) == 0);
+ puts(p);
+ assert_se(endswith(p, "/test-path-util"));
+ assert_se(path_is_absolute(p));
+ free(p);
+
+ assert_se(find_executable("sh", &p) == 0);
+ puts(p);
+ assert_se(endswith(p, "/sh"));
+ assert_se(path_is_absolute(p));
+ free(p);
+
+ assert_se(find_executable("/bin/touch", &p) == 0);
+ assert_se(streq(p, "/bin/touch"));
+ free(p);
+
+ assert_se(find_executable("touch", &p) == 0);
+ assert_se(path_is_absolute(p));
+ assert_se(streq(basename(p), "touch"));
+ free(p);
+
+ assert_se(find_executable("xxxx-xxxx", &p) == -ENOENT);
+ assert_se(find_executable("/some/dir/xxxx-xxxx", &p) == -ENOENT);
+ assert_se(find_executable("/proc/filesystems", &p) == -EACCES);
+}
+
+static void test_find_executable_exec_one(const char *path) {
+ _cleanup_free_ char *t = NULL;
+ _cleanup_close_ int fd = -EBADF;
+ pid_t pid;
+ int r;
+
+ r = find_executable_full(path, NULL, NULL, false, &t, &fd);
+
+ log_info_errno(r, "%s: %s → %s: %d/%m", __func__, path, t ?: "-", fd);
+
+ assert_se(fd > STDERR_FILENO);
+ assert_se(path_is_absolute(t));
+ if (path_is_absolute(path))
+ assert_se(streq(t, path));
+
+ pid = fork();
+ assert_se(pid >= 0);
+ if (pid == 0) {
+ r = fexecve_or_execve(fd, t, STRV_MAKE(t, "--version"), STRV_MAKE(NULL));
+ log_error_errno(r, "[f]execve: %m");
+ _exit(EXIT_FAILURE);
+ }
+
+ assert_se(wait_for_terminate_and_check(t, pid, WAIT_LOG) == 0);
+}
+
+TEST(find_executable_exec) {
+ test_find_executable_exec_one("touch");
+ test_find_executable_exec_one("/bin/touch");
+
+ _cleanup_free_ char *script = NULL;
+ assert_se(get_testdata_dir("test-path-util/script.sh", &script) >= 0);
+ test_find_executable_exec_one(script);
+}
+
+TEST(prefixes) {
+ static const char* const values[] = {
+ "/a/b/c/d",
+ "/a/b/c",
+ "/a/b",
+ "/a",
+ "",
+ NULL
+ };
+ unsigned i;
+ char s[PATH_MAX];
+ bool b;
+
+ i = 0;
+ PATH_FOREACH_PREFIX_MORE(s, "/a/b/c/d") {
+ log_error("---%s---", s);
+ assert_se(streq(s, values[i++]));
+ }
+ assert_se(values[i] == NULL);
+
+ i = 1;
+ PATH_FOREACH_PREFIX(s, "/a/b/c/d") {
+ log_error("---%s---", s);
+ assert_se(streq(s, values[i++]));
+ }
+ assert_se(values[i] == NULL);
+
+ i = 0;
+ PATH_FOREACH_PREFIX_MORE(s, "////a////b////c///d///////")
+ assert_se(streq(s, values[i++]));
+ assert_se(values[i] == NULL);
+
+ i = 1;
+ PATH_FOREACH_PREFIX(s, "////a////b////c///d///////")
+ assert_se(streq(s, values[i++]));
+ assert_se(values[i] == NULL);
+
+ PATH_FOREACH_PREFIX(s, "////")
+ assert_not_reached();
+
+ b = false;
+ PATH_FOREACH_PREFIX_MORE(s, "////") {
+ assert_se(!b);
+ assert_se(streq(s, ""));
+ b = true;
+ }
+ assert_se(b);
+
+ PATH_FOREACH_PREFIX(s, "")
+ assert_not_reached();
+
+ b = false;
+ PATH_FOREACH_PREFIX_MORE(s, "") {
+ assert_se(!b);
+ assert_se(streq(s, ""));
+ b = true;
+ }
+}
+
+TEST(path_join) {
+#define test_join(expected, ...) { \
+ _cleanup_free_ char *z = NULL; \
+ z = path_join(__VA_ARGS__); \
+ log_debug("got \"%s\", expected \"%s\"", z, expected); \
+ assert_se(streq(z, expected)); \
+ }
+
+ test_join("/root/a/b/c", "/root", "/a/b", "/c");
+ test_join("/root/a/b/c", "/root", "a/b", "c");
+ test_join("/root/a/b/c", "/root", "/a/b", "c");
+ test_join("/root/c", "/root", "/", "c");
+ test_join("/root/", "/root", "/", NULL);
+
+ test_join("/a/b/c", "", "/a/b", "/c");
+ test_join("a/b/c", "", "a/b", "c");
+ test_join("/a/b/c", "", "/a/b", "c");
+ test_join("/c", "", "/", "c");
+ test_join("/", "", "/", NULL);
+
+ test_join("/a/b/c", NULL, "/a/b", "/c");
+ test_join("a/b/c", NULL, "a/b", "c");
+ test_join("/a/b/c", NULL, "/a/b", "c");
+ test_join("/c", NULL, "/", "c");
+ test_join("/", NULL, "/", NULL);
+
+ test_join("", "", NULL);
+ test_join("", NULL, "");
+ test_join("", NULL, NULL);
+
+ test_join("foo/bar", "foo", "bar");
+ test_join("foo/bar", "", "foo", "bar");
+ test_join("foo/bar", NULL, "foo", NULL, "bar");
+ test_join("foo/bar", "", "foo", "", "bar", "");
+ test_join("foo/bar", "", "", "", "", "foo", "", "", "", "bar", "", "", "");
+
+ test_join("//foo///bar//", "", "/", "", "/foo/", "", "/", "", "/bar/", "", "/", "");
+ test_join("/foo/bar/", "/", "foo", "/", "bar", "/");
+ test_join("foo/bar/baz", "foo", "bar", "baz");
+ test_join("foo/bar/baz", "foo/", "bar", "/baz");
+ test_join("foo//bar//baz", "foo/", "/bar/", "/baz");
+ test_join("//foo////bar////baz//", "//foo/", "///bar/", "///baz//");
+}
+
+TEST(path_extend) {
+ _cleanup_free_ char *p = NULL;
+
+ assert_se(path_extend(&p, "foo", "bar", "baz") == p);
+ assert_se(streq(p, "foo/bar/baz"));
+
+ assert_se(path_extend(&p, "foo", "bar", "baz") == p);
+ assert_se(streq(p, "foo/bar/baz/foo/bar/baz"));
+
+ p = mfree(p);
+ assert_se(path_extend(&p, "foo") == p);
+ assert_se(streq(p, "foo"));
+
+ assert_se(path_extend(&p, "/foo") == p);
+ assert_se(streq(p, "foo/foo"));
+ assert_se(path_extend(&p, "/waaaah/wahhh//") == p);
+ assert_se(streq(p, "foo/foo/waaaah/wahhh//")); /* path_extend() does not drop redundant slashes */
+ assert_se(path_extend(&p, "/aaa/bbb/") == p);
+ assert_se(streq(p, "foo/foo/waaaah/wahhh///aaa/bbb/")); /* but not add an extra slash */
+
+ assert_se(free_and_strdup(&p, "/") >= 0);
+ assert_se(path_extend(&p, "foo") == p);
+ assert_se(streq(p, "/foo"));
+}
+
+TEST(fsck_exists) {
+ /* Ensure we use a sane default for PATH. */
+ assert_se(unsetenv("PATH") == 0);
+
+ /* We might or might not find one of these, so keep the test lax. */
+ assert_se(fsck_exists_for_fstype("minix") >= 0);
+
+ assert_se(fsck_exists_for_fstype("AbCdE") == 0);
+ assert_se(fsck_exists_for_fstype("/../bin/") == 0);
+}
+
+static void test_path_make_relative_one(const char *from, const char *to, const char *expected) {
+ _cleanup_free_ char *z = NULL;
+ int r;
+
+ log_info("/* %s(%s, %s) */", __func__, from, to);
+
+ r = path_make_relative(from, to, &z);
+ assert_se((r >= 0) == !!expected);
+ assert_se(streq_ptr(z, expected));
+}
+
+TEST(path_make_relative) {
+ test_path_make_relative_one("some/relative/path", "/some/path", NULL);
+ test_path_make_relative_one("/some/path", "some/relative/path", NULL);
+ test_path_make_relative_one("/some/dotdot/../path", "/some/path", NULL);
+
+ test_path_make_relative_one("/", "/", ".");
+ test_path_make_relative_one("/", "/some/path", "some/path");
+ test_path_make_relative_one("/some/path", "/some/path", ".");
+ test_path_make_relative_one("/some/path", "/some/path/in/subdir", "in/subdir");
+ test_path_make_relative_one("/some/path", "/", "../..");
+ test_path_make_relative_one("/some/path", "/some/other/path", "../other/path");
+ test_path_make_relative_one("/some/path/./dot", "/some/further/path", "../../further/path");
+ test_path_make_relative_one("//extra.//.//./.slashes//./won't////fo.ol///anybody//", "/././/extra././/.slashes////ar.e/.just/././.fine///", "../../../ar.e/.just/.fine");
+}
+
+static void test_path_make_relative_parent_one(const char *from, const char *to, const char *expected) {
+ _cleanup_free_ char *z = NULL;
+ int r;
+
+ log_info("/* %s(%s, %s) */", __func__, from, to);
+
+ r = path_make_relative_parent(from, to, &z);
+ assert_se((r >= 0) == !!expected);
+ assert_se(streq_ptr(z, expected));
+}
+
+TEST(path_make_relative_parent) {
+ test_path_make_relative_parent_one("some/relative/path/hoge", "/some/path", NULL);
+ test_path_make_relative_parent_one("/some/path/hoge", "some/relative/path", NULL);
+ test_path_make_relative_parent_one("/some/dotdot/../path/hoge", "/some/path", NULL);
+ test_path_make_relative_parent_one("/", "/aaa", NULL);
+
+ test_path_make_relative_parent_one("/hoge", "/", ".");
+ test_path_make_relative_parent_one("/hoge", "/some/path", "some/path");
+ test_path_make_relative_parent_one("/some/path/hoge", "/some/path", ".");
+ test_path_make_relative_parent_one("/some/path/hoge", "/some/path/in/subdir", "in/subdir");
+ test_path_make_relative_parent_one("/some/path/hoge", "/", "../..");
+ test_path_make_relative_parent_one("/some/path/hoge", "/some/other/path", "../other/path");
+ test_path_make_relative_parent_one("/some/path/./dot/hoge", "/some/further/path", "../../further/path");
+ test_path_make_relative_parent_one("//extra.//.//./.slashes//./won't////fo.ol///anybody//hoge", "/././/extra././/.slashes////ar.e/.just/././.fine///", "../../../ar.e/.just/.fine");
+}
+
+TEST(path_strv_resolve) {
+ char tmp_dir[] = "/tmp/test-path-util-XXXXXX";
+ _cleanup_strv_free_ char **search_dirs = NULL;
+ _cleanup_strv_free_ char **absolute_dirs = NULL;
+
+ assert_se(mkdtemp(tmp_dir) != NULL);
+
+ search_dirs = strv_new("/dir1", "/dir2", "/dir3");
+ assert_se(search_dirs);
+ STRV_FOREACH(d, search_dirs) {
+ char *p = path_join(tmp_dir, *d);
+ assert_se(p);
+ assert_se(strv_push(&absolute_dirs, p) == 0);
+ }
+
+ assert_se(mkdir(absolute_dirs[0], 0700) == 0);
+ assert_se(mkdir(absolute_dirs[1], 0700) == 0);
+ assert_se(symlink("dir2", absolute_dirs[2]) == 0);
+
+ path_strv_resolve(search_dirs, tmp_dir);
+ assert_se(streq(search_dirs[0], "/dir1"));
+ assert_se(streq(search_dirs[1], "/dir2"));
+ assert_se(streq(search_dirs[2], "/dir2"));
+
+ assert_se(rm_rf(tmp_dir, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
+}
+
+static void test_path_startswith_one(const char *path, const char *prefix, const char *skipped, const char *expected) {
+ const char *p, *q;
+
+ log_debug("/* %s(%s, %s) */", __func__, path, prefix);
+
+ p = path_startswith(path, prefix);
+ assert_se(streq_ptr(p, expected));
+ if (p) {
+ q = strjoina(skipped, p);
+ assert_se(streq(q, path));
+ assert_se(p == path + strlen(skipped));
+ }
+}
+
+TEST(path_startswith) {
+ test_path_startswith_one("/foo/bar/barfoo/", "/foo", "/foo/", "bar/barfoo/");
+ test_path_startswith_one("/foo/bar/barfoo/", "/foo/", "/foo/", "bar/barfoo/");
+ test_path_startswith_one("/foo/bar/barfoo/", "/", "/", "foo/bar/barfoo/");
+ test_path_startswith_one("/foo/bar/barfoo/", "////", "/", "foo/bar/barfoo/");
+ test_path_startswith_one("/foo/bar/barfoo/", "/foo//bar/////barfoo///", "/foo/bar/barfoo/", "");
+ test_path_startswith_one("/foo/bar/barfoo/", "/foo/bar/barfoo////", "/foo/bar/barfoo/", "");
+ test_path_startswith_one("/foo/bar/barfoo/", "/foo/bar///barfoo/", "/foo/bar/barfoo/", "");
+ test_path_startswith_one("/foo/bar/barfoo/", "/foo////bar/barfoo/", "/foo/bar/barfoo/", "");
+ test_path_startswith_one("/foo/bar/barfoo/", "////foo/bar/barfoo/", "/foo/bar/barfoo/", "");
+ test_path_startswith_one("/foo/bar/barfoo/", "/foo/bar/barfoo", "/foo/bar/barfoo/", "");
+
+ test_path_startswith_one("/foo/./bar///barfoo/./.", "/foo", "/foo/./", "bar///barfoo/./.");
+ test_path_startswith_one("/foo/./bar///barfoo/./.", "/foo/", "/foo/./", "bar///barfoo/./.");
+ test_path_startswith_one("/foo/./bar///barfoo/./.", "/", "/", "foo/./bar///barfoo/./.");
+ test_path_startswith_one("/foo/./bar///barfoo/./.", "////", "/", "foo/./bar///barfoo/./.");
+ test_path_startswith_one("/foo/./bar///barfoo/./.", "/foo//bar/////barfoo///", "/foo/./bar///barfoo/./.", "");
+ test_path_startswith_one("/foo/./bar///barfoo/./.", "/foo/bar/barfoo////", "/foo/./bar///barfoo/./.", "");
+ test_path_startswith_one("/foo/./bar///barfoo/./.", "/foo/bar///barfoo/", "/foo/./bar///barfoo/./.", "");
+ test_path_startswith_one("/foo/./bar///barfoo/./.", "/foo////bar/barfoo/", "/foo/./bar///barfoo/./.", "");
+ test_path_startswith_one("/foo/./bar///barfoo/./.", "////foo/bar/barfoo/", "/foo/./bar///barfoo/./.", "");
+ test_path_startswith_one("/foo/./bar///barfoo/./.", "/foo/bar/barfoo", "/foo/./bar///barfoo/./.", "");
+
+ test_path_startswith_one("/foo/bar/barfoo/", "/foo/bar/barfooa/", NULL, NULL);
+ test_path_startswith_one("/foo/bar/barfoo/", "/foo/bar/barfooa", NULL, NULL);
+ test_path_startswith_one("/foo/bar/barfoo/", "", NULL, NULL);
+ test_path_startswith_one("/foo/bar/barfoo/", "/bar/foo", NULL, NULL);
+ test_path_startswith_one("/foo/bar/barfoo/", "/f/b/b/", NULL, NULL);
+ test_path_startswith_one("/foo/bar/barfoo/", "/foo/bar/barfo", NULL, NULL);
+ test_path_startswith_one("/foo/bar/barfoo/", "/foo/bar/bar", NULL, NULL);
+ test_path_startswith_one("/foo/bar/barfoo/", "/fo", NULL, NULL);
+}
+
+static void test_prefix_root_one(const char *r, const char *p, const char *expected) {
+ _cleanup_free_ char *s = NULL;
+ const char *t;
+
+ assert_se(s = path_join(r, p));
+ assert_se(path_equal_ptr(s, expected));
+
+ t = prefix_roota(r, p);
+ assert_se(t);
+ assert_se(path_equal_ptr(t, expected));
+}
+
+TEST(prefix_root) {
+ test_prefix_root_one("/", "/foo", "/foo");
+ test_prefix_root_one(NULL, "/foo", "/foo");
+ test_prefix_root_one("", "/foo", "/foo");
+ test_prefix_root_one("///", "/foo", "/foo");
+ test_prefix_root_one("/", "////foo", "/foo");
+ test_prefix_root_one(NULL, "////foo", "/foo");
+ test_prefix_root_one("/", "foo", "/foo");
+ test_prefix_root_one("", "foo", "foo");
+ test_prefix_root_one(NULL, "foo", "foo");
+
+ test_prefix_root_one("/foo", "/bar", "/foo/bar");
+ test_prefix_root_one("/foo", "bar", "/foo/bar");
+ test_prefix_root_one("foo", "bar", "foo/bar");
+ test_prefix_root_one("/foo/", "/bar", "/foo/bar");
+ test_prefix_root_one("/foo/", "//bar", "/foo/bar");
+ test_prefix_root_one("/foo///", "//bar", "/foo/bar");
+}
+
+TEST(file_in_same_dir) {
+ char *t;
+
+ assert_se(file_in_same_dir("/", "a", &t) == -EADDRNOTAVAIL);
+
+ assert_se(file_in_same_dir("/", "/a", &t) >= 0);
+ assert_se(streq(t, "/a"));
+ free(t);
+
+ assert_se(file_in_same_dir("", "a", &t) == -EINVAL);
+
+ assert_se(file_in_same_dir("a/", "x", &t) >= 0);
+ assert_se(streq(t, "x"));
+ free(t);
+
+ assert_se(file_in_same_dir("bar/foo", "bar", &t) >= 0);
+ assert_se(streq(t, "bar/bar"));
+ free(t);
+}
+
+static void test_path_find_first_component_one(
+ const char *path,
+ bool accept_dot_dot,
+ char **expected,
+ int ret) {
+
+ log_debug("/* %s(\"%s\", accept_dot_dot=%s) */", __func__, strnull(path), yes_no(accept_dot_dot));
+
+ for (const char *p = path;;) {
+ const char *e;
+ int r;
+
+ r = path_find_first_component(&p, accept_dot_dot, &e);
+ if (r <= 0) {
+ if (r == 0) {
+ if (path) {
+ assert_se(p == path + strlen_ptr(path));
+ assert_se(isempty(p));
+ } else
+ assert_se(!p);
+ assert_se(!e);
+ }
+ assert_se(r == ret);
+ assert_se(strv_isempty(expected));
+ return;
+ }
+
+ assert_se(e);
+ assert_se(strcspn(e, "/") == (size_t) r);
+ assert_se(strlen_ptr(*expected) == (size_t) r);
+ assert_se(strneq(e, *expected++, r));
+
+ assert_se(p);
+ log_debug("p=%s", p);
+ if (!isempty(*expected))
+ assert_se(startswith(p, *expected));
+ else if (ret >= 0) {
+ assert_se(p == path + strlen_ptr(path));
+ assert_se(isempty(p));
+ }
+ }
+}
+
+TEST(path_find_first_component) {
+ _cleanup_free_ char *hoge = NULL;
+ char foo[NAME_MAX * 2];
+
+ test_path_find_first_component_one(NULL, false, NULL, 0);
+ test_path_find_first_component_one("", false, NULL, 0);
+ test_path_find_first_component_one("/", false, NULL, 0);
+ test_path_find_first_component_one(".", false, NULL, 0);
+ test_path_find_first_component_one("./", false, NULL, 0);
+ test_path_find_first_component_one("./.", false, NULL, 0);
+ test_path_find_first_component_one("..", false, NULL, -EINVAL);
+ test_path_find_first_component_one("/..", false, NULL, -EINVAL);
+ test_path_find_first_component_one("./..", false, NULL, -EINVAL);
+ test_path_find_first_component_one("////./././//.", false, NULL, 0);
+ test_path_find_first_component_one("a/b/c", false, STRV_MAKE("a", "b", "c"), 0);
+ test_path_find_first_component_one("././//.///aa/bbb//./ccc", false, STRV_MAKE("aa", "bbb", "ccc"), 0);
+ test_path_find_first_component_one("././//.///aa/.../../bbb//./ccc/.", false, STRV_MAKE("aa", "..."), -EINVAL);
+ test_path_find_first_component_one("//./aaa///.//./.bbb/..///c.//d.dd///..eeee/.", false, STRV_MAKE("aaa", ".bbb"), -EINVAL);
+ test_path_find_first_component_one("a/foo./b//././/", false, STRV_MAKE("a", "foo.", "b"), 0);
+
+ test_path_find_first_component_one(NULL, true, NULL, 0);
+ test_path_find_first_component_one("", true, NULL, 0);
+ test_path_find_first_component_one("/", true, NULL, 0);
+ test_path_find_first_component_one(".", true, NULL, 0);
+ test_path_find_first_component_one("./", true, NULL, 0);
+ test_path_find_first_component_one("./.", true, NULL, 0);
+ test_path_find_first_component_one("..", true, STRV_MAKE(".."), 0);
+ test_path_find_first_component_one("/..", true, STRV_MAKE(".."), 0);
+ test_path_find_first_component_one("./..", true, STRV_MAKE(".."), 0);
+ test_path_find_first_component_one("////./././//.", true, NULL, 0);
+ test_path_find_first_component_one("a/b/c", true, STRV_MAKE("a", "b", "c"), 0);
+ test_path_find_first_component_one("././//.///aa/bbb//./ccc", true, STRV_MAKE("aa", "bbb", "ccc"), 0);
+ test_path_find_first_component_one("././//.///aa/.../../bbb//./ccc/.", true, STRV_MAKE("aa", "...", "..", "bbb", "ccc"), 0);
+ test_path_find_first_component_one("//./aaa///.//./.bbb/..///c.//d.dd///..eeee/.", true, STRV_MAKE("aaa", ".bbb", "..", "c.", "d.dd", "..eeee"), 0);
+ test_path_find_first_component_one("a/foo./b//././/", true, STRV_MAKE("a", "foo.", "b"), 0);
+
+ memset(foo, 'a', sizeof(foo) -1);
+ char_array_0(foo);
+
+ test_path_find_first_component_one(foo, false, NULL, -EINVAL);
+ test_path_find_first_component_one(foo, true, NULL, -EINVAL);
+
+ hoge = strjoin("a/b/c/", foo, "//d/e/.//f/");
+ assert_se(hoge);
+
+ test_path_find_first_component_one(hoge, false, STRV_MAKE("a", "b", "c"), -EINVAL);
+ test_path_find_first_component_one(hoge, true, STRV_MAKE("a", "b", "c"), -EINVAL);
+}
+
+static void test_path_find_last_component_one(
+ const char *path,
+ bool accept_dot_dot,
+ char **expected,
+ int ret) {
+
+ log_debug("/* %s(\"%s\", accept_dot_dot=%s) */", __func__, strnull(path), yes_no(accept_dot_dot));
+
+ for (const char *next = NULL;;) {
+ const char *e;
+ int r;
+
+ r = path_find_last_component(path, accept_dot_dot, &next, &e);
+ if (r <= 0) {
+ if (r == 0) {
+ assert_se(next == path);
+ assert_se(!e);
+ }
+ assert_se(r == ret);
+ assert_se(strv_isempty(expected));
+ return;
+ }
+
+ assert_se(e);
+ assert_se(strcspn(e, "/") == (size_t) r);
+ assert_se(strlen_ptr(*expected) == (size_t) r);
+ assert_se(strneq(e, *expected++, r));
+
+ assert_se(next);
+ log_debug("path=%s\nnext=%s", path, next);
+ if (!isempty(*expected)) {
+ assert_se(next < path + strlen(path));
+ assert_se(next >= path + strlen(*expected));
+ assert_se(startswith(next - strlen(*expected), *expected));
+ } else if (ret >= 0)
+ assert_se(next == path);
+ }
+}
+
+TEST(path_find_last_component) {
+ _cleanup_free_ char *hoge = NULL;
+ char foo[NAME_MAX * 2];
+
+ test_path_find_last_component_one(NULL, false, NULL, 0);
+ test_path_find_last_component_one("", false, NULL, 0);
+ test_path_find_last_component_one("/", false, NULL, 0);
+ test_path_find_last_component_one(".", false, NULL, 0);
+ test_path_find_last_component_one("./", false, NULL, 0);
+ test_path_find_last_component_one("./.", false, NULL, 0);
+ test_path_find_last_component_one("..", false, NULL, -EINVAL);
+ test_path_find_last_component_one("/..", false, NULL, -EINVAL);
+ test_path_find_last_component_one("./..", false, NULL, -EINVAL);
+ test_path_find_last_component_one("////./././//.", false, NULL, 0);
+ test_path_find_last_component_one("a/b/c", false, STRV_MAKE("c", "b", "a"), 0);
+ test_path_find_last_component_one("././//.///aa./.bbb//./ccc/././/", false, STRV_MAKE("ccc", ".bbb", "aa."), 0);
+ test_path_find_last_component_one("././//.///aa/../.../bbb//./ccc/.", false, STRV_MAKE("ccc", "bbb", "..."), -EINVAL);
+ test_path_find_last_component_one("//./aaa///.//./.bbb/..///c.//d.dd///..eeee/.", false, STRV_MAKE("..eeee", "d.dd", "c."), -EINVAL);
+
+ test_path_find_last_component_one(NULL, true, NULL, 0);
+ test_path_find_last_component_one("", true, NULL, 0);
+ test_path_find_last_component_one("/", true, NULL, 0);
+ test_path_find_last_component_one(".", true, NULL, 0);
+ test_path_find_last_component_one("./", true, NULL, 0);
+ test_path_find_last_component_one("./.", true, NULL, 0);
+ test_path_find_last_component_one("..", true, STRV_MAKE(".."), 0);
+ test_path_find_last_component_one("/..", true, STRV_MAKE(".."), 0);
+ test_path_find_last_component_one("./..", true, STRV_MAKE(".."), 0);
+ test_path_find_last_component_one("////./././//.", true, NULL, 0);
+ test_path_find_last_component_one("a/b/c", true, STRV_MAKE("c", "b", "a"), 0);
+ test_path_find_last_component_one("././//.///aa./.bbb//./ccc/././/", true, STRV_MAKE("ccc", ".bbb", "aa."), 0);
+ test_path_find_last_component_one("././//.///aa/../.../bbb//./ccc/.", true, STRV_MAKE("ccc", "bbb", "...", "..", "aa"), 0);
+ test_path_find_last_component_one("//./aaa///.//./.bbb/..///c.//d.dd///..eeee/.", true, STRV_MAKE("..eeee", "d.dd", "c.", "..", ".bbb", "aaa"), 0);
+
+ memset(foo, 'a', sizeof(foo) -1);
+ char_array_0(foo);
+
+ test_path_find_last_component_one(foo, false, NULL, -EINVAL);
+ test_path_find_last_component_one(foo, true, NULL, -EINVAL);
+
+ hoge = strjoin(foo, "/a/b/c/");
+ assert_se(hoge);
+
+ test_path_find_last_component_one(hoge, false, STRV_MAKE("c", "b", "a"), -EINVAL);
+ test_path_find_last_component_one(hoge, true, STRV_MAKE("c", "b", "a"), -EINVAL);
+}
+
+TEST(last_path_component) {
+ assert_se(last_path_component(NULL) == NULL);
+ assert_se(streq(last_path_component("a/b/c"), "c"));
+ assert_se(streq(last_path_component("a/b/c/"), "c/"));
+ assert_se(streq(last_path_component("/"), "/"));
+ assert_se(streq(last_path_component("//"), "/"));
+ assert_se(streq(last_path_component("///"), "/"));
+ assert_se(streq(last_path_component("."), "."));
+ assert_se(streq(last_path_component("./."), "."));
+ assert_se(streq(last_path_component("././"), "./"));
+ assert_se(streq(last_path_component("././/"), ".//"));
+ assert_se(streq(last_path_component("/foo/a"), "a"));
+ assert_se(streq(last_path_component("/foo/a/"), "a/"));
+ assert_se(streq(last_path_component(""), ""));
+ assert_se(streq(last_path_component("a"), "a"));
+ assert_se(streq(last_path_component("a/"), "a/"));
+ assert_se(streq(last_path_component("/a"), "a"));
+ assert_se(streq(last_path_component("/a/"), "a/"));
+}
+
+static void test_path_extract_filename_one(const char *input, const char *output, int ret) {
+ _cleanup_free_ char *k = NULL;
+ int r;
+
+ r = path_extract_filename(input, &k);
+ log_info("%s → %s/%s [expected: %s/%s]",
+ strnull(input),
+ strnull(k), r < 0 ? STRERROR(r) : "-",
+ strnull(output), ret < 0 ? STRERROR(ret) : "-");
+ assert_se(streq_ptr(k, output));
+ assert_se(r == ret);
+}
+
+TEST(path_extract_filename) {
+ test_path_extract_filename_one(NULL, NULL, -EINVAL);
+ test_path_extract_filename_one("a/b/c", "c", 0);
+ test_path_extract_filename_one("a/b/c/", "c", O_DIRECTORY);
+ test_path_extract_filename_one("/", NULL, -EADDRNOTAVAIL);
+ test_path_extract_filename_one("//", NULL, -EADDRNOTAVAIL);
+ test_path_extract_filename_one("///", NULL, -EADDRNOTAVAIL);
+ test_path_extract_filename_one("/.", NULL, -EADDRNOTAVAIL);
+ test_path_extract_filename_one(".", NULL, -EADDRNOTAVAIL);
+ test_path_extract_filename_one("./", NULL, -EADDRNOTAVAIL);
+ test_path_extract_filename_one("./.", NULL, -EADDRNOTAVAIL);
+ test_path_extract_filename_one("././", NULL, -EADDRNOTAVAIL);
+ test_path_extract_filename_one("././/", NULL, -EADDRNOTAVAIL);
+ test_path_extract_filename_one("/foo/a", "a", 0);
+ test_path_extract_filename_one("/foo/a/", "a", O_DIRECTORY);
+ test_path_extract_filename_one("", NULL, -EINVAL);
+ test_path_extract_filename_one("a", "a", 0);
+ test_path_extract_filename_one("a/", "a", O_DIRECTORY);
+ test_path_extract_filename_one("a/././//.", "a", O_DIRECTORY);
+ test_path_extract_filename_one("/a", "a", 0);
+ test_path_extract_filename_one("/a/", "a", O_DIRECTORY);
+ test_path_extract_filename_one("/a//./.", "a", O_DIRECTORY);
+ test_path_extract_filename_one("/////////////a/////////////", "a", O_DIRECTORY);
+ test_path_extract_filename_one("//./a/.///b./././.c//./d//.", "d", O_DIRECTORY);
+ test_path_extract_filename_one("xx/.", "xx", O_DIRECTORY);
+ test_path_extract_filename_one("xx/..", NULL, -EINVAL);
+ test_path_extract_filename_one("..", NULL, -EINVAL);
+ test_path_extract_filename_one("/..", NULL, -EINVAL);
+ test_path_extract_filename_one("../", NULL, -EINVAL);
+}
+
+static void test_path_extract_directory_one(const char *input, const char *output, int ret) {
+ _cleanup_free_ char *k = NULL;
+ int r;
+
+ r = path_extract_directory(input, &k);
+ log_info("%s → %s/%s [expected: %s/%s]",
+ strnull(input),
+ strnull(k), r < 0 ? STRERROR(r) : "-",
+ strnull(output), STRERROR(ret));
+ assert_se(streq_ptr(k, output));
+ assert_se(r == ret);
+
+ /* Extra safety check: let's make sure that if we split out the filename too (and it works) the
+ * joined parts are identical to the original again */
+ if (r >= 0) {
+ _cleanup_free_ char *f = NULL;
+
+ r = path_extract_filename(input, &f);
+ if (r >= 0) {
+ _cleanup_free_ char *j = NULL;
+
+ assert_se(j = path_join(k, f));
+ assert_se(path_equal(input, j));
+ }
+ }
+}
+
+TEST(path_extract_directory) {
+ test_path_extract_directory_one(NULL, NULL, -EINVAL);
+ test_path_extract_directory_one("a/b/c", "a/b", 0);
+ test_path_extract_directory_one("a/b/c/", "a/b", 0);
+ test_path_extract_directory_one("/", NULL, -EADDRNOTAVAIL);
+ test_path_extract_directory_one("//", NULL, -EADDRNOTAVAIL);
+ test_path_extract_directory_one("///", NULL, -EADDRNOTAVAIL);
+ test_path_extract_directory_one("/.", NULL, -EADDRNOTAVAIL);
+ test_path_extract_directory_one(".", NULL, -EADDRNOTAVAIL);
+ test_path_extract_directory_one("./", NULL, -EADDRNOTAVAIL);
+ test_path_extract_directory_one("./.", NULL, -EADDRNOTAVAIL);
+ test_path_extract_directory_one("././", NULL, -EADDRNOTAVAIL);
+ test_path_extract_directory_one("././/", NULL, -EADDRNOTAVAIL);
+ test_path_extract_directory_one("/foo/a", "/foo", 0);
+ test_path_extract_directory_one("/foo/a/", "/foo", 0);
+ test_path_extract_directory_one("", NULL, -EINVAL);
+ test_path_extract_directory_one("a", NULL, -EDESTADDRREQ);
+ test_path_extract_directory_one("a/", NULL, -EDESTADDRREQ);
+ test_path_extract_directory_one("a/././//.", NULL, -EDESTADDRREQ);
+ test_path_extract_directory_one("/a", "/", 0);
+ test_path_extract_directory_one("/a/", "/", 0);
+ test_path_extract_directory_one("/a//./.", "/", 0);
+ test_path_extract_directory_one("/////////////a/////////////", "/", 0);
+ test_path_extract_directory_one("//./a/.///b./././.c//./d//.", "/a/b./.c", 0);
+ test_path_extract_directory_one("xx/.", NULL, -EDESTADDRREQ);
+ test_path_extract_directory_one("xx/..", NULL, -EINVAL);
+ test_path_extract_directory_one("..", NULL, -EINVAL);
+ test_path_extract_directory_one("/..", NULL, -EINVAL);
+ test_path_extract_directory_one("../", NULL, -EINVAL);
+}
+
+TEST(filename_is_valid) {
+ char foo[NAME_MAX+2];
+
+ assert_se(!filename_is_valid(""));
+ assert_se(!filename_is_valid("/bar/foo"));
+ assert_se(!filename_is_valid("/"));
+ assert_se(!filename_is_valid("."));
+ assert_se(!filename_is_valid(".."));
+ assert_se(!filename_is_valid("bar/foo"));
+ assert_se(!filename_is_valid("bar/foo/"));
+ assert_se(!filename_is_valid("bar//"));
+
+ memset(foo, 'a', sizeof(foo) - 1);
+ char_array_0(foo);
+
+ assert_se(!filename_is_valid(foo));
+
+ assert_se(filename_is_valid("foo_bar-333"));
+ assert_se(filename_is_valid("o.o"));
+}
+
+static void test_path_is_valid_and_safe_one(const char *p, bool ret) {
+ log_debug("/* %s(\"%s\") */", __func__, strnull(p));
+
+ assert_se(path_is_valid(p) == ret);
+ if (ret)
+ ret = !streq(p, "..") &&
+ !startswith(p, "../") &&
+ !endswith(p, "/..") &&
+ !strstr(p, "/../");
+ assert_se(path_is_safe(p) == ret);
+}
+
+TEST(path_is_valid_and_safe) {
+ char foo[PATH_MAX+2];
+ const char *c;
+
+ test_path_is_valid_and_safe_one("", false);
+ test_path_is_valid_and_safe_one("/bar/foo", true);
+ test_path_is_valid_and_safe_one("/bar/foo/", true);
+ test_path_is_valid_and_safe_one("/bar/foo/", true);
+ test_path_is_valid_and_safe_one("//bar//foo//", true);
+ test_path_is_valid_and_safe_one("/", true);
+ test_path_is_valid_and_safe_one("/////", true);
+ test_path_is_valid_and_safe_one("/////.///.////...///..//.", true);
+ test_path_is_valid_and_safe_one(".", true);
+ test_path_is_valid_and_safe_one("..", true);
+ test_path_is_valid_and_safe_one("bar/foo", true);
+ test_path_is_valid_and_safe_one("bar/foo/", true);
+ test_path_is_valid_and_safe_one("bar//", true);
+
+ memset(foo, 'a', sizeof(foo) -1);
+ char_array_0(foo);
+
+ test_path_is_valid_and_safe_one(foo, false);
+
+ c = strjoina("/xxx/", foo, "/yyy");
+ test_path_is_valid_and_safe_one(c, false);
+
+ test_path_is_valid_and_safe_one("foo_bar-333", true);
+ test_path_is_valid_and_safe_one("o.o", true);
+}
+
+TEST(hidden_or_backup_file) {
+ assert_se(hidden_or_backup_file(".hidden"));
+ assert_se(hidden_or_backup_file("..hidden"));
+ assert_se(!hidden_or_backup_file("hidden."));
+
+ assert_se(hidden_or_backup_file("backup~"));
+ assert_se(hidden_or_backup_file(".backup~"));
+
+ assert_se(hidden_or_backup_file("lost+found"));
+ assert_se(hidden_or_backup_file("aquota.user"));
+ assert_se(hidden_or_backup_file("aquota.group"));
+
+ assert_se(hidden_or_backup_file("test.rpmnew"));
+ assert_se(hidden_or_backup_file("test.dpkg-old"));
+ assert_se(hidden_or_backup_file("test.dpkg-remove"));
+ assert_se(hidden_or_backup_file("test.swp"));
+
+ assert_se(!hidden_or_backup_file("test.rpmnew."));
+ assert_se(!hidden_or_backup_file("test.dpkg-old.foo"));
+}
+
+TEST(skip_dev_prefix) {
+ assert_se(streq(skip_dev_prefix("/"), "/"));
+ assert_se(streq(skip_dev_prefix("/dev"), ""));
+ assert_se(streq(skip_dev_prefix("/dev/"), ""));
+ assert_se(streq(skip_dev_prefix("/dev/foo"), "foo"));
+ assert_se(streq(skip_dev_prefix("/dev/foo/bar"), "foo/bar"));
+ assert_se(streq(skip_dev_prefix("//dev"), ""));
+ assert_se(streq(skip_dev_prefix("//dev//"), ""));
+ assert_se(streq(skip_dev_prefix("/dev///foo"), "foo"));
+ assert_se(streq(skip_dev_prefix("///dev///foo///bar"), "foo///bar"));
+ assert_se(streq(skip_dev_prefix("//foo"), "//foo"));
+ assert_se(streq(skip_dev_prefix("foo"), "foo"));
+}
+
+TEST(empty_or_root) {
+ assert_se(empty_or_root(NULL));
+ assert_se(empty_or_root(""));
+ assert_se(empty_or_root("/"));
+ assert_se(empty_or_root("//"));
+ assert_se(empty_or_root("///"));
+ assert_se(empty_or_root("/////////////////"));
+ assert_se(!empty_or_root("xxx"));
+ assert_se(!empty_or_root("/xxx"));
+ assert_se(!empty_or_root("/xxx/"));
+ assert_se(!empty_or_root("//yy//"));
+}
+
+TEST(path_startswith_set) {
+ assert_se(streq_ptr(PATH_STARTSWITH_SET("/foo/bar", "/foo/quux", "/foo/bar", "/zzz"), ""));
+ assert_se(streq_ptr(PATH_STARTSWITH_SET("/foo/bar", "/foo/quux", "/foo/", "/zzz"), "bar"));
+ assert_se(streq_ptr(PATH_STARTSWITH_SET("/foo/bar", "/foo/quux", "/foo", "/zzz"), "bar"));
+ assert_se(streq_ptr(PATH_STARTSWITH_SET("/foo/bar", "/foo/quux", "/", "/zzz"), "foo/bar"));
+ assert_se(streq_ptr(PATH_STARTSWITH_SET("/foo/bar", "/foo/quux", "", "/zzz"), NULL));
+
+ assert_se(streq_ptr(PATH_STARTSWITH_SET("/foo/bar2", "/foo/quux", "/foo/bar", "/zzz"), NULL));
+ assert_se(streq_ptr(PATH_STARTSWITH_SET("/foo/bar2", "/foo/quux", "/foo/", "/zzz"), "bar2"));
+ assert_se(streq_ptr(PATH_STARTSWITH_SET("/foo/bar2", "/foo/quux", "/foo", "/zzz"), "bar2"));
+ assert_se(streq_ptr(PATH_STARTSWITH_SET("/foo/bar2", "/foo/quux", "/", "/zzz"), "foo/bar2"));
+ assert_se(streq_ptr(PATH_STARTSWITH_SET("/foo/bar2", "/foo/quux", "", "/zzz"), NULL));
+
+ assert_se(streq_ptr(PATH_STARTSWITH_SET("/foo2/bar", "/foo/quux", "/foo/bar", "/zzz"), NULL));
+ assert_se(streq_ptr(PATH_STARTSWITH_SET("/foo2/bar", "/foo/quux", "/foo/", "/zzz"), NULL));
+ assert_se(streq_ptr(PATH_STARTSWITH_SET("/foo2/bar", "/foo/quux", "/foo", "/zzz"), NULL));
+ assert_se(streq_ptr(PATH_STARTSWITH_SET("/foo2/bar", "/foo/quux", "/", "/zzz"), "foo2/bar"));
+ assert_se(streq_ptr(PATH_STARTSWITH_SET("/foo2/bar", "/foo/quux", "", "/zzz"), NULL));
+}
+
+TEST(path_startswith_strv) {
+ assert_se(streq_ptr(path_startswith_strv("/foo/bar", STRV_MAKE("/foo/quux", "/foo/bar", "/zzz")), ""));
+ assert_se(streq_ptr(path_startswith_strv("/foo/bar", STRV_MAKE("/foo/quux", "/foo/", "/zzz")), "bar"));
+ assert_se(streq_ptr(path_startswith_strv("/foo/bar", STRV_MAKE("/foo/quux", "/foo", "/zzz")), "bar"));
+ assert_se(streq_ptr(path_startswith_strv("/foo/bar", STRV_MAKE("/foo/quux", "/", "/zzz")), "foo/bar"));
+ assert_se(streq_ptr(path_startswith_strv("/foo/bar", STRV_MAKE("/foo/quux", "", "/zzz")), NULL));
+
+ assert_se(streq_ptr(path_startswith_strv("/foo/bar2", STRV_MAKE("/foo/quux", "/foo/bar", "/zzz")), NULL));
+ assert_se(streq_ptr(path_startswith_strv("/foo/bar2", STRV_MAKE("/foo/quux", "/foo/", "/zzz")), "bar2"));
+ assert_se(streq_ptr(path_startswith_strv("/foo/bar2", STRV_MAKE("/foo/quux", "/foo", "/zzz")), "bar2"));
+ assert_se(streq_ptr(path_startswith_strv("/foo/bar2", STRV_MAKE("/foo/quux", "/", "/zzz")), "foo/bar2"));
+ assert_se(streq_ptr(path_startswith_strv("/foo/bar2", STRV_MAKE("/foo/quux", "", "/zzz")), NULL));
+
+ assert_se(streq_ptr(path_startswith_strv("/foo2/bar", STRV_MAKE("/foo/quux", "/foo/bar", "/zzz")), NULL));
+ assert_se(streq_ptr(path_startswith_strv("/foo2/bar", STRV_MAKE("/foo/quux", "/foo/", "/zzz")), NULL));
+ assert_se(streq_ptr(path_startswith_strv("/foo2/bar", STRV_MAKE("/foo/quux", "/foo", "/zzz")), NULL));
+ assert_se(streq_ptr(path_startswith_strv("/foo2/bar", STRV_MAKE("/foo/quux", "/", "/zzz")), "foo2/bar"));
+ assert_se(streq_ptr(path_startswith_strv("/foo2/bar", STRV_MAKE("/foo/quux", "", "/zzz")), NULL));
+}
+
+static void test_path_glob_can_match_one(const char *pattern, const char *prefix, const char *expected) {
+ _cleanup_free_ char *result = NULL;
+
+ log_debug("%s(%s, %s, %s)", __func__, pattern, prefix, strnull(expected));
+
+ assert_se(path_glob_can_match(pattern, prefix, &result) == !!expected);
+ assert_se(streq_ptr(result, expected));
+}
+
+TEST(path_glob_can_match) {
+ test_path_glob_can_match_one("/foo/hoge/aaa", "/foo/hoge/aaa/bbb", NULL);
+ test_path_glob_can_match_one("/foo/hoge/aaa", "/foo/hoge/aaa", "/foo/hoge/aaa");
+ test_path_glob_can_match_one("/foo/hoge/aaa", "/foo/hoge", "/foo/hoge/aaa");
+ test_path_glob_can_match_one("/foo/hoge/aaa", "/foo", "/foo/hoge/aaa");
+ test_path_glob_can_match_one("/foo/hoge/aaa", "/", "/foo/hoge/aaa");
+
+ test_path_glob_can_match_one("/foo/*/aaa", "/foo/hoge/aaa/bbb", NULL);
+ test_path_glob_can_match_one("/foo/*/aaa", "/foo/hoge/aaa", "/foo/hoge/aaa");
+ test_path_glob_can_match_one("/foo/*/aaa", "/foo/hoge", "/foo/hoge/aaa");
+ test_path_glob_can_match_one("/foo/*/aaa", "/foo", "/foo/*/aaa");
+ test_path_glob_can_match_one("/foo/*/aaa", "/", "/foo/*/aaa");
+
+ test_path_glob_can_match_one("/foo/*/*/aaa", "/foo/xxx/yyy/aaa/bbb", NULL);
+ test_path_glob_can_match_one("/foo/*/*/aaa", "/foo/xxx/yyy/aaa", "/foo/xxx/yyy/aaa");
+ test_path_glob_can_match_one("/foo/*/*/aaa", "/foo/xxx/yyy", "/foo/xxx/yyy/aaa");
+ test_path_glob_can_match_one("/foo/*/*/aaa", "/foo/xxx", "/foo/xxx/*/aaa");
+ test_path_glob_can_match_one("/foo/*/*/aaa", "/foo", "/foo/*/*/aaa");
+ test_path_glob_can_match_one("/foo/*/*/aaa", "/", "/foo/*/*/aaa");
+
+ test_path_glob_can_match_one("/foo/*/aaa/*", "/foo/xxx/aaa/bbb/ccc", NULL);
+ test_path_glob_can_match_one("/foo/*/aaa/*", "/foo/xxx/aaa/bbb", "/foo/xxx/aaa/bbb");
+ test_path_glob_can_match_one("/foo/*/aaa/*", "/foo/xxx/ccc", NULL);
+ test_path_glob_can_match_one("/foo/*/aaa/*", "/foo/xxx/aaa", "/foo/xxx/aaa/*");
+ test_path_glob_can_match_one("/foo/*/aaa/*", "/foo/xxx", "/foo/xxx/aaa/*");
+ test_path_glob_can_match_one("/foo/*/aaa/*", "/foo", "/foo/*/aaa/*");
+ test_path_glob_can_match_one("/foo/*/aaa/*", "/", "/foo/*/aaa/*");
+}
+
+TEST(print_MAX) {
+ log_info("PATH_MAX=%zu\n"
+ "FILENAME_MAX=%zu\n"
+ "NAME_MAX=%zu",
+ (size_t) PATH_MAX,
+ (size_t) FILENAME_MAX,
+ (size_t) NAME_MAX);
+
+ assert_cc(FILENAME_MAX == PATH_MAX);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-path.c b/src/test/test-path.c
new file mode 100644
index 0000000..22ed88f
--- /dev/null
+++ b/src/test/test-path.c
@@ -0,0 +1,418 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdbool.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "alloc-util.h"
+#include "all-units.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "macro.h"
+#include "manager.h"
+#include "mkdir.h"
+#include "path-util.h"
+#include "rm-rf.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "unit.h"
+
+typedef void (*test_function_t)(Manager *m);
+
+static int setup_test(Manager **m) {
+ char **tests_path = STRV_MAKE("exists", "existsglobFOOBAR", "changed", "modified", "unit",
+ "directorynotempty", "makedirectory");
+ Manager *tmp = NULL;
+ int r;
+
+ assert_se(m);
+
+ r = enter_cgroup_subroot(NULL);
+ if (r == -ENOMEDIUM)
+ return log_tests_skipped("cgroupfs not available");
+
+ r = manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_BASIC, &tmp);
+ if (manager_errno_skip_test(r))
+ return log_tests_skipped_errno(r, "manager_new");
+ assert_se(r >= 0);
+ assert_se(manager_startup(tmp, NULL, NULL, NULL) >= 0);
+
+ STRV_FOREACH(test_path, tests_path) {
+ _cleanup_free_ char *p = NULL;
+
+ p = strjoin("/tmp/test-path_", *test_path);
+ assert_se(p);
+
+ (void) rm_rf(p, REMOVE_ROOT|REMOVE_PHYSICAL);
+ }
+
+ *m = tmp;
+
+ return 0;
+}
+
+static void shutdown_test(Manager *m) {
+ assert_se(m);
+
+ manager_free(m);
+}
+
+static Service *service_for_path(Manager *m, Path *path, const char *service_name) {
+ _cleanup_free_ char *tmp = NULL;
+ Unit *service_unit = NULL;
+
+ assert_se(m);
+ assert_se(path);
+
+ if (!service_name) {
+ assert_se(tmp = strreplace(UNIT(path)->id, ".path", ".service"));
+ service_unit = manager_get_unit(m, tmp);
+ } else
+ service_unit = manager_get_unit(m, service_name);
+ assert_se(service_unit);
+
+ return SERVICE(service_unit);
+}
+
+static int _check_states(unsigned line,
+ Manager *m, Path *path, Service *service, PathState path_state, ServiceState service_state) {
+ assert_se(m);
+ assert_se(service);
+
+ usec_t end = now(CLOCK_MONOTONIC) + 30 * USEC_PER_SEC;
+
+ while (path->state != path_state || service->state != service_state ||
+ path->result != PATH_SUCCESS || service->result != SERVICE_SUCCESS) {
+
+ assert_se(sd_event_run(m->event, 100 * USEC_PER_MSEC) >= 0);
+
+ usec_t n = now(CLOCK_MONOTONIC);
+ log_info("line %u: %s: state = %s; result = %s (left: %" PRIi64 ")",
+ line,
+ UNIT(path)->id,
+ path_state_to_string(path->state),
+ path_result_to_string(path->result),
+ (int64_t) (end - n));
+ log_info("line %u: %s: state = %s; result = %s",
+ line,
+ UNIT(service)->id,
+ service_state_to_string(service->state),
+ service_result_to_string(service->result));
+
+ if (service->state == SERVICE_FAILED && service->main_exec_status.status == EXIT_CGROUP) {
+ const char *ci = ci_environment();
+
+ /* On a general purpose system we may fail to start the service for reasons which are
+ * not under our control: permission limits, resource exhaustion, etc. Let's skip the
+ * test in those cases. On developer machines we require proper setup. */
+ if (!ci)
+ return log_notice_errno(SYNTHETIC_ERRNO(ECANCELED),
+ "Failed to start service %s, aborting test: %s/%s",
+ UNIT(service)->id,
+ service_state_to_string(service->state),
+ service_result_to_string(service->result));
+
+ /* On Salsa we can't setup cgroups so the unit always fails. The test checks if it
+ * can but continues if it cannot at the beginning, but on Salsa it fails here. */
+ if (streq(ci, "salsa-ci"))
+ exit(EXIT_TEST_SKIP);
+ }
+
+ if (n >= end) {
+ log_error("Test timeout when testing %s", UNIT(path)->id);
+ exit(EXIT_FAILURE);
+ }
+ }
+
+ return 0;
+}
+#define check_states(...) _check_states(__LINE__, __VA_ARGS__)
+
+static void test_path_exists(Manager *m) {
+ const char *test_path = "/tmp/test-path_exists";
+ Unit *unit = NULL;
+ Path *path = NULL;
+ Service *service = NULL;
+
+ assert_se(m);
+
+ assert_se(manager_load_startable_unit_or_warn(m, "path-exists.path", NULL, &unit) >= 0);
+
+ path = PATH(unit);
+ service = service_for_path(m, path, NULL);
+
+ assert_se(unit_start(unit, NULL) >= 0);
+ if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
+ return;
+
+ assert_se(touch(test_path) >= 0);
+ if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
+ return;
+
+ /* Service restarts if file still exists */
+ assert_se(unit_stop(UNIT(service)) >= 0);
+ if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
+ return;
+
+ assert_se(rm_rf(test_path, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
+ assert_se(unit_stop(UNIT(service)) >= 0);
+ if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
+ return;
+
+ assert_se(unit_stop(unit) >= 0);
+}
+
+static void test_path_existsglob(Manager *m) {
+ const char *test_path = "/tmp/test-path_existsglobFOOBAR";
+ Unit *unit = NULL;
+ Path *path = NULL;
+ Service *service = NULL;
+
+ assert_se(m);
+
+ assert_se(manager_load_startable_unit_or_warn(m, "path-existsglob.path", NULL, &unit) >= 0);
+
+ path = PATH(unit);
+ service = service_for_path(m, path, NULL);
+
+ assert_se(unit_start(unit, NULL) >= 0);
+ if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
+ return;
+
+ assert_se(touch(test_path) >= 0);
+ if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
+ return;
+
+ /* Service restarts if file still exists */
+ assert_se(unit_stop(UNIT(service)) >= 0);
+ if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
+ return;
+
+ assert_se(rm_rf(test_path, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
+ assert_se(unit_stop(UNIT(service)) >= 0);
+ if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
+ return;
+
+ assert_se(unit_stop(unit) >= 0);
+}
+
+static void test_path_changed(Manager *m) {
+ const char *test_path = "/tmp/test-path_changed";
+ FILE *f;
+ Unit *unit = NULL;
+ Path *path = NULL;
+ Service *service = NULL;
+
+ assert_se(m);
+
+ assert_se(manager_load_startable_unit_or_warn(m, "path-changed.path", NULL, &unit) >= 0);
+
+ path = PATH(unit);
+ service = service_for_path(m, path, NULL);
+
+ assert_se(unit_start(unit, NULL) >= 0);
+ if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
+ return;
+
+ assert_se(touch(test_path) >= 0);
+ if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
+ return;
+
+ /* Service does not restart if file still exists */
+ assert_se(unit_stop(UNIT(service)) >= 0);
+ if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
+ return;
+
+ f = fopen(test_path, "w");
+ assert_se(f);
+ fclose(f);
+
+ if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
+ return;
+
+ assert_se(unit_stop(UNIT(service)) >= 0);
+ if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
+ return;
+
+ (void) rm_rf(test_path, REMOVE_ROOT|REMOVE_PHYSICAL);
+ assert_se(unit_stop(unit) >= 0);
+}
+
+static void test_path_modified(Manager *m) {
+ _cleanup_fclose_ FILE *f = NULL;
+ const char *test_path = "/tmp/test-path_modified";
+ Unit *unit = NULL;
+ Path *path = NULL;
+ Service *service = NULL;
+
+ assert_se(m);
+
+ assert_se(manager_load_startable_unit_or_warn(m, "path-modified.path", NULL, &unit) >= 0);
+
+ path = PATH(unit);
+ service = service_for_path(m, path, NULL);
+
+ assert_se(unit_start(unit, NULL) >= 0);
+ if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
+ return;
+
+ assert_se(touch(test_path) >= 0);
+ if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
+ return;
+
+ /* Service does not restart if file still exists */
+ assert_se(unit_stop(UNIT(service)) >= 0);
+ if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
+ return;
+
+ f = fopen(test_path, "w");
+ assert_se(f);
+ fputs("test", f);
+
+ if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
+ return;
+
+ assert_se(unit_stop(UNIT(service)) >= 0);
+ if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
+ return;
+
+ (void) rm_rf(test_path, REMOVE_ROOT|REMOVE_PHYSICAL);
+ assert_se(unit_stop(unit) >= 0);
+}
+
+static void test_path_unit(Manager *m) {
+ const char *test_path = "/tmp/test-path_unit";
+ Unit *unit = NULL;
+ Path *path = NULL;
+ Service *service = NULL;
+
+ assert_se(m);
+
+ assert_se(manager_load_startable_unit_or_warn(m, "path-unit.path", NULL, &unit) >= 0);
+
+ path = PATH(unit);
+ service = service_for_path(m, path, "path-mycustomunit.service");
+
+ assert_se(unit_start(unit, NULL) >= 0);
+ if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
+ return;
+
+ assert_se(touch(test_path) >= 0);
+ if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
+ return;
+
+ assert_se(rm_rf(test_path, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
+ assert_se(unit_stop(UNIT(service)) >= 0);
+ if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
+ return;
+
+ assert_se(unit_stop(unit) >= 0);
+}
+
+static void test_path_directorynotempty(Manager *m) {
+ const char *test_file, *test_path = "/tmp/test-path_directorynotempty/";
+ Unit *unit = NULL;
+ Path *path = NULL;
+ Service *service = NULL;
+
+ assert_se(m);
+
+ assert_se(manager_load_startable_unit_or_warn(m, "path-directorynotempty.path", NULL, &unit) >= 0);
+
+ path = PATH(unit);
+ service = service_for_path(m, path, NULL);
+
+ assert_se(access(test_path, F_OK) < 0);
+
+ assert_se(unit_start(unit, NULL) >= 0);
+ if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
+ return;
+
+ /* MakeDirectory default to no */
+ assert_se(access(test_path, F_OK) < 0);
+
+ assert_se(mkdir_p(test_path, 0755) >= 0);
+ test_file = strjoina(test_path, "test_file");
+ assert_se(touch(test_file) >= 0);
+ if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
+ return;
+
+ /* Service restarts if directory is still not empty */
+ assert_se(unit_stop(UNIT(service)) >= 0);
+ if (check_states(m, path, service, PATH_RUNNING, SERVICE_RUNNING) < 0)
+ return;
+
+ assert_se(rm_rf(test_path, REMOVE_ROOT|REMOVE_PHYSICAL) == 0);
+ assert_se(unit_stop(UNIT(service)) >= 0);
+ if (check_states(m, path, service, PATH_WAITING, SERVICE_DEAD) < 0)
+ return;
+
+ assert_se(unit_stop(unit) >= 0);
+}
+
+static void test_path_makedirectory_directorymode(Manager *m) {
+ const char *test_path = "/tmp/test-path_makedirectory/";
+ Unit *unit = NULL;
+ struct stat s;
+
+ assert_se(m);
+
+ assert_se(manager_load_startable_unit_or_warn(m, "path-makedirectory.path", NULL, &unit) >= 0);
+
+ assert_se(access(test_path, F_OK) < 0);
+
+ assert_se(unit_start(unit, NULL) >= 0);
+
+ /* Check if the directory has been created */
+ assert_se(access(test_path, F_OK) >= 0);
+
+ /* Check the mode we specified with DirectoryMode=0744 */
+ assert_se(stat(test_path, &s) >= 0);
+ assert_se((s.st_mode & S_IRWXU) == 0700);
+ assert_se((s.st_mode & S_IRWXG) == 0040);
+ assert_se((s.st_mode & S_IRWXO) == 0004);
+
+ assert_se(unit_stop(unit) >= 0);
+ (void) rm_rf(test_path, REMOVE_ROOT|REMOVE_PHYSICAL);
+}
+
+int main(int argc, char *argv[]) {
+ static const test_function_t tests[] = {
+ test_path_exists,
+ test_path_existsglob,
+ test_path_changed,
+ test_path_modified,
+ test_path_unit,
+ test_path_directorynotempty,
+ test_path_makedirectory_directorymode,
+ NULL,
+ };
+
+ _cleanup_free_ char *test_path = NULL;
+ _cleanup_(rm_rf_physical_and_freep) char *runtime_dir = NULL;
+
+ umask(022);
+
+ test_setup_logging(LOG_INFO);
+
+ assert_se(get_testdata_dir("test-path", &test_path) >= 0);
+ assert_se(set_unit_path(test_path) >= 0);
+ assert_se(runtime_dir = setup_fake_runtime_dir());
+
+ for (const test_function_t *test = tests; *test; test++) {
+ Manager *m = NULL;
+ int r;
+
+ /* We create a clean environment for each test */
+ r = setup_test(&m);
+ if (r != 0)
+ return r;
+
+ (*test)(m);
+
+ shutdown_test(m);
+ }
+
+ return 0;
+}
diff --git a/src/test/test-percent-util.c b/src/test/test-percent-util.c
new file mode 100644
index 0000000..7e8e11b
--- /dev/null
+++ b/src/test/test-percent-util.c
@@ -0,0 +1,199 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "percent-util.h"
+#include "tests.h"
+#include "time-util.h"
+
+TEST(parse_percent) {
+ assert_se(parse_percent("") == -EINVAL);
+ assert_se(parse_percent("foo") == -EINVAL);
+ assert_se(parse_percent("0") == -EINVAL);
+ assert_se(parse_percent("0.1") == -EINVAL);
+ assert_se(parse_percent("50") == -EINVAL);
+ assert_se(parse_percent("100") == -EINVAL);
+ assert_se(parse_percent("-1") == -EINVAL);
+ assert_se(parse_percent("0%") == 0);
+ assert_se(parse_percent("55%") == 55);
+ assert_se(parse_percent("100%") == 100);
+ assert_se(parse_percent("-7%") == -ERANGE);
+ assert_se(parse_percent("107%") == -ERANGE);
+ assert_se(parse_percent("%") == -EINVAL);
+ assert_se(parse_percent("%%") == -EINVAL);
+ assert_se(parse_percent("%1") == -EINVAL);
+ assert_se(parse_percent("1%%") == -EINVAL);
+ assert_se(parse_percent("3.2%") == -EINVAL);
+}
+
+TEST(parse_percent_unbounded) {
+ assert_se(parse_percent_unbounded("101%") == 101);
+ assert_se(parse_percent_unbounded("400%") == 400);
+}
+
+TEST(parse_permille) {
+ assert_se(parse_permille("") == -EINVAL);
+ assert_se(parse_permille("foo") == -EINVAL);
+ assert_se(parse_permille("0") == -EINVAL);
+ assert_se(parse_permille("50") == -EINVAL);
+ assert_se(parse_permille("100") == -EINVAL);
+ assert_se(parse_permille("-1") == -EINVAL);
+ assert_se(parse_permille("0.1") == -EINVAL);
+ assert_se(parse_permille("5%") == 50);
+ assert_se(parse_permille("5.5%") == 55);
+ assert_se(parse_permille("5.12%") == -EINVAL);
+
+ assert_se(parse_permille("0‰") == 0);
+ assert_se(parse_permille("555‰") == 555);
+ assert_se(parse_permille("1000‰") == 1000);
+ assert_se(parse_permille("-7‰") == -ERANGE);
+ assert_se(parse_permille("1007‰") == -ERANGE);
+ assert_se(parse_permille("‰") == -EINVAL);
+ assert_se(parse_permille("‰‰") == -EINVAL);
+ assert_se(parse_permille("‰1") == -EINVAL);
+ assert_se(parse_permille("1‰‰") == -EINVAL);
+ assert_se(parse_permille("3.2‰") == -EINVAL);
+ assert_se(parse_permille("0.1‰") == -EINVAL);
+
+ assert_se(parse_permille("0%") == 0);
+ assert_se(parse_permille("55%") == 550);
+ assert_se(parse_permille("55.5%") == 555);
+ assert_se(parse_permille("100%") == 1000);
+ assert_se(parse_permille("-7%") == -ERANGE);
+ assert_se(parse_permille("107%") == -ERANGE);
+ assert_se(parse_permille("%") == -EINVAL);
+ assert_se(parse_permille("%%") == -EINVAL);
+ assert_se(parse_permille("%1") == -EINVAL);
+ assert_se(parse_permille("1%%") == -EINVAL);
+ assert_se(parse_permille("3.21%") == -EINVAL);
+ assert_se(parse_permille("0.1%") == 1);
+}
+
+TEST(parse_permille_unbounded) {
+ assert_se(parse_permille_unbounded("1001‰") == 1001);
+ assert_se(parse_permille_unbounded("4000‰") == 4000);
+ assert_se(parse_permille_unbounded("2147483647‰") == 2147483647);
+ assert_se(parse_permille_unbounded("2147483648‰") == -ERANGE);
+ assert_se(parse_permille_unbounded("4294967295‰") == -ERANGE);
+ assert_se(parse_permille_unbounded("4294967296‰") == -ERANGE);
+
+ assert_se(parse_permille_unbounded("101%") == 1010);
+ assert_se(parse_permille_unbounded("400%") == 4000);
+ assert_se(parse_permille_unbounded("214748364.7%") == 2147483647);
+ assert_se(parse_permille_unbounded("214748364.8%") == -ERANGE);
+ assert_se(parse_permille_unbounded("429496729.5%") == -ERANGE);
+ assert_se(parse_permille_unbounded("429496729.6%") == -ERANGE);
+}
+
+TEST(parse_permyriad) {
+ assert_se(parse_permyriad("") == -EINVAL);
+ assert_se(parse_permyriad("foo") == -EINVAL);
+ assert_se(parse_permyriad("0") == -EINVAL);
+ assert_se(parse_permyriad("50") == -EINVAL);
+ assert_se(parse_permyriad("100") == -EINVAL);
+ assert_se(parse_permyriad("-1") == -EINVAL);
+
+ assert_se(parse_permyriad("0‱") == 0);
+ assert_se(parse_permyriad("555‱") == 555);
+ assert_se(parse_permyriad("1000‱") == 1000);
+ assert_se(parse_permyriad("-7‱") == -ERANGE);
+ assert_se(parse_permyriad("10007‱") == -ERANGE);
+ assert_se(parse_permyriad("‱") == -EINVAL);
+ assert_se(parse_permyriad("‱‱") == -EINVAL);
+ assert_se(parse_permyriad("‱1") == -EINVAL);
+ assert_se(parse_permyriad("1‱‱") == -EINVAL);
+ assert_se(parse_permyriad("3.2‱") == -EINVAL);
+
+ assert_se(parse_permyriad("0‰") == 0);
+ assert_se(parse_permyriad("555.5‰") == 5555);
+ assert_se(parse_permyriad("1000.0‰") == 10000);
+ assert_se(parse_permyriad("-7‰") == -ERANGE);
+ assert_se(parse_permyriad("1007‰") == -ERANGE);
+ assert_se(parse_permyriad("‰") == -EINVAL);
+ assert_se(parse_permyriad("‰‰") == -EINVAL);
+ assert_se(parse_permyriad("‰1") == -EINVAL);
+ assert_se(parse_permyriad("1‰‰") == -EINVAL);
+ assert_se(parse_permyriad("3.22‰") == -EINVAL);
+
+ assert_se(parse_permyriad("0%") == 0);
+ assert_se(parse_permyriad("55%") == 5500);
+ assert_se(parse_permyriad("55.5%") == 5550);
+ assert_se(parse_permyriad("55.50%") == 5550);
+ assert_se(parse_permyriad("55.53%") == 5553);
+ assert_se(parse_permyriad("100%") == 10000);
+ assert_se(parse_permyriad("-7%") == -ERANGE);
+ assert_se(parse_permyriad("107%") == -ERANGE);
+ assert_se(parse_permyriad("%") == -EINVAL);
+ assert_se(parse_permyriad("%%") == -EINVAL);
+ assert_se(parse_permyriad("%1") == -EINVAL);
+ assert_se(parse_permyriad("1%%") == -EINVAL);
+ assert_se(parse_permyriad("3.212%") == -EINVAL);
+}
+
+TEST(parse_permyriad_unbounded) {
+ assert_se(parse_permyriad_unbounded("1001‱") == 1001);
+ assert_se(parse_permyriad_unbounded("4000‱") == 4000);
+ assert_se(parse_permyriad_unbounded("2147483647‱") == 2147483647);
+ assert_se(parse_permyriad_unbounded("2147483648‱") == -ERANGE);
+ assert_se(parse_permyriad_unbounded("4294967295‱") == -ERANGE);
+ assert_se(parse_permyriad_unbounded("4294967296‱") == -ERANGE);
+
+ assert_se(parse_permyriad_unbounded("101‰") == 1010);
+ assert_se(parse_permyriad_unbounded("400‰") == 4000);
+ assert_se(parse_permyriad_unbounded("214748364.7‰") == 2147483647);
+ assert_se(parse_permyriad_unbounded("214748364.8‰") == -ERANGE);
+ assert_se(parse_permyriad_unbounded("429496729.5‰") == -ERANGE);
+ assert_se(parse_permyriad_unbounded("429496729.6‰") == -ERANGE);
+
+ assert_se(parse_permyriad_unbounded("99%") == 9900);
+ assert_se(parse_permyriad_unbounded("40%") == 4000);
+ assert_se(parse_permyriad_unbounded("21474836.47%") == 2147483647);
+ assert_se(parse_permyriad_unbounded("21474836.48%") == -ERANGE);
+ assert_se(parse_permyriad_unbounded("42949672.95%") == -ERANGE);
+ assert_se(parse_permyriad_unbounded("42949672.96%") == -ERANGE);
+}
+
+TEST(scale) {
+ /* Check some fixed values */
+ assert_se(UINT32_SCALE_FROM_PERCENT(0) == 0);
+ assert_se(UINT32_SCALE_FROM_PERCENT(50) == UINT32_MAX/2+1);
+ assert_se(UINT32_SCALE_FROM_PERCENT(100) == UINT32_MAX);
+
+ assert_se(UINT32_SCALE_FROM_PERMILLE(0) == 0);
+ assert_se(UINT32_SCALE_FROM_PERMILLE(500) == UINT32_MAX/2+1);
+ assert_se(UINT32_SCALE_FROM_PERMILLE(1000) == UINT32_MAX);
+
+ assert_se(UINT32_SCALE_FROM_PERMYRIAD(0) == 0);
+ assert_se(UINT32_SCALE_FROM_PERMYRIAD(5000) == UINT32_MAX/2+1);
+ assert_se(UINT32_SCALE_FROM_PERMYRIAD(10000) == UINT32_MAX);
+
+ /* Make sure there's no numeric noise on the 0%…100% scale when converting from percent and back. */
+ for (int percent = 0; percent <= 100; percent++) {
+ log_debug("%i%% → %" PRIu32 " → %i%%",
+ percent,
+ UINT32_SCALE_FROM_PERCENT(percent),
+ UINT32_SCALE_TO_PERCENT(UINT32_SCALE_FROM_PERCENT(percent)));
+
+ assert_se(UINT32_SCALE_TO_PERCENT(UINT32_SCALE_FROM_PERCENT(percent)) == percent);
+ }
+
+ /* Make sure there's no numeric noise on the 0‰…1000‰ scale when converting from permille and back. */
+ for (int permille = 0; permille <= 1000; permille++) {
+ log_debug("%i‰ → %" PRIu32 " → %i‰",
+ permille,
+ UINT32_SCALE_FROM_PERMILLE(permille),
+ UINT32_SCALE_TO_PERMILLE(UINT32_SCALE_FROM_PERMILLE(permille)));
+
+ assert_se(UINT32_SCALE_TO_PERMILLE(UINT32_SCALE_FROM_PERMILLE(permille)) == permille);
+ }
+
+ /* Make sure there's no numeric noise on the 0‱…10000‱ scale when converting from permyriad and back. */
+ for (int permyriad = 0; permyriad <= 10000; permyriad++) {
+ log_debug("%i‱ → %" PRIu32 " → %i‱",
+ permyriad,
+ UINT32_SCALE_FROM_PERMYRIAD(permyriad),
+ UINT32_SCALE_TO_PERMYRIAD(UINT32_SCALE_FROM_PERMYRIAD(permyriad)));
+
+ assert_se(UINT32_SCALE_TO_PERMYRIAD(UINT32_SCALE_FROM_PERMYRIAD(permyriad)) == permyriad);
+ }
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-pretty-print.c b/src/test/test-pretty-print.c
new file mode 100644
index 0000000..52b2bc8
--- /dev/null
+++ b/src/test/test-pretty-print.c
@@ -0,0 +1,79 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "macro.h"
+#include "pretty-print.h"
+#include "strv.h"
+#include "tests.h"
+
+#define CYLON_WIDTH 6
+
+static void test_draw_cylon_one(unsigned pos) {
+ char buf[CYLON_WIDTH + CYLON_BUFFER_EXTRA + 1];
+
+ log_debug("/* %s(%u) */", __func__, pos);
+
+ assert(pos <= CYLON_WIDTH + 1);
+
+ memset(buf, 0xff, sizeof(buf));
+ draw_cylon(buf, sizeof(buf), CYLON_WIDTH, pos);
+ assert_se(strlen(buf) < sizeof(buf));
+}
+
+TEST(draw_cylon) {
+ bool saved = log_get_show_color();
+
+ log_show_color(false);
+ for (unsigned i = 0; i <= CYLON_WIDTH + 1; i++)
+ test_draw_cylon_one(i);
+
+ log_show_color(true);
+ for (unsigned i = 0; i <= CYLON_WIDTH + 1; i++)
+ test_draw_cylon_one(i);
+
+ log_show_color(saved);
+}
+
+TEST(terminal_urlify) {
+ _cleanup_free_ char *formatted = NULL;
+
+ assert_se(terminal_urlify("https://www.freedesktop.org/wiki/Software/systemd", "systemd homepage", &formatted) >= 0);
+ printf("Hey, consider visiting the %s right now! It is very good!\n", formatted);
+
+ formatted = mfree(formatted);
+
+ assert_se(terminal_urlify_path("/etc/fstab", "this link to your /etc/fstab", &formatted) >= 0);
+ printf("Or click on %s to have a look at it!\n", formatted);
+}
+
+TEST(cat_files) {
+ assert_se(cat_files("/no/such/file", NULL, 0) == -ENOENT);
+ assert_se(cat_files(NULL, NULL, 0) == 0);
+
+ if (access("/etc/fstab", R_OK) >= 0)
+ assert_se(cat_files("/etc/fstab", STRV_MAKE("/etc/fstab", "/etc/fstab"), 0) == 0);
+}
+
+TEST(red_green_cross_check_mark) {
+ bool b = false;
+
+ printf("yea: <%s>\n", GREEN_CHECK_MARK());
+ printf("nay: <%s>\n", RED_CROSS_MARK());
+
+ printf("%s → %s → %s → %s\n",
+ COLOR_MARK_BOOL(b),
+ COLOR_MARK_BOOL(!b),
+ COLOR_MARK_BOOL(!!b),
+ COLOR_MARK_BOOL(!!!b));
+}
+
+TEST(print_separator) {
+ print_separator();
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-prioq.c b/src/test/test-prioq.c
new file mode 100644
index 0000000..540863c
--- /dev/null
+++ b/src/test/test-prioq.c
@@ -0,0 +1,123 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdlib.h>
+
+#include "alloc-util.h"
+#include "prioq.h"
+#include "set.h"
+#include "siphash24.h"
+#include "sort-util.h"
+#include "tests.h"
+
+#define SET_SIZE 1024*4
+
+static int unsigned_compare(const unsigned *a, const unsigned *b) {
+ return CMP(*a, *b);
+}
+
+TEST(unsigned) {
+ _cleanup_(prioq_freep) Prioq *q = NULL;
+ unsigned buffer[SET_SIZE], i, u, n;
+
+ srand(0);
+
+ assert_se(q = prioq_new(trivial_compare_func));
+
+ for (i = 0; i < ELEMENTSOF(buffer); i++) {
+ u = (unsigned) rand();
+ buffer[i] = u;
+ assert_se(prioq_put(q, UINT_TO_PTR(u), NULL) >= 0);
+
+ n = prioq_size(q);
+ assert_se(prioq_remove(q, UINT_TO_PTR(u), &n) == 0);
+ }
+
+ typesafe_qsort(buffer, ELEMENTSOF(buffer), unsigned_compare);
+
+ for (i = 0; i < ELEMENTSOF(buffer); i++) {
+ assert_se(prioq_size(q) == ELEMENTSOF(buffer) - i);
+
+ u = PTR_TO_UINT(prioq_pop(q));
+ assert_se(buffer[i] == u);
+ }
+
+ assert_se(prioq_isempty(q));
+}
+
+struct test {
+ unsigned value;
+ unsigned idx;
+};
+
+static int test_compare(const struct test *x, const struct test *y) {
+ return CMP(x->value, y->value);
+}
+
+static void test_hash(const struct test *x, struct siphash *state) {
+ siphash24_compress(&x->value, sizeof(x->value), state);
+}
+
+DEFINE_PRIVATE_HASH_OPS(test_hash_ops, struct test, test_hash, test_compare);
+
+TEST(struct) {
+ _cleanup_(prioq_freep) Prioq *q = NULL;
+ _cleanup_set_free_ Set *s = NULL;
+ unsigned previous = 0, i;
+ struct test *t;
+
+ srand(0);
+
+ assert_se(q = prioq_new((compare_func_t) test_compare));
+ assert_se(s = set_new(&test_hash_ops));
+
+ assert_se(prioq_peek(q) == NULL);
+ assert_se(prioq_peek_by_index(q, 0) == NULL);
+ assert_se(prioq_peek_by_index(q, 1) == NULL);
+ assert_se(prioq_peek_by_index(q, UINT_MAX) == NULL);
+
+ for (i = 0; i < SET_SIZE; i++) {
+ assert_se(t = new0(struct test, 1));
+ t->value = (unsigned) rand();
+
+ assert_se(prioq_put(q, t, &t->idx) >= 0);
+
+ if (i % 4 == 0)
+ assert_se(set_consume(s, t) >= 0);
+ }
+
+ for (i = 0; i < SET_SIZE; i++)
+ assert_se(prioq_peek_by_index(q, i));
+ assert_se(prioq_peek_by_index(q, SET_SIZE) == NULL);
+
+ unsigned count = 0;
+ PRIOQ_FOREACH_ITEM(q, t) {
+ assert_se(t);
+ count++;
+ }
+ assert_se(count == SET_SIZE);
+
+ while ((t = set_steal_first(s))) {
+ assert_se(prioq_remove(q, t, &t->idx) == 1);
+ assert_se(prioq_remove(q, t, &t->idx) == 0);
+ assert_se(prioq_remove(q, t, NULL) == 0);
+
+ free(t);
+ }
+
+ for (i = 0; i < SET_SIZE * 3 / 4; i++) {
+ assert_se(prioq_size(q) == (SET_SIZE * 3 / 4) - i);
+
+ assert_se(t = prioq_pop(q));
+ assert_se(prioq_remove(q, t, &t->idx) == 0);
+ assert_se(prioq_remove(q, t, NULL) == 0);
+ assert_se(previous <= t->value);
+
+ previous = t->value;
+ free(t);
+ }
+
+ assert_se(prioq_isempty(q));
+ assert_se(set_isempty(s));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-proc-cmdline.c b/src/test/test-proc-cmdline.c
new file mode 100644
index 0000000..8b5bbb0
--- /dev/null
+++ b/src/test/test-proc-cmdline.c
@@ -0,0 +1,354 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "env-util.h"
+#include "errno-util.h"
+#include "initrd-util.h"
+#include "log.h"
+#include "macro.h"
+#include "nulstr-util.h"
+#include "proc-cmdline.h"
+#include "process-util.h"
+#include "special.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+
+static int obj;
+
+static int parse_item(const char *key, const char *value, void *data) {
+ assert_se(key);
+ assert_se(data == &obj);
+
+ log_info("kernel cmdline option <%s> = <%s>", key, strna(value));
+ return 0;
+}
+
+TEST(proc_cmdline_parse) {
+ assert_se(proc_cmdline_parse(parse_item, &obj, PROC_CMDLINE_STRIP_RD_PREFIX) >= 0);
+}
+
+TEST(proc_cmdline_override) {
+ _cleanup_free_ char *line = NULL, *value = NULL;
+ _cleanup_strv_free_ char **args = NULL;
+
+ assert_se(putenv((char*) "SYSTEMD_PROC_CMDLINE=foo_bar=quux wuff-piep=tuet zumm some_arg_with_space='foo bar' and_one_more=\"zzz aaa\"") == 0);
+ assert_se(putenv((char*) "SYSTEMD_EFI_OPTIONS=different") == 0);
+
+ /* First test if the overrides for /proc/cmdline still work */
+ assert_se(proc_cmdline(&line) >= 0);
+ assert_se(streq(line, "foo_bar=quux wuff-piep=tuet zumm some_arg_with_space='foo bar' and_one_more=\"zzz aaa\""));
+ line = mfree(line);
+ assert_se(proc_cmdline_strv(&args) >= 0);
+ assert_se(strv_equal(args, STRV_MAKE("foo_bar=quux", "wuff-piep=tuet", "zumm", "some_arg_with_space=foo bar", "and_one_more=zzz aaa")));
+ args = strv_free(args);
+
+ /* Test if parsing makes uses of the override */
+ assert_se(proc_cmdline_get_key("foo_bar", 0, &value) > 0 && streq_ptr(value, "quux"));
+ value = mfree(value);
+
+ assert_se(proc_cmdline_get_key("some_arg_with_space", 0, &value) > 0 && streq_ptr(value, "foo bar"));
+ value = mfree(value);
+
+ assert_se(proc_cmdline_get_key("and_one_more", 0, &value) > 0 && streq_ptr(value, "zzz aaa"));
+ value = mfree(value);
+
+ assert_se(putenv((char*) "SYSTEMD_PROC_CMDLINE=hoge") == 0);
+ assert_se(putenv((char*) "SYSTEMD_EFI_OPTIONS=foo_bar=quux wuff-piep=tuet zumm some_arg_with_space='foo bar' and_one_more=\"zzz aaa\"") == 0);
+
+ assert_se(proc_cmdline(&line) >= 0);
+ assert_se(streq(line, "hoge"));
+ line = mfree(line);
+ assert_se(proc_cmdline_strv(&args) >= 0);
+ assert_se(strv_equal(args, STRV_MAKE("hoge")));
+ args = strv_free(args);
+
+#if ENABLE_EFI
+ assert_se(proc_cmdline_get_key("foo_bar", 0, &value) > 0 && streq_ptr(value, "quux"));
+ value = mfree(value);
+
+ assert_se(proc_cmdline_get_key("some_arg_with_space", 0, &value) > 0 && streq_ptr(value, "foo bar"));
+ value = mfree(value);
+
+ assert_se(proc_cmdline_get_key("and_one_more", 0, &value) > 0 && streq_ptr(value, "zzz aaa"));
+ value = mfree(value);
+#endif
+}
+
+static int parse_item_given(const char *key, const char *value, void *data) {
+ assert_se(key);
+ assert_se(data);
+
+ bool *strip = data;
+
+ log_info("%s: option <%s> = <%s>", __func__, key, strna(value));
+ if (proc_cmdline_key_streq(key, "foo_bar"))
+ assert_se(streq(value, "quux"));
+ else if (proc_cmdline_key_streq(key, "wuff-piep"))
+ assert_se(streq(value, "tuet "));
+ else if (proc_cmdline_key_streq(key, "space"))
+ assert_se(streq(value, "x y z"));
+ else if (proc_cmdline_key_streq(key, "miepf"))
+ assert_se(streq(value, "uuu"));
+ else if (in_initrd() && *strip && proc_cmdline_key_streq(key, "zumm"))
+ assert_se(!value);
+ else if (in_initrd() && !*strip && proc_cmdline_key_streq(key, "rd.zumm"))
+ assert_se(!value);
+ else
+ assert_not_reached();
+
+ return 0;
+}
+
+static void test_proc_cmdline_given_one(bool flip_initrd) {
+ log_info("/* %s (flip: %s) */", __func__, yes_no(flip_initrd));
+
+ if (flip_initrd)
+ in_initrd_force(!in_initrd());
+
+ bool t = true, f = false;
+ assert_se(proc_cmdline_parse(parse_item_given, &t, PROC_CMDLINE_STRIP_RD_PREFIX) >= 0);
+ assert_se(proc_cmdline_parse(parse_item_given, &f, 0) >= 0);
+
+ if (flip_initrd)
+ in_initrd_force(!in_initrd());
+}
+
+TEST(proc_cmdline_given) {
+ assert_se(putenv((char*) "SYSTEMD_PROC_CMDLINE=foo_bar=quux wuff-piep=\"tuet \" rd.zumm space='x y z' miepf=\"uuu\"") == 0);
+ assert_se(putenv((char*) "SYSTEMD_EFI_OPTIONS=miepf=\"uuu\"") == 0);
+
+ test_proc_cmdline_given_one(false);
+ /* Repeat the same thing, but now flip our ininitrdness */
+ test_proc_cmdline_given_one(true);
+}
+
+TEST(proc_cmdline_get_key) {
+ _cleanup_free_ char *value = NULL;
+
+ assert_se(putenv((char*) "SYSTEMD_PROC_CMDLINE=foo_bar=quux wuff-piep=tuet zumm-ghh spaaace='ö ü ß' ticks=\"''\"\n\nkkk=uuu\n\n\n") == 0);
+
+ assert_se(proc_cmdline_get_key("", 0, &value) == -EINVAL);
+ assert_se(proc_cmdline_get_key("abc", 0, NULL) == 0);
+ assert_se(proc_cmdline_get_key("abc", 0, &value) == 0 && value == NULL);
+ assert_se(proc_cmdline_get_key("abc", PROC_CMDLINE_VALUE_OPTIONAL, &value) == 0 && value == NULL);
+
+ assert_se(proc_cmdline_get_key("foo_bar", 0, &value) > 0 && streq_ptr(value, "quux"));
+ value = mfree(value);
+ assert_se(proc_cmdline_get_key("foo_bar", PROC_CMDLINE_VALUE_OPTIONAL, &value) > 0 && streq_ptr(value, "quux"));
+ value = mfree(value);
+ assert_se(proc_cmdline_get_key("foo_bar", 0, NULL) == 0);
+ assert_se(proc_cmdline_get_key("foo-bar", 0, &value) > 0 && streq_ptr(value, "quux"));
+ value = mfree(value);
+ assert_se(proc_cmdline_get_key("foo-bar", PROC_CMDLINE_VALUE_OPTIONAL, &value) > 0 && streq_ptr(value, "quux"));
+ value = mfree(value);
+ assert_se(proc_cmdline_get_key("foo-bar", 0, NULL) == 0);
+ assert_se(proc_cmdline_get_key("foo-bar", PROC_CMDLINE_VALUE_OPTIONAL, NULL) == -EINVAL);
+
+ assert_se(proc_cmdline_get_key("wuff-piep", 0, &value) > 0 && streq_ptr(value, "tuet"));
+ value = mfree(value);
+ assert_se(proc_cmdline_get_key("wuff-piep", PROC_CMDLINE_VALUE_OPTIONAL, &value) > 0 && streq_ptr(value, "tuet"));
+ value = mfree(value);
+ assert_se(proc_cmdline_get_key("wuff_piep", 0, &value) > 0 && streq_ptr(value, "tuet"));
+ value = mfree(value);
+ assert_se(proc_cmdline_get_key("wuff_piep", PROC_CMDLINE_VALUE_OPTIONAL, &value) > 0 && streq_ptr(value, "tuet"));
+ value = mfree(value);
+ assert_se(proc_cmdline_get_key("wuff_piep", 0, NULL) == 0);
+ assert_se(proc_cmdline_get_key("wuff_piep", PROC_CMDLINE_VALUE_OPTIONAL, NULL) == -EINVAL);
+
+ assert_se(proc_cmdline_get_key("zumm-ghh", 0, &value) == 0 && value == NULL);
+ assert_se(proc_cmdline_get_key("zumm-ghh", PROC_CMDLINE_VALUE_OPTIONAL, &value) > 0 && value == NULL);
+ assert_se(proc_cmdline_get_key("zumm-ghh", 0, NULL) > 0);
+ assert_se(proc_cmdline_get_key("zumm_ghh", 0, &value) == 0 && value == NULL);
+ assert_se(proc_cmdline_get_key("zumm_ghh", PROC_CMDLINE_VALUE_OPTIONAL, &value) > 0 && value == NULL);
+ assert_se(proc_cmdline_get_key("zumm_ghh", 0, NULL) > 0);
+
+ assert_se(proc_cmdline_get_key("spaaace", 0, &value) > 0 && streq_ptr(value, "ö ü ß"));
+ value = mfree(value);
+
+ assert_se(proc_cmdline_get_key("ticks", 0, &value) > 0 && streq_ptr(value, "''"));
+ value = mfree(value);
+
+ assert_se(proc_cmdline_get_key("kkk", 0, &value) > 0 && streq_ptr(value, "uuu"));
+}
+
+TEST(proc_cmdline_get_bool) {
+ bool value = false;
+
+ assert_se(putenv((char*) "SYSTEMD_PROC_CMDLINE=foo_bar bar-waldo=1 x_y-z=0 quux=miep\nda=yes\nthe=1") == 0);
+ assert_se(putenv((char*) "SYSTEMD_EFI_OPTIONS=") == 0);
+
+ assert_se(proc_cmdline_get_bool("", /* flags = */ 0, &value) == -EINVAL);
+ assert_se(proc_cmdline_get_bool("abc", /* flags = */ 0, &value) == 0 && value == false);
+ assert_se(proc_cmdline_get_bool("unspecified", PROC_CMDLINE_TRUE_WHEN_MISSING, &value) == 0 && value == true);
+ assert_se(proc_cmdline_get_bool("foo_bar", /* flags = */ 0, &value) > 0 && value == true);
+ assert_se(proc_cmdline_get_bool("foo-bar", /* flags = */ 0, &value) > 0 && value == true);
+ assert_se(proc_cmdline_get_bool("bar-waldo", /* flags = */ 0, &value) > 0 && value == true);
+ assert_se(proc_cmdline_get_bool("bar_waldo", /* flags = */ 0, &value) > 0 && value == true);
+ assert_se(proc_cmdline_get_bool("x_y-z", /* flags = */ 0, &value) > 0 && value == false);
+ assert_se(proc_cmdline_get_bool("x-y-z", /* flags = */ 0, &value) > 0 && value == false);
+ assert_se(proc_cmdline_get_bool("x-y_z", /* flags = */ 0, &value) > 0 && value == false);
+ assert_se(proc_cmdline_get_bool("x_y_z", /* flags = */ 0, &value) > 0 && value == false);
+ assert_se(proc_cmdline_get_bool("quux", /* flags = */ 0, &value) == -EINVAL && value == false);
+ assert_se(proc_cmdline_get_bool("da", /* flags = */ 0, &value) > 0 && value == true);
+ assert_se(proc_cmdline_get_bool("the", /* flags = */ 0, &value) > 0 && value == true);
+}
+
+#if ENABLE_EFI
+TEST(proc_cmdline_get_bool_efi) {
+ bool value = false;
+
+ assert_se(putenv((char*) "SYSTEMD_PROC_CMDLINE=") == 0);
+ assert_se(putenv((char*) "SYSTEMD_EFI_OPTIONS=foo_bar bar-waldo=1 x_y-z=0 quux=miep\nda=yes\nthe=1") == 0);
+
+ assert_se(proc_cmdline_get_bool("", /* flags = */ 0, &value) == -EINVAL);
+ assert_se(proc_cmdline_get_bool("abc", /* flags = */ 0, &value) == 0 && value == false);
+ assert_se(proc_cmdline_get_bool("foo_bar", /* flags = */ 0, &value) > 0 && value == true);
+ assert_se(proc_cmdline_get_bool("foo-bar", /* flags = */ 0, &value) > 0 && value == true);
+ assert_se(proc_cmdline_get_bool("bar-waldo", /* flags = */ 0, &value) > 0 && value == true);
+ assert_se(proc_cmdline_get_bool("bar_waldo", /* flags = */ 0, &value) > 0 && value == true);
+ assert_se(proc_cmdline_get_bool("x_y-z", /* flags = */ 0, &value) > 0 && value == false);
+ assert_se(proc_cmdline_get_bool("x-y-z", /* flags = */ 0, &value) > 0 && value == false);
+ assert_se(proc_cmdline_get_bool("x-y_z", /* flags = */ 0, &value) > 0 && value == false);
+ assert_se(proc_cmdline_get_bool("x_y_z", /* flags = */ 0, &value) > 0 && value == false);
+ assert_se(proc_cmdline_get_bool("quux", /* flags = */ 0, &value) == -EINVAL && value == false);
+ assert_se(proc_cmdline_get_bool("da", /* flags = */ 0, &value) > 0 && value == true);
+ assert_se(proc_cmdline_get_bool("the", /* flags = */ 0, &value) > 0 && value == true);
+}
+#endif
+
+TEST(proc_cmdline_get_key_many) {
+ _cleanup_free_ char *value1 = NULL, *value2 = NULL, *value3 = NULL, *value4 = NULL, *value5 = NULL, *value6 = NULL, *value7 = NULL;
+
+ assert_se(putenv((char*) "SYSTEMD_PROC_CMDLINE=foo_bar=quux wuff-piep=tuet zumm SPACE='one two' doubleticks=\" aaa aaa \"\n\nzummm='\n'\n") == 0);
+
+ assert_se(proc_cmdline_get_key_many(0,
+ "wuff-piep", &value3,
+ "foo_bar", &value1,
+ "idontexist", &value2,
+ "zumm", &value4,
+ "SPACE", &value5,
+ "doubleticks", &value6,
+ "zummm", &value7) == 5);
+
+ assert_se(streq_ptr(value1, "quux"));
+ assert_se(!value2);
+ assert_se(streq_ptr(value3, "tuet"));
+ assert_se(!value4);
+ assert_se(streq_ptr(value5, "one two"));
+ assert_se(streq_ptr(value6, " aaa aaa "));
+ assert_se(streq_ptr(value7, "\n"));
+}
+
+TEST(proc_cmdline_key_streq) {
+ assert_se(proc_cmdline_key_streq("", ""));
+ assert_se(proc_cmdline_key_streq("a", "a"));
+ assert_se(!proc_cmdline_key_streq("", "a"));
+ assert_se(!proc_cmdline_key_streq("a", ""));
+ assert_se(proc_cmdline_key_streq("a", "a"));
+ assert_se(!proc_cmdline_key_streq("a", "b"));
+ assert_se(proc_cmdline_key_streq("x-y-z", "x-y-z"));
+ assert_se(proc_cmdline_key_streq("x-y-z", "x_y_z"));
+ assert_se(proc_cmdline_key_streq("x-y-z", "x-y_z"));
+ assert_se(proc_cmdline_key_streq("x-y-z", "x_y-z"));
+ assert_se(proc_cmdline_key_streq("x_y-z", "x-y_z"));
+ assert_se(!proc_cmdline_key_streq("x_y-z", "x-z_z"));
+}
+
+TEST(proc_cmdline_key_startswith) {
+ assert_se(proc_cmdline_key_startswith("", ""));
+ assert_se(proc_cmdline_key_startswith("x", ""));
+ assert_se(!proc_cmdline_key_startswith("", "x"));
+ assert_se(proc_cmdline_key_startswith("x", "x"));
+ assert_se(!proc_cmdline_key_startswith("x", "y"));
+ assert_se(!proc_cmdline_key_startswith("foo-bar", "quux"));
+ assert_se(proc_cmdline_key_startswith("foo-bar", "foo"));
+ assert_se(proc_cmdline_key_startswith("foo-bar", "foo-bar"));
+ assert_se(proc_cmdline_key_startswith("foo-bar", "foo_bar"));
+ assert_se(proc_cmdline_key_startswith("foo-bar", "foo_"));
+ assert_se(!proc_cmdline_key_startswith("foo-bar", "foo_xx"));
+}
+
+#define test_proc_cmdline_filter_pid1_args_one(nulstr, expected) \
+ ({ \
+ _cleanup_strv_free_ char **a = NULL, **b = NULL; \
+ const char s[] = (nulstr); \
+ \
+ /* This emulates pid_get_cmdline_strv(). */ \
+ assert_se(a = strv_parse_nulstr_full(s, ELEMENTSOF(s), \
+ /* drop_trailing_nuls = */ true)); \
+ assert_se(proc_cmdline_filter_pid1_args(a, &b) >= 0); \
+ assert_se(strv_equal(b, expected)); \
+ })
+
+TEST(proc_cmdline_filter_pid1_args) {
+ test_proc_cmdline_filter_pid1_args_one("systemd\0",
+ STRV_MAKE_EMPTY);
+
+ /* short option */
+ test_proc_cmdline_filter_pid1_args_one("systemd\0"
+ "-a\0" /* unknown option */
+ "-abc\0" /* unknown options */
+ "-h\0" /* known option */
+ "-hDbs\0" /* known options */
+ "-hsx\0" /* mixed (known and unknown) options */
+ "-z\0drop1\0" /* option with argument */
+ "-z\0-z\0accept1\0" /* the second -z is handled as argument */
+ "-az\0drop2\0" /* options with argument */
+ "-za\0accept2\0" /* options with argument */
+ "-z\0--\0-x\0", /* "--" is handled as argument */
+ STRV_MAKE("accept1", "accept2"));
+
+ /* long option */
+ test_proc_cmdline_filter_pid1_args_one("systemd\0"
+ "--unknown\0accept1\0" /* unknown option */
+ "--system\0accept2\0" /* no argument */
+ "--log-level\0drop1\0" /* required argument (separated with space) */
+ "--log-level=drop2\0accept3\0" /* required argument (concatenated with '=') */
+ "--log-level\0--log-level\0accept4\0" /* the second "--log-level" is handled as argument */
+ "--log-level\0--\0-x\0" /* "--" is handled as argument */
+ "--log-color\0--log-level\0drop3\0" /* optional argument ("--log-level" is handled as another option) */
+ "--log-color\0accept5\0" /* optional argument (separated with space) */
+ "--log-color=drop4\0accept6\0" /* optional argument (concatenated with '=') */
+ "--log-color\0--\0" /* "--" is _not_ handled as argument, and remaining strings are accepted */
+ "remaining\0-x\0--foo\0",
+ STRV_MAKE("accept1", "accept2", "accept3", "accept4", "accept5", "accept6", "remaining", "-x", "--foo"));
+
+ /* test for "--" */
+ test_proc_cmdline_filter_pid1_args_one("systemd\0"
+ "-a\0"
+ "--dropped\0"
+ "--\0" /* remaining strings are accepted */
+ "-x\0"
+ "-abc\0"
+ "--hoge\0"
+ "accepted\0",
+ STRV_MAKE("-x", "-abc", "--hoge", "accepted"));
+
+ /* test for space */
+ test_proc_cmdline_filter_pid1_args_one("/usr/lib/systemd/systemd\0"
+ "--switched-root\0"
+ "--system\0"
+ "--deserialize\030\0" /* followed with space */
+ "--deserialize=31\0" /* followed with '=' */
+ "--exit-code=42\0"
+ "\0\0\0"
+ "systemd.log_level=debug\0"
+ "--unit\0foo.target\0"
+ " ' quoted '\0"
+ "systemd.log_target=console\0"
+ "\t\0"
+ " arg with space \0"
+ "3\0"
+ "\0\0\0",
+ STRV_MAKE("", "", "", "systemd.log_level=debug", " ' quoted '", "systemd.log_target=console", "\t", " arg with space ", "3"));
+}
+
+static int intro(void) {
+ if (access("/proc/cmdline", R_OK) < 0 && ERRNO_IS_PRIVILEGE(errno))
+ return log_tests_skipped("can't read /proc/cmdline");
+
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_INFO, intro);
diff --git a/src/test/test-process-util.c b/src/test/test-process-util.c
new file mode 100644
index 0000000..957e214
--- /dev/null
+++ b/src/test/test-process-util.c
@@ -0,0 +1,954 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <linux/oom.h>
+#include <pthread.h>
+#include <sys/mount.h>
+#include <sys/personality.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#if HAVE_VALGRIND_VALGRIND_H
+#include <valgrind/valgrind.h>
+#endif
+
+#include "alloc-util.h"
+#include "architecture.h"
+#include "dirent-util.h"
+#include "errno-list.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "ioprio-util.h"
+#include "log.h"
+#include "macro.h"
+#include "missing_sched.h"
+#include "missing_syscall.h"
+#include "namespace-util.h"
+#include "parse-util.h"
+#include "process-util.h"
+#include "procfs-util.h"
+#include "rlimit-util.h"
+#include "signal-util.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "terminal-util.h"
+#include "tests.h"
+#include "user-util.h"
+#include "virt.h"
+
+static void test_pid_get_comm_one(pid_t pid) {
+ struct stat st;
+ _cleanup_free_ char *a = NULL, *c = NULL, *d = NULL, *f = NULL, *i = NULL;
+ _cleanup_free_ char *env = NULL;
+ char path[STRLEN("/proc//comm") + DECIMAL_STR_MAX(pid_t)];
+ pid_t e;
+ uid_t u;
+ gid_t g;
+ dev_t h;
+ int r;
+
+ log_info("/* %s */", __func__);
+
+ xsprintf(path, "/proc/"PID_FMT"/comm", pid);
+
+ if (stat(path, &st) == 0) {
+ assert_se(pid_get_comm(pid, &a) >= 0);
+ log_info("PID"PID_FMT" comm: '%s'", pid, a);
+ } else
+ log_warning("%s not exist.", path);
+
+ assert_se(pid_get_cmdline(pid, 0, PROCESS_CMDLINE_COMM_FALLBACK, &c) >= 0);
+ log_info("PID"PID_FMT" cmdline: '%s'", pid, c);
+
+ assert_se(pid_get_cmdline(pid, 8, 0, &d) >= 0);
+ log_info("PID"PID_FMT" cmdline truncated to 8: '%s'", pid, d);
+
+ free(d);
+ assert_se(pid_get_cmdline(pid, 1, 0, &d) >= 0);
+ log_info("PID"PID_FMT" cmdline truncated to 1: '%s'", pid, d);
+
+ r = get_process_ppid(pid, &e);
+ assert_se(pid == 1 ? r == -EADDRNOTAVAIL : r >= 0);
+ if (r >= 0) {
+ log_info("PID"PID_FMT" PPID: "PID_FMT, pid, e);
+ assert_se(e > 0);
+ }
+
+ assert_se(pid_is_kernel_thread(pid) == 0 || pid != 1);
+
+ r = get_process_exe(pid, &f);
+ assert_se(r >= 0 || r == -EACCES);
+ log_info("PID"PID_FMT" exe: '%s'", pid, strna(f));
+
+ assert_se(pid_get_uid(pid, &u) == 0);
+ log_info("PID"PID_FMT" UID: "UID_FMT, pid, u);
+
+ assert_se(get_process_gid(pid, &g) == 0);
+ log_info("PID"PID_FMT" GID: "GID_FMT, pid, g);
+
+ r = get_process_environ(pid, &env);
+ assert_se(r >= 0 || r == -EACCES);
+ log_info("PID"PID_FMT" strlen(environ): %zi", pid, env ? (ssize_t)strlen(env) : (ssize_t)-errno);
+
+ if (!detect_container())
+ assert_se(get_ctty_devnr(pid, &h) == -ENXIO || pid != 1);
+
+ (void) getenv_for_pid(pid, "PATH", &i);
+ log_info("PID"PID_FMT" $PATH: '%s'", pid, strna(i));
+}
+
+TEST(pid_get_comm) {
+ if (saved_argc > 1) {
+ pid_t pid = 0;
+
+ (void) parse_pid(saved_argv[1], &pid);
+ test_pid_get_comm_one(pid);
+ } else {
+ TEST_REQ_RUNNING_SYSTEMD(test_pid_get_comm_one(1));
+ test_pid_get_comm_one(getpid());
+ }
+}
+
+static void test_pid_get_cmdline_one(pid_t pid) {
+ _cleanup_free_ char *c = NULL, *d = NULL, *e = NULL, *f = NULL, *g = NULL, *h = NULL, *joined = NULL;
+ _cleanup_strv_free_ char **strv_a = NULL, **strv_b = NULL;
+ int r;
+
+ r = pid_get_cmdline(pid, SIZE_MAX, 0, &c);
+ log_info("PID "PID_FMT": %s", pid, r >= 0 ? c : errno_to_name(r));
+
+ r = pid_get_cmdline(pid, SIZE_MAX, PROCESS_CMDLINE_COMM_FALLBACK, &d);
+ log_info(" %s", r >= 0 ? d : errno_to_name(r));
+
+ r = pid_get_cmdline(pid, SIZE_MAX, PROCESS_CMDLINE_QUOTE, &e);
+ log_info(" %s", r >= 0 ? e : errno_to_name(r));
+
+ r = pid_get_cmdline(pid, SIZE_MAX, PROCESS_CMDLINE_QUOTE | PROCESS_CMDLINE_COMM_FALLBACK, &f);
+ log_info(" %s", r >= 0 ? f : errno_to_name(r));
+
+ r = pid_get_cmdline(pid, SIZE_MAX, PROCESS_CMDLINE_QUOTE_POSIX, &g);
+ log_info(" %s", r >= 0 ? g : errno_to_name(r));
+
+ r = pid_get_cmdline(pid, SIZE_MAX, PROCESS_CMDLINE_QUOTE_POSIX | PROCESS_CMDLINE_COMM_FALLBACK, &h);
+ log_info(" %s", r >= 0 ? h : errno_to_name(r));
+
+ r = pid_get_cmdline_strv(pid, 0, &strv_a);
+ if (r >= 0)
+ assert_se(joined = strv_join(strv_a, "\", \""));
+ log_info(" \"%s\"", r >= 0 ? joined : errno_to_name(r));
+
+ joined = mfree(joined);
+
+ r = pid_get_cmdline_strv(pid, PROCESS_CMDLINE_COMM_FALLBACK, &strv_b);
+ if (r >= 0)
+ assert_se(joined = strv_join(strv_b, "\", \""));
+ log_info(" \"%s\"", r >= 0 ? joined : errno_to_name(r));
+}
+
+TEST(pid_get_cmdline) {
+ _cleanup_closedir_ DIR *d = NULL;
+ int r;
+
+ assert_se(proc_dir_open(&d) >= 0);
+
+ for (;;) {
+ pid_t pid;
+
+ r = proc_dir_read(d, &pid);
+ assert_se(r >= 0);
+
+ if (r == 0) /* EOF */
+ break;
+
+ test_pid_get_cmdline_one(pid);
+ }
+}
+
+static void test_pid_get_comm_escape_one(const char *input, const char *output) {
+ _cleanup_free_ char *n = NULL;
+
+ log_debug("input: <%s> — output: <%s>", input, output);
+
+ assert_se(prctl(PR_SET_NAME, input) >= 0);
+ assert_se(pid_get_comm(0, &n) >= 0);
+
+ log_debug("got: <%s>", n);
+
+ assert_se(streq_ptr(n, output));
+}
+
+TEST(pid_get_comm_escape) {
+ _cleanup_free_ char *saved = NULL;
+
+ assert_se(pid_get_comm(0, &saved) >= 0);
+
+ test_pid_get_comm_escape_one("", "");
+ test_pid_get_comm_escape_one("foo", "foo");
+ test_pid_get_comm_escape_one("012345678901234", "012345678901234");
+ test_pid_get_comm_escape_one("0123456789012345", "012345678901234");
+ test_pid_get_comm_escape_one("äöüß", "\\303\\244\\303\\266\\303\\274\\303\\237");
+ test_pid_get_comm_escape_one("xäöüß", "x\\303\\244\\303\\266\\303\\274\\303\\237");
+ test_pid_get_comm_escape_one("xxäöüß", "xx\\303\\244\\303\\266\\303\\274\\303\\237");
+ test_pid_get_comm_escape_one("xxxäöüß", "xxx\\303\\244\\303\\266\\303\\274\\303\\237");
+ test_pid_get_comm_escape_one("xxxxäöüß", "xxxx\\303\\244\\303\\266\\303\\274\\303\\237");
+ test_pid_get_comm_escape_one("xxxxxäöüß", "xxxxx\\303\\244\\303\\266\\303\\274\\303\\237");
+
+ assert_se(prctl(PR_SET_NAME, saved) >= 0);
+}
+
+TEST(pid_is_unwaited) {
+ pid_t pid;
+
+ pid = fork();
+ assert_se(pid >= 0);
+ if (pid == 0) {
+ _exit(EXIT_SUCCESS);
+ } else {
+ int status;
+
+ assert_se(waitpid(pid, &status, 0) == pid);
+ assert_se(pid_is_unwaited(pid) == 0);
+ }
+ assert_se(pid_is_unwaited(getpid_cached()) > 0);
+ assert_se(pid_is_unwaited(-1) < 0);
+}
+
+TEST(pid_is_alive) {
+ pid_t pid;
+
+ pid = fork();
+ assert_se(pid >= 0);
+ if (pid == 0) {
+ _exit(EXIT_SUCCESS);
+ } else {
+ int status;
+
+ assert_se(waitpid(pid, &status, 0) == pid);
+ assert_se(pid_is_alive(pid) == 0);
+ }
+ assert_se(pid_is_alive(getpid_cached()) > 0);
+ assert_se(pid_is_alive(-1) < 0);
+}
+
+TEST(personality) {
+ assert_se(personality_to_string(PER_LINUX));
+ assert_se(!personality_to_string(PERSONALITY_INVALID));
+
+ assert_se(streq(personality_to_string(PER_LINUX), architecture_to_string(native_architecture())));
+
+ assert_se(personality_from_string(personality_to_string(PER_LINUX)) == PER_LINUX);
+ assert_se(personality_from_string(architecture_to_string(native_architecture())) == PER_LINUX);
+
+#ifdef __x86_64__
+ assert_se(streq_ptr(personality_to_string(PER_LINUX), "x86-64"));
+ assert_se(streq_ptr(personality_to_string(PER_LINUX32), "x86"));
+
+ assert_se(personality_from_string("x86-64") == PER_LINUX);
+ assert_se(personality_from_string("x86") == PER_LINUX32);
+ assert_se(personality_from_string("ia64") == PERSONALITY_INVALID);
+ assert_se(personality_from_string(NULL) == PERSONALITY_INVALID);
+
+ assert_se(personality_from_string(personality_to_string(PER_LINUX32)) == PER_LINUX32);
+#endif
+}
+
+TEST(pid_get_cmdline_harder) {
+ char path[] = "/tmp/test-cmdlineXXXXXX";
+ _cleanup_close_ int fd = -EBADF;
+ _cleanup_free_ char *line = NULL;
+ _cleanup_strv_free_ char **args = NULL;
+ pid_t pid;
+ int r;
+
+ if (geteuid() != 0) {
+ log_info("Skipping %s: not root", __func__);
+ return;
+ }
+
+ if (!have_namespaces()) {
+ log_notice("Testing without namespaces, skipping %s", __func__);
+ return;
+ }
+
+#if HAVE_VALGRIND_VALGRIND_H
+ /* valgrind patches open(/proc//cmdline)
+ * so, test_pid_get_cmdline_harder fails always
+ * See https://github.com/systemd/systemd/pull/3555#issuecomment-226564908 */
+ if (RUNNING_ON_VALGRIND) {
+ log_info("Skipping %s: running on valgrind", __func__);
+ return;
+ }
+#endif
+
+ pid = fork();
+ if (pid > 0) {
+ siginfo_t si;
+
+ (void) wait_for_terminate(pid, &si);
+
+ assert_se(si.si_code == CLD_EXITED);
+ assert_se(si.si_status == 0);
+
+ return;
+ }
+
+ assert_se(pid == 0);
+
+ r = detach_mount_namespace();
+ if (r < 0) {
+ log_warning_errno(r, "detach mount namespace failed: %m");
+ assert_se(ERRNO_IS_PRIVILEGE(r));
+ return;
+ }
+
+ fd = mkostemp(path, O_CLOEXEC);
+ assert_se(fd >= 0);
+
+ /* Note that we don't unmount the following bind-mount at the end of the test because the kernel
+ * will clear up its /proc/PID/ hierarchy automatically as soon as the test stops. */
+ if (mount(path, "/proc/self/cmdline", "bind", MS_BIND, NULL) < 0) {
+ /* This happens under selinux… Abort the test in this case. */
+ log_warning_errno(errno, "mount(..., \"/proc/self/cmdline\", \"bind\", ...) failed: %m");
+ assert_se(IN_SET(errno, EPERM, EACCES));
+ return;
+ }
+
+ /* Set RLIMIT_STACK to infinity to test we don't try to allocate unnecessarily large values to read
+ * the cmdline. */
+ if (setrlimit(RLIMIT_STACK, &RLIMIT_MAKE_CONST(RLIM_INFINITY)) < 0)
+ log_warning("Testing without RLIMIT_STACK=infinity");
+
+ assert_se(unlink(path) >= 0);
+
+ assert_se(prctl(PR_SET_NAME, "testa") >= 0);
+
+ assert_se(pid_get_cmdline(0, SIZE_MAX, 0, &line) == -ENOENT);
+
+ assert_se(pid_get_cmdline(0, SIZE_MAX, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "[testa]"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, SIZE_MAX, PROCESS_CMDLINE_COMM_FALLBACK | PROCESS_CMDLINE_QUOTE, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "\"[testa]\"")); /* quoting is enabled here */
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 0, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, ""));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 1, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ assert_se(streq(line, "…"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 2, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ assert_se(streq(line, "[…"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 3, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ assert_se(streq(line, "[t…"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 4, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ assert_se(streq(line, "[te…"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 5, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ assert_se(streq(line, "[tes…"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 6, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ assert_se(streq(line, "[test…"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 7, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ assert_se(streq(line, "[testa]"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 8, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ assert_se(streq(line, "[testa]"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline_strv(0, PROCESS_CMDLINE_COMM_FALLBACK, &args) >= 0);
+ assert_se(strv_equal(args, STRV_MAKE("[testa]")));
+ args = strv_free(args);
+
+ /* Test with multiple arguments that don't require quoting */
+
+ assert_se(write(fd, "foo\0bar", 8) == 8);
+
+ assert_se(pid_get_cmdline(0, SIZE_MAX, 0, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "foo bar"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, SIZE_MAX, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ assert_se(streq(line, "foo bar"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline_strv(0, PROCESS_CMDLINE_COMM_FALLBACK, &args) >= 0);
+ assert_se(strv_equal(args, STRV_MAKE("foo", "bar")));
+ args = strv_free(args);
+
+ assert_se(write(fd, "quux", 4) == 4);
+ assert_se(pid_get_cmdline(0, SIZE_MAX, 0, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "foo bar quux"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, SIZE_MAX, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "foo bar quux"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 1, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "…"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 2, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "f…"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 3, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "fo…"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 4, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "foo…"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 5, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "foo …"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 6, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "foo b…"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 7, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "foo ba…"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 8, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "foo bar…"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 9, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "foo bar …"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 10, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "foo bar q…"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 11, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "foo bar qu…"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 12, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "foo bar quux"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 13, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "foo bar quux"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 14, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "foo bar quux"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 1000, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "foo bar quux"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline_strv(0, PROCESS_CMDLINE_COMM_FALLBACK, &args) >= 0);
+ assert_se(strv_equal(args, STRV_MAKE("foo", "bar", "quux")));
+ args = strv_free(args);
+
+ assert_se(ftruncate(fd, 0) >= 0);
+ assert_se(prctl(PR_SET_NAME, "aaaa bbbb cccc") >= 0);
+
+ assert_se(pid_get_cmdline(0, SIZE_MAX, 0, &line) == -ENOENT);
+
+ assert_se(pid_get_cmdline(0, SIZE_MAX, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "[aaaa bbbb cccc]"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 10, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "[aaaa bbb…"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 11, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "[aaaa bbbb…"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, 12, PROCESS_CMDLINE_COMM_FALLBACK, &line) >= 0);
+ log_debug("'%s'", line);
+ assert_se(streq(line, "[aaaa bbbb …"));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline_strv(0, PROCESS_CMDLINE_COMM_FALLBACK, &args) >= 0);
+ assert_se(strv_equal(args, STRV_MAKE("[aaaa bbbb cccc]")));
+ args = strv_free(args);
+
+ /* Test with multiple arguments that do require quoting */
+
+#define CMDLINE1 "foo\0'bar'\0\"bar$\"\0x y z\0!``\0"
+#define EXPECT1 "foo \"'bar'\" \"\\\"bar\\$\\\"\" \"x y z\" \"!\\`\\`\""
+#define EXPECT1p "foo $'\\'bar\\'' $'\"bar$\"' $'x y z' $'!``'"
+#define EXPECT1v STRV_MAKE("foo", "'bar'", "\"bar$\"", "x y z", "!``")
+
+ assert_se(lseek(fd, SEEK_SET, 0) == 0);
+ assert_se(write(fd, CMDLINE1, sizeof CMDLINE1) == sizeof CMDLINE1);
+ assert_se(ftruncate(fd, sizeof CMDLINE1) == 0);
+
+ assert_se(pid_get_cmdline(0, SIZE_MAX, PROCESS_CMDLINE_QUOTE, &line) >= 0);
+ log_debug("got: ==%s==", line);
+ log_debug("exp: ==%s==", EXPECT1);
+ assert_se(streq(line, EXPECT1));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, SIZE_MAX, PROCESS_CMDLINE_QUOTE_POSIX, &line) >= 0);
+ log_debug("got: ==%s==", line);
+ log_debug("exp: ==%s==", EXPECT1p);
+ assert_se(streq(line, EXPECT1p));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline_strv(0, 0, &args) >= 0);
+ assert_se(strv_equal(args, EXPECT1v));
+ args = strv_free(args);
+
+#define CMDLINE2 "foo\0\1\2\3\0\0"
+#define EXPECT2 "foo \"\\001\\002\\003\""
+#define EXPECT2p "foo $'\\001\\002\\003'"
+#define EXPECT2v STRV_MAKE("foo", "\1\2\3")
+
+ assert_se(lseek(fd, SEEK_SET, 0) == 0);
+ assert_se(write(fd, CMDLINE2, sizeof CMDLINE2) == sizeof CMDLINE2);
+ assert_se(ftruncate(fd, sizeof CMDLINE2) == 0);
+
+ assert_se(pid_get_cmdline(0, SIZE_MAX, PROCESS_CMDLINE_QUOTE, &line) >= 0);
+ log_debug("got: ==%s==", line);
+ log_debug("exp: ==%s==", EXPECT2);
+ assert_se(streq(line, EXPECT2));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline(0, SIZE_MAX, PROCESS_CMDLINE_QUOTE_POSIX, &line) >= 0);
+ log_debug("got: ==%s==", line);
+ log_debug("exp: ==%s==", EXPECT2p);
+ assert_se(streq(line, EXPECT2p));
+ line = mfree(line);
+
+ assert_se(pid_get_cmdline_strv(0, 0, &args) >= 0);
+ assert_se(strv_equal(args, EXPECT2v));
+ args = strv_free(args);
+
+ safe_close(fd);
+ _exit(EXIT_SUCCESS);
+}
+
+TEST(getpid_cached) {
+ siginfo_t si;
+ pid_t a, b, c, d, e, f, child;
+
+ a = raw_getpid();
+ b = getpid_cached();
+ c = getpid();
+
+ assert_se(a == b && a == c);
+
+ child = fork();
+ assert_se(child >= 0);
+
+ if (child == 0) {
+ /* In child */
+ a = raw_getpid();
+ b = getpid_cached();
+ c = getpid();
+
+ assert_se(a == b && a == c);
+ _exit(EXIT_SUCCESS);
+ }
+
+ d = raw_getpid();
+ e = getpid_cached();
+ f = getpid();
+
+ assert_se(a == d && a == e && a == f);
+
+ assert_se(wait_for_terminate(child, &si) >= 0);
+ assert_se(si.si_status == 0);
+ assert_se(si.si_code == CLD_EXITED);
+}
+
+TEST(getpid_measure) {
+ usec_t t, q;
+
+ unsigned long long iterations = slow_tests_enabled() ? 1000000 : 1000;
+
+ log_info("/* %s (%llu iterations) */", __func__, iterations);
+
+ t = now(CLOCK_MONOTONIC);
+ for (unsigned long long i = 0; i < iterations; i++)
+ (void) getpid();
+ q = now(CLOCK_MONOTONIC) - t;
+
+ log_info(" glibc getpid(): %lf μs each", (double) q / iterations);
+
+ iterations *= 50; /* _cached() is about 50 times faster, so we need more iterations */
+
+ t = now(CLOCK_MONOTONIC);
+ for (unsigned long long i = 0; i < iterations; i++)
+ (void) getpid_cached();
+ q = now(CLOCK_MONOTONIC) - t;
+
+ log_info("getpid_cached(): %lf μs each", (double) q / iterations);
+}
+
+TEST(safe_fork) {
+ siginfo_t status;
+ pid_t pid;
+ int r;
+
+ BLOCK_SIGNALS(SIGCHLD);
+
+ r = safe_fork("(test-child)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_REARRANGE_STDIO|FORK_REOPEN_LOG, &pid);
+ assert_se(r >= 0);
+
+ if (r == 0) {
+ /* child */
+ usleep_safe(100 * USEC_PER_MSEC);
+
+ _exit(88);
+ }
+
+ assert_se(wait_for_terminate(pid, &status) >= 0);
+ assert_se(status.si_code == CLD_EXITED);
+ assert_se(status.si_status == 88);
+}
+
+TEST(pid_to_ptr) {
+ assert_se(PTR_TO_PID(NULL) == 0);
+ assert_se(PID_TO_PTR(0) == NULL);
+
+ assert_se(PTR_TO_PID(PID_TO_PTR(1)) == 1);
+ assert_se(PTR_TO_PID(PID_TO_PTR(2)) == 2);
+ assert_se(PTR_TO_PID(PID_TO_PTR(-1)) == -1);
+ assert_se(PTR_TO_PID(PID_TO_PTR(-2)) == -2);
+
+ assert_se(PTR_TO_PID(PID_TO_PTR(INT16_MAX)) == INT16_MAX);
+ assert_se(PTR_TO_PID(PID_TO_PTR(INT16_MIN)) == INT16_MIN);
+
+ assert_se(PTR_TO_PID(PID_TO_PTR(INT32_MAX)) == INT32_MAX);
+ assert_se(PTR_TO_PID(PID_TO_PTR(INT32_MIN)) == INT32_MIN);
+}
+
+static void test_ioprio_class_from_to_string_one(const char *val, int expected, int normalized) {
+ assert_se(ioprio_class_from_string(val) == expected);
+ if (expected >= 0) {
+ _cleanup_free_ char *s = NULL;
+ unsigned ret;
+ int combined;
+
+ assert_se(ioprio_class_to_string_alloc(expected, &s) == 0);
+ /* We sometimes get a class number and sometimes a name back */
+ assert_se(streq(s, val) ||
+ safe_atou(val, &ret) == 0);
+
+ /* Make sure normalization works, i.e. NONE → BE gets normalized */
+ combined = ioprio_normalize(ioprio_prio_value(expected, 0));
+ assert_se(ioprio_prio_class(combined) == normalized);
+ assert_se(expected != IOPRIO_CLASS_NONE || ioprio_prio_data(combined) == 4);
+ }
+}
+
+TEST(ioprio_class_from_to_string) {
+ test_ioprio_class_from_to_string_one("none", IOPRIO_CLASS_NONE, IOPRIO_CLASS_BE);
+ test_ioprio_class_from_to_string_one("realtime", IOPRIO_CLASS_RT, IOPRIO_CLASS_RT);
+ test_ioprio_class_from_to_string_one("best-effort", IOPRIO_CLASS_BE, IOPRIO_CLASS_BE);
+ test_ioprio_class_from_to_string_one("idle", IOPRIO_CLASS_IDLE, IOPRIO_CLASS_IDLE);
+ test_ioprio_class_from_to_string_one("0", IOPRIO_CLASS_NONE, IOPRIO_CLASS_BE);
+ test_ioprio_class_from_to_string_one("1", 1, 1);
+ test_ioprio_class_from_to_string_one("7", 7, 7);
+ test_ioprio_class_from_to_string_one("8", 8, 8);
+ test_ioprio_class_from_to_string_one("9", -EINVAL, -EINVAL);
+ test_ioprio_class_from_to_string_one("-1", -EINVAL, -EINVAL);
+}
+
+TEST(setpriority_closest) {
+ int r;
+
+ r = safe_fork("(test-setprio)",
+ FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|FORK_WAIT|FORK_LOG, NULL);
+ assert_se(r >= 0);
+
+ if (r == 0) {
+ bool full_test;
+ int p, q;
+ /* child */
+
+ /* rlimit of 30 equals nice level of -10 */
+ if (setrlimit(RLIMIT_NICE, &RLIMIT_MAKE_CONST(30)) < 0) {
+ /* If this fails we are probably unprivileged or in a userns of some kind, let's skip
+ * the full test */
+ assert_se(ERRNO_IS_PRIVILEGE(errno));
+ full_test = false;
+ } else {
+ /* However, if the hard limit was above 30, setrlimit would succeed unprivileged, so
+ * check if the UID/GID can be changed before enabling the full test. */
+ if (setresgid(GID_NOBODY, GID_NOBODY, GID_NOBODY) < 0) {
+ assert_se(ERRNO_IS_PRIVILEGE(errno));
+ full_test = false;
+ } else if (setresuid(UID_NOBODY, UID_NOBODY, UID_NOBODY) < 0) {
+ assert_se(ERRNO_IS_PRIVILEGE(errno));
+ full_test = false;
+ } else
+ full_test = true;
+ }
+
+ errno = 0;
+ p = getpriority(PRIO_PROCESS, 0);
+ assert_se(errno == 0);
+
+ /* It should always be possible to set our nice level to the current one */
+ assert_se(setpriority_closest(p) > 0);
+
+ errno = 0;
+ q = getpriority(PRIO_PROCESS, 0);
+ assert_se(errno == 0 && p == q);
+
+ /* It should also be possible to set the nice level to one higher */
+ if (p < PRIO_MAX-1) {
+ assert_se(setpriority_closest(++p) > 0);
+
+ errno = 0;
+ q = getpriority(PRIO_PROCESS, 0);
+ assert_se(errno == 0 && p == q);
+ }
+
+ /* It should also be possible to set the nice level to two higher */
+ if (p < PRIO_MAX-1) {
+ assert_se(setpriority_closest(++p) > 0);
+
+ errno = 0;
+ q = getpriority(PRIO_PROCESS, 0);
+ assert_se(errno == 0 && p == q);
+ }
+
+ if (full_test) {
+ /* These two should work, given the RLIMIT_NICE we set above */
+ assert_se(setpriority_closest(-10) > 0);
+ errno = 0;
+ q = getpriority(PRIO_PROCESS, 0);
+ assert_se(errno == 0 && q == -10);
+
+ assert_se(setpriority_closest(-9) > 0);
+ errno = 0;
+ q = getpriority(PRIO_PROCESS, 0);
+ assert_se(errno == 0 && q == -9);
+
+ /* This should succeed but should be clamped to the limit */
+ assert_se(setpriority_closest(-11) == 0);
+ errno = 0;
+ q = getpriority(PRIO_PROCESS, 0);
+ assert_se(errno == 0 && q == -10);
+
+ assert_se(setpriority_closest(-8) > 0);
+ errno = 0;
+ q = getpriority(PRIO_PROCESS, 0);
+ assert_se(errno == 0 && q == -8);
+
+ /* This should succeed but should be clamped to the limit */
+ assert_se(setpriority_closest(-12) == 0);
+ errno = 0;
+ q = getpriority(PRIO_PROCESS, 0);
+ assert_se(errno == 0 && q == -10);
+ }
+
+ _exit(EXIT_SUCCESS);
+ }
+}
+
+TEST(get_process_ppid) {
+ uint64_t limit;
+ int r;
+
+ assert_se(get_process_ppid(1, NULL) == -EADDRNOTAVAIL);
+
+ /* the process with the PID above the global limit definitely doesn't exist. Verify that */
+ assert_se(procfs_get_pid_max(&limit) >= 0);
+ log_debug("kernel.pid_max = %"PRIu64, limit);
+
+ if (limit < INT_MAX) {
+ r = get_process_ppid(limit + 1, NULL);
+ log_debug_errno(r, "get_process_limit(%"PRIu64") → %d/%m", limit + 1, r);
+ assert(r == -ESRCH);
+ }
+
+ for (pid_t pid = 0;;) {
+ _cleanup_free_ char *c1 = NULL, *c2 = NULL;
+ pid_t ppid;
+
+ r = get_process_ppid(pid, &ppid);
+ if (r == -EADDRNOTAVAIL) {
+ log_info("No further parent PID");
+ break;
+ }
+
+ assert_se(r >= 0);
+
+ assert_se(pid_get_cmdline(pid, SIZE_MAX, PROCESS_CMDLINE_COMM_FALLBACK, &c1) >= 0);
+ assert_se(pid_get_cmdline(ppid, SIZE_MAX, PROCESS_CMDLINE_COMM_FALLBACK, &c2) >= 0);
+
+ log_info("Parent of " PID_FMT " (%s) is " PID_FMT " (%s).", pid, c1, ppid, c2);
+
+ pid = ppid;
+ }
+}
+
+TEST(set_oom_score_adjust) {
+ int a, b, r;
+
+ assert_se(get_oom_score_adjust(&a) >= 0);
+
+ r = set_oom_score_adjust(OOM_SCORE_ADJ_MIN);
+ assert_se(r >= 0 || ERRNO_IS_PRIVILEGE(r));
+
+ if (r >= 0) {
+ assert_se(get_oom_score_adjust(&b) >= 0);
+ assert_se(b == OOM_SCORE_ADJ_MIN);
+ }
+
+ assert_se(set_oom_score_adjust(a) >= 0);
+ assert_se(get_oom_score_adjust(&b) >= 0);
+ assert_se(b == a);
+}
+
+static void* dummy_thread(void *p) {
+ int fd = PTR_TO_FD(p);
+ char x;
+
+ /* let main thread know we are ready */
+ assert_se(write(fd, &(const char) { 'x' }, 1) == 1);
+
+ /* wait for the main thread to tell us to shut down */
+ assert_se(read(fd, &x, 1) == 1);
+ return NULL;
+}
+
+TEST(get_process_threads) {
+ int r;
+
+ /* Run this test in a child, so that we can guarantee there's exactly one thread around in the child */
+ r = safe_fork("(nthreads)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_REOPEN_LOG|FORK_WAIT|FORK_LOG, NULL);
+ assert_se(r >= 0);
+
+ if (r == 0) {
+ _cleanup_close_pair_ int pfd[2] = EBADF_PAIR, ppfd[2] = EBADF_PAIR;
+ pthread_t t, tt;
+ char x;
+
+ assert_se(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, pfd) >= 0);
+ assert_se(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, ppfd) >= 0);
+
+ assert_se(get_process_threads(0) == 1);
+ assert_se(pthread_create(&t, NULL, &dummy_thread, FD_TO_PTR(pfd[0])) == 0);
+ assert_se(read(pfd[1], &x, 1) == 1);
+ assert_se(get_process_threads(0) == 2);
+ assert_se(pthread_create(&tt, NULL, &dummy_thread, FD_TO_PTR(ppfd[0])) == 0);
+ assert_se(read(ppfd[1], &x, 1) == 1);
+ assert_se(get_process_threads(0) == 3);
+
+ assert_se(write(pfd[1], &(const char) { 'x' }, 1) == 1);
+ assert_se(pthread_join(t, NULL) == 0);
+
+ /* the value reported via /proc/ is decreased asynchronously, and there appears to be no nice
+ * way to sync on it. Hence we do the weak >= 2 check, even though == 2 is what we'd actually
+ * like to check here */
+ assert_se(get_process_threads(0) >= 2);
+
+ assert_se(write(ppfd[1], &(const char) { 'x' }, 1) == 1);
+ assert_se(pthread_join(tt, NULL) == 0);
+
+ /* similar here */
+ assert_se(get_process_threads(0) >= 1);
+
+ _exit(EXIT_SUCCESS);
+ }
+}
+
+TEST(is_reaper_process) {
+ int r;
+
+ r = safe_fork("(regular)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_WAIT, NULL);
+ assert_se(r >= 0);
+ if (r == 0) {
+ /* child */
+
+ assert_se(is_reaper_process() == 0);
+ _exit(EXIT_SUCCESS);
+ }
+
+ r = safe_fork("(newpid)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_WAIT, NULL);
+ assert_se(r >= 0);
+ if (r == 0) {
+ /* child */
+
+ if (unshare(CLONE_NEWPID) < 0) {
+ if (ERRNO_IS_PRIVILEGE(errno) || ERRNO_IS_NOT_SUPPORTED(errno)) {
+ log_notice("Skipping CLONE_NEWPID reaper check, lacking privileges/support");
+ _exit(EXIT_SUCCESS);
+ }
+ }
+
+ r = safe_fork("(newpid1)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_WAIT, NULL);
+ assert_se(r >= 0);
+ if (r == 0) {
+ /* grandchild, which is PID1 in a pidns */
+ assert_se(getpid_cached() == 1);
+ assert_se(is_reaper_process() > 0);
+ _exit(EXIT_SUCCESS);
+ }
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ r = safe_fork("(subreaper)", FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_WAIT, NULL);
+ assert_se(r >= 0);
+ if (r == 0) {
+ /* child */
+ assert_se(make_reaper_process(true) >= 0);
+
+ assert_se(is_reaper_process() > 0);
+ _exit(EXIT_SUCCESS);
+ }
+}
+
+static int intro(void) {
+ log_show_color(true);
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_INFO, intro);
diff --git a/src/test/test-procfs-util.c b/src/test/test-procfs-util.c
new file mode 100644
index 0000000..2b19a98
--- /dev/null
+++ b/src/test/test-procfs-util.c
@@ -0,0 +1,76 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+
+#include "errno-util.h"
+#include "format-util.h"
+#include "log.h"
+#include "procfs-util.h"
+#include "process-util.h"
+#include "tests.h"
+
+int main(int argc, char *argv[]) {
+ nsec_t nsec;
+ uint64_t v, pid_max, threads_max, limit;
+ int r;
+
+ test_setup_logging(LOG_DEBUG);
+
+ assert_se(procfs_cpu_get_usage(&nsec) >= 0);
+ log_info("Current system CPU time: %s", FORMAT_TIMESPAN(nsec/NSEC_PER_USEC, 1));
+
+ assert_se(procfs_memory_get_used(&v) >= 0);
+ log_info("Current memory usage: %s", FORMAT_BYTES(v));
+
+ assert_se(procfs_tasks_get_current(&v) >= 0);
+ log_info("Current number of tasks: %" PRIu64, v);
+
+ pid_max = TASKS_MAX;
+ r = procfs_get_pid_max(&pid_max);
+ if (r == -ENOENT || ERRNO_IS_NEG_PRIVILEGE(r))
+ return log_tests_skipped_errno(r, "can't get pid max");
+ assert(r >= 0);
+ log_info("kernel.pid_max: %"PRIu64, pid_max);
+
+ threads_max = TASKS_MAX;
+ r = procfs_get_threads_max(&threads_max);
+ if (r == -ENOENT || ERRNO_IS_NEG_PRIVILEGE(r))
+ return log_tests_skipped_errno(r, "can't get threads max");
+ assert(r >= 0);
+ log_info("kernel.threads-max: %"PRIu64, threads_max);
+
+ limit = MIN(pid_max - (pid_max > 0), threads_max);
+
+ assert_se(r >= 0);
+ log_info("Limit of tasks: %" PRIu64, limit);
+ assert_se(limit > 0);
+
+ /* This call should never fail, as we're trying to set it to the same limit */
+ assert(procfs_tasks_set_limit(limit) >= 0);
+
+ if (limit > 100) {
+ log_info("Reducing limit by one to %"PRIu64"…", limit-1);
+
+ r = procfs_tasks_set_limit(limit-1);
+ if (IN_SET(r, -ENOENT, -EROFS) || ERRNO_IS_PRIVILEGE(r))
+ return log_tests_skipped_errno(r, "can't set tasks limit");
+ assert_se(r >= 0);
+
+ assert_se(procfs_get_pid_max(&v) >= 0);
+ /* We never decrease the pid_max, so it shouldn't have changed */
+ assert_se(v == pid_max);
+
+ assert_se(procfs_get_threads_max(&v) >= 0);
+ assert_se(v == limit-1);
+
+ assert_se(procfs_tasks_set_limit(limit) >= 0);
+
+ assert_se(procfs_get_pid_max(&v) >= 0);
+ assert_se(v == pid_max);
+
+ assert_se(procfs_get_threads_max(&v) >= 0);
+ assert_se(v == limit);
+ }
+
+ return 0;
+}
diff --git a/src/test/test-psi-util.c b/src/test/test-psi-util.c
new file mode 100644
index 0000000..4ce0811
--- /dev/null
+++ b/src/test/test-psi-util.c
@@ -0,0 +1,78 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "parse-util.h"
+#include "psi-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+TEST(read_mem_pressure) {
+ _cleanup_(unlink_tempfilep) char path[] = "/tmp/pressurereadtestXXXXXX";
+ _cleanup_close_ int fd = -EBADF;
+ ResourcePressure rp;
+
+ if (geteuid() != 0)
+ return (void) log_tests_skipped("not root");
+
+ assert_se((fd = mkostemp_safe(path)) >= 0);
+
+ assert_se(read_resource_pressure("/verylikelynonexistentpath", PRESSURE_TYPE_SOME, &rp) < 0);
+ assert_se(read_resource_pressure(path, PRESSURE_TYPE_SOME, &rp) < 0);
+
+ assert_se(write_string_file(path, "herpdederp\n", WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(read_resource_pressure(path, PRESSURE_TYPE_SOME, &rp) < 0);
+
+ /* Pressure file with some invalid values */
+ assert_se(write_string_file(path, "some avg10=0.22=55 avg60=0.17=8 avg300=1.11=00 total=58761459\n"
+ "full avg10=0.23=55 avg60=0.16=8 avg300=1.08=00 total=58464525", WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(read_resource_pressure(path, PRESSURE_TYPE_SOME, &rp) < 0);
+
+ /* Same pressure valid values as below but with duplicate avg60 field */
+ assert_se(write_string_file(path, "some avg10=0.22 avg60=0.17 avg60=0.18 avg300=1.11 total=58761459\n"
+ "full avg10=0.23 avg60=0.16 avg300=1.08 total=58464525", WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(read_resource_pressure(path, PRESSURE_TYPE_SOME, &rp) < 0);
+
+ assert_se(write_string_file(path, "some avg10=0.22 avg60=0.17 avg300=1.11 total=58761459\n"
+ "full avg10=0.23 avg60=0.16 avg300=1.08 total=58464525", WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(read_resource_pressure(path, PRESSURE_TYPE_SOME, &rp) == 0);
+ assert_se(LOADAVG_INT_SIDE(rp.avg10) == 0);
+ assert_se(LOADAVG_DECIMAL_SIDE(rp.avg10) == 22);
+ assert_se(LOADAVG_INT_SIDE(rp.avg60) == 0);
+ assert_se(LOADAVG_DECIMAL_SIDE(rp.avg60) == 17);
+ assert_se(LOADAVG_INT_SIDE(rp.avg300) == 1);
+ assert_se(LOADAVG_DECIMAL_SIDE(rp.avg300) == 11);
+ assert_se(rp.total == 58761459);
+ assert_se(read_resource_pressure(path, PRESSURE_TYPE_FULL, &rp) == 0);
+ assert_se(LOADAVG_INT_SIDE(rp.avg10) == 0);
+ assert_se(LOADAVG_DECIMAL_SIDE(rp.avg10) == 23);
+ assert_se(LOADAVG_INT_SIDE(rp.avg60) == 0);
+ assert_se(LOADAVG_DECIMAL_SIDE(rp.avg60) == 16);
+ assert_se(LOADAVG_INT_SIDE(rp.avg300) == 1);
+ assert_se(LOADAVG_DECIMAL_SIDE(rp.avg300) == 8);
+ assert_se(rp.total == 58464525);
+
+ /* Pressure file with extra unsupported fields */
+ assert_se(write_string_file(path, "some avg5=0.55 avg10=0.22 avg60=0.17 avg300=1.11 total=58761459\n"
+ "full avg10=0.23 avg60=0.16 avg300=1.08 avg600=2.00 total=58464525", WRITE_STRING_FILE_CREATE) == 0);
+ assert_se(read_resource_pressure(path, PRESSURE_TYPE_SOME, &rp) == 0);
+ assert_se(LOADAVG_INT_SIDE(rp.avg10) == 0);
+ assert_se(LOADAVG_DECIMAL_SIDE(rp.avg10) == 22);
+ assert_se(LOADAVG_INT_SIDE(rp.avg60) == 0);
+ assert_se(LOADAVG_DECIMAL_SIDE(rp.avg60) == 17);
+ assert_se(LOADAVG_INT_SIDE(rp.avg300) == 1);
+ assert_se(LOADAVG_DECIMAL_SIDE(rp.avg300) == 11);
+ assert_se(rp.total == 58761459);
+ assert_se(read_resource_pressure(path, PRESSURE_TYPE_FULL, &rp) == 0);
+ assert_se(LOADAVG_INT_SIDE(rp.avg10) == 0);
+ assert_se(LOADAVG_DECIMAL_SIDE(rp.avg10) == 23);
+ assert_se(LOADAVG_INT_SIDE(rp.avg60) == 0);
+ assert_se(LOADAVG_DECIMAL_SIDE(rp.avg60) == 16);
+ assert_se(LOADAVG_INT_SIDE(rp.avg300) == 1);
+ assert_se(LOADAVG_DECIMAL_SIDE(rp.avg300) == 8);
+ assert_se(rp.total == 58464525);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-qrcode-util.c b/src/test/test-qrcode-util.c
new file mode 100644
index 0000000..221ad85
--- /dev/null
+++ b/src/test/test-qrcode-util.c
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "locale-util.h"
+#include "main-func.h"
+#include "qrcode-util.h"
+#include "tests.h"
+
+static int run(int argc, char **argv) {
+ int r;
+
+ test_setup_logging(LOG_DEBUG);
+
+ assert_se(setenv("SYSTEMD_COLORS", "1", 1) == 0); /* Force the qrcode to be printed */
+
+ r = print_qrcode(stdout, "This should say \"TEST\"", "TEST");
+ if (r == -EOPNOTSUPP)
+ return log_tests_skipped("not supported");
+ if (r < 0)
+ return log_error_errno(r, "Failed to print QR code: %m");
+ return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/test/test-random-util.c b/src/test/test-random-util.c
new file mode 100644
index 0000000..e597271
--- /dev/null
+++ b/src/test/test-random-util.c
@@ -0,0 +1,79 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <math.h>
+
+#include "hexdecoct.h"
+#include "log.h"
+#include "memory-util.h"
+#include "random-util.h"
+#include "terminal-util.h"
+#include "tests.h"
+
+TEST(random_bytes) {
+ uint8_t buf[16] = {};
+
+ for (size_t i = 1; i < sizeof buf; i++) {
+ random_bytes(buf, i);
+ if (i + 1 < sizeof buf)
+ assert_se(buf[i] == 0);
+
+ hexdump(stdout, buf, i);
+ }
+}
+
+TEST(crypto_random_bytes) {
+ uint8_t buf[16] = {};
+
+ for (size_t i = 1; i < sizeof buf; i++) {
+ assert_se(crypto_random_bytes(buf, i) == 0);
+ if (i + 1 < sizeof buf)
+ assert_se(buf[i] == 0);
+
+ hexdump(stdout, buf, i);
+ }
+}
+
+#define TOTAL 100000
+
+static void test_random_u64_range_one(unsigned mod) {
+ log_info("/* %s(%u) */", __func__, mod);
+
+ unsigned max = 0, count[mod];
+ zero(count);
+
+ for (unsigned i = 0; i < TOTAL; i++) {
+ uint64_t x;
+
+ x = random_u64_range(mod);
+
+ count[x]++;
+ max = MAX(max, count[x]);
+ }
+
+ /* Print histogram: vertical axis — value, horizontal axis — count.
+ *
+ * The expected value is always TOTAL/mod, because the distribution should be flat. The expected
+ * variance is TOTAL×p×(1-p), where p==1/mod, and standard deviation the root of the variance.
+ * Assert that the deviation from the expected value is less than 6 standard deviations.
+ */
+ unsigned scale = 2 * max / (columns() < 20 ? 80 : columns() - 20);
+ double exp = (double) TOTAL / mod;
+
+ for (size_t i = 0; i < mod; i++) {
+ double dev = (count[i] - exp) / sqrt(exp * (mod > 1 ? mod - 1 : 1) / mod);
+ log_debug("%02zu: %5u (%+.3f)%*s",
+ i, count[i], dev,
+ (int) (count[i] / scale), "x");
+
+ assert_se(fabs(dev) < 6); /* 6 sigma is excessive, but this check should be enough to
+ * identify catastrophic failure while minimizing false
+ * positives. */
+ }
+}
+
+TEST(random_u64_range) {
+ for (unsigned mod = 1; mod < 29; mod++)
+ test_random_u64_range_one(mod);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-ratelimit.c b/src/test/test-ratelimit.c
new file mode 100644
index 0000000..de208c7
--- /dev/null
+++ b/src/test/test-ratelimit.c
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <unistd.h>
+
+#include "macro.h"
+#include "ratelimit.h"
+#include "tests.h"
+#include "time-util.h"
+
+TEST(ratelimit_below) {
+ int i;
+ RateLimit ratelimit = { 1 * USEC_PER_SEC, 10 };
+
+ for (i = 0; i < 10; i++)
+ assert_se(ratelimit_below(&ratelimit));
+ assert_se(!ratelimit_below(&ratelimit));
+ sleep(1);
+ for (i = 0; i < 10; i++)
+ assert_se(ratelimit_below(&ratelimit));
+
+ ratelimit = (const RateLimit) { 0, 10 };
+ for (i = 0; i < 10000; i++)
+ assert_se(ratelimit_below(&ratelimit));
+}
+
+TEST(ratelimit_num_dropped) {
+ int i;
+ RateLimit ratelimit = { 1 * USEC_PER_SEC, 10 };
+
+ for (i = 0; i < 10; i++) {
+ assert_se(ratelimit_below(&ratelimit));
+ assert_se(ratelimit_num_dropped(&ratelimit) == 0);
+ }
+ assert_se(!ratelimit_below(&ratelimit));
+ assert_se(ratelimit_num_dropped(&ratelimit) == 1);
+ assert_se(!ratelimit_below(&ratelimit));
+ assert_se(ratelimit_num_dropped(&ratelimit) == 2);
+ sleep(1);
+ assert_se(ratelimit_below(&ratelimit));
+ assert_se(ratelimit_num_dropped(&ratelimit) == 0);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-raw-clone.c b/src/test/test-raw-clone.c
new file mode 100644
index 0000000..23ec7d1
--- /dev/null
+++ b/src/test/test-raw-clone.c
@@ -0,0 +1,41 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "errno-util.h"
+#include "format-util.h"
+#include "missing_syscall.h"
+#include "raw-clone.h"
+#include "tests.h"
+
+TEST(raw_clone) {
+ pid_t parent, pid, pid2;
+
+ parent = getpid();
+ log_info("before clone: getpid()→"PID_FMT, parent);
+ assert_se(raw_getpid() == parent);
+
+ pid = raw_clone(0);
+ assert_se(pid >= 0);
+
+ pid2 = raw_getpid();
+ log_info("raw_clone: "PID_FMT" getpid()→"PID_FMT" raw_getpid()→"PID_FMT,
+ pid, getpid(), pid2);
+ if (pid == 0) {
+ assert_se(pid2 != parent);
+ _exit(EXIT_SUCCESS);
+ } else {
+ int status;
+
+ assert_se(pid2 == parent);
+ waitpid(pid, &status, __WCLONE);
+ assert_se(WIFEXITED(status) && WEXITSTATUS(status) == EXIT_SUCCESS);
+ }
+
+ errno = 0;
+ assert_se(raw_clone(CLONE_FS|CLONE_NEWNS) == -1);
+ assert_se(errno == EINVAL || ERRNO_IS_PRIVILEGE(errno)); /* Certain container environments prohibit namespaces to us, don't fail in that case */
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-recurse-dir.c b/src/test/test-recurse-dir.c
new file mode 100644
index 0000000..8684d06
--- /dev/null
+++ b/src/test/test-recurse-dir.c
@@ -0,0 +1,177 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <ftw.h>
+
+#include "fd-util.h"
+#include "log.h"
+#include "missing_magic.h"
+#include "recurse-dir.h"
+#include "strv.h"
+#include "tests.h"
+
+static char **list_nftw = NULL;
+
+static int nftw_cb(
+ const char *fpath,
+ const struct stat *sb,
+ int typeflag,
+ struct FTW *ftwbuf) {
+
+ if (ftwbuf->level == 0) /* skip top-level */
+ return FTW_CONTINUE;
+
+ switch (typeflag) {
+
+ case FTW_F:
+ log_debug("ftw found %s", fpath);
+ assert_se(strv_extend(&list_nftw, fpath) >= 0);
+ break;
+
+ case FTW_SL:
+ log_debug("ftw found symlink %s, ignoring.", fpath);
+ break;
+
+ case FTW_D:
+ log_debug("ftw entering %s", fpath);
+ assert_se(strv_extendf(&list_nftw, "%s/", fpath) >= 0);
+ break;
+
+ case FTW_DNR:
+ log_debug("ftw open directory failed %s", fpath);
+ break;
+
+ case FTW_NS:
+ log_debug("ftw stat inode failed %s", fpath);
+ break;
+
+ case FTW_DP:
+ case FTW_SLN:
+ default:
+ assert_not_reached();
+ }
+
+ return FTW_CONTINUE;
+}
+
+static int recurse_dir_callback(
+ RecurseDirEvent event,
+ const char *path,
+ int dir_fd,
+ int inode_fd,
+ const struct dirent *de,
+ const struct statx *sx,
+ void *userdata) {
+
+ char ***l = userdata;
+
+ assert_se(path);
+ assert_se(de);
+
+ switch (event) {
+
+ case RECURSE_DIR_ENTRY:
+ assert_se(!IN_SET(de->d_type, DT_UNKNOWN, DT_DIR));
+
+ log_debug("found %s%s", path,
+ de->d_type == DT_LNK ? ", ignoring." : "");
+
+ if (de->d_type != DT_LNK)
+ assert_se(strv_extend(l, path) >= 0);
+ break;
+
+ case RECURSE_DIR_ENTER:
+ assert_se(de->d_type == DT_DIR);
+
+ log_debug("entering %s", path);
+ assert_se(strv_extendf(l, "%s/", path) >= 0);
+ break;
+
+ case RECURSE_DIR_LEAVE:
+ log_debug("leaving %s", path);
+ break;
+
+ case RECURSE_DIR_SKIP_MOUNT:
+ log_debug("skipping mount %s", path);
+ break;
+
+ case RECURSE_DIR_SKIP_DEPTH:
+ log_debug("skipping depth %s", path);
+ break;
+
+ case RECURSE_DIR_SKIP_OPEN_DIR_ERROR_BASE...RECURSE_DIR_SKIP_OPEN_DIR_ERROR_MAX:
+ log_debug_errno(event - RECURSE_DIR_SKIP_OPEN_DIR_ERROR_BASE, "failed to open dir %s: %m", path);
+ break;
+
+ case RECURSE_DIR_SKIP_OPEN_INODE_ERROR_BASE...RECURSE_DIR_SKIP_OPEN_INODE_ERROR_MAX:
+ log_debug_errno(event - RECURSE_DIR_SKIP_OPEN_INODE_ERROR_BASE, "failed to open inode %s: %m", path);
+ break;
+
+ case RECURSE_DIR_SKIP_STAT_INODE_ERROR_BASE...RECURSE_DIR_SKIP_STAT_INODE_ERROR_MAX:
+ log_debug_errno(event - RECURSE_DIR_SKIP_STAT_INODE_ERROR_BASE, "failed to stat inode %s: %m", path);
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ return RECURSE_DIR_CONTINUE;
+}
+
+int main(int argc, char *argv[]) {
+ _cleanup_strv_free_ char **list_recurse_dir = NULL;
+ const char *p;
+ usec_t t1, t2, t3, t4;
+ _cleanup_close_ int fd = -EBADF;
+
+ log_show_color(true);
+ test_setup_logging(LOG_INFO);
+
+ if (argc > 1)
+ p = argv[1];
+ else
+ p = "/usr/share/man"; /* something hopefully reasonably stable while we run (and limited in size) */
+
+ fd = open(p, O_DIRECTORY|O_CLOEXEC);
+ if (fd < 0 && errno == ENOENT)
+ return log_tests_skipped_errno(errno, "Couldn't open directory %s", p);
+ assert_se(fd >= 0);
+
+ /* If the test directory is on an overlayfs then files and their directory may return different
+ * st_dev in stat results, which confuses nftw into thinking they're on different filesystems and
+ * won't return the result when the FTW_MOUNT flag is set. */
+ if (fd_is_fs_type(fd, OVERLAYFS_SUPER_MAGIC))
+ return log_tests_skipped("nftw mountpoint detection produces false-positives on overlayfs");
+
+ /* Enumerate the specified dirs in full, once via nftw(), and once via recurse_dir(), and ensure the
+ * results are identical. nftw() sometimes skips symlinks (see
+ * https://github.com/systemd/systemd/issues/29603), so ignore them to avoid bogus errors. */
+
+ t1 = now(CLOCK_MONOTONIC);
+ assert_se(recurse_dir(fd, p, 0, UINT_MAX, RECURSE_DIR_SORT|RECURSE_DIR_ENSURE_TYPE|RECURSE_DIR_SAME_MOUNT, recurse_dir_callback, &list_recurse_dir) >= 0);
+ t2 = now(CLOCK_MONOTONIC);
+
+ t3 = now(CLOCK_MONOTONIC);
+ assert_se(nftw(p, nftw_cb, 64, FTW_PHYS|FTW_MOUNT) >= 0);
+ t4 = now(CLOCK_MONOTONIC);
+
+ log_info("recurse_dir(): %s – nftw(): %s", FORMAT_TIMESPAN(t2 - t1, 1), FORMAT_TIMESPAN(t4 - t3, 1));
+
+ strv_sort(list_recurse_dir);
+ strv_sort(list_nftw);
+
+ for (size_t i = 0;; i++) {
+ const char *a = list_nftw ? list_nftw[i] : NULL,
+ *b = list_recurse_dir ? list_recurse_dir[i] : NULL;
+
+ if (!streq_ptr(a, b)) {
+ log_error("entry %zu different: %s vs %s", i, strna(a), strna(b));
+ assert_not_reached();
+ }
+
+ if (!a)
+ break;
+ }
+
+ list_nftw = strv_free(list_nftw);
+ return 0;
+}
diff --git a/src/test/test-replace-var.c b/src/test/test-replace-var.c
new file mode 100644
index 0000000..f861b27
--- /dev/null
+++ b/src/test/test-replace-var.c
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+
+#include "macro.h"
+#include "replace-var.h"
+#include "string-util.h"
+#include "tests.h"
+
+static char *lookup(const char *variable, void *userdata) {
+ return strjoin("<<<", variable, ">>>");
+}
+
+TEST(replace_var) {
+ char *r;
+
+ assert_se(r = replace_var("@@@foobar@xyz@HALLO@foobar@test@@testtest@TEST@...@@@", lookup, NULL));
+ puts(r);
+ assert_se(streq(r, "@@@foobar@xyz<<<HALLO>>>foobar@test@@testtest<<<TEST>>>...@@@"));
+ free(r);
+}
+
+TEST(strreplace) {
+ char *r;
+
+ assert_se(r = strreplace("XYZFFFFXYZFFFFXYZ", "XYZ", "ABC"));
+ puts(r);
+ assert_se(streq(r, "ABCFFFFABCFFFFABC"));
+ free(r);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-rlimit-util.c b/src/test/test-rlimit-util.c
new file mode 100644
index 0000000..86d0c04
--- /dev/null
+++ b/src/test/test-rlimit-util.c
@@ -0,0 +1,139 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/resource.h>
+
+#include "alloc-util.h"
+#include "capability-util.h"
+#include "macro.h"
+#include "missing_resource.h"
+#include "rlimit-util.h"
+#include "string-util.h"
+#include "tests.h"
+#include "time-util.h"
+
+static void test_rlimit_parse_format_one(int resource, const char *string, rlim_t soft, rlim_t hard, int ret, const char *formatted) {
+ _cleanup_free_ char *f = NULL;
+ struct rlimit rl = {
+ .rlim_cur = 4711,
+ .rlim_max = 4712,
+ }, rl2 = {
+ .rlim_cur = 4713,
+ .rlim_max = 4714
+ };
+
+ assert_se(rlimit_parse(resource, string, &rl) == ret);
+ if (ret < 0)
+ return;
+
+ assert_se(rl.rlim_cur == soft);
+ assert_se(rl.rlim_max == hard);
+
+ assert_se(rlimit_format(&rl, &f) >= 0);
+ assert_se(streq(formatted, f));
+
+ assert_se(rlimit_parse(resource, formatted, &rl2) >= 0);
+ assert_se(memcmp(&rl, &rl2, sizeof(struct rlimit)) == 0);
+}
+
+TEST(rlimit_parse_format) {
+ test_rlimit_parse_format_one(RLIMIT_NOFILE, "4:5", 4, 5, 0, "4:5");
+ test_rlimit_parse_format_one(RLIMIT_NOFILE, "6", 6, 6, 0, "6");
+ test_rlimit_parse_format_one(RLIMIT_NOFILE, "infinity", RLIM_INFINITY, RLIM_INFINITY, 0, "infinity");
+ test_rlimit_parse_format_one(RLIMIT_NOFILE, "infinity:infinity", RLIM_INFINITY, RLIM_INFINITY, 0, "infinity");
+ test_rlimit_parse_format_one(RLIMIT_NOFILE, "8:infinity", 8, RLIM_INFINITY, 0, "8:infinity");
+ test_rlimit_parse_format_one(RLIMIT_CPU, "25min:13h", (25*USEC_PER_MINUTE) / USEC_PER_SEC, (13*USEC_PER_HOUR) / USEC_PER_SEC, 0, "1500:46800");
+ test_rlimit_parse_format_one(RLIMIT_NOFILE, "", 0, 0, -EINVAL, NULL);
+ test_rlimit_parse_format_one(RLIMIT_NOFILE, "5:4", 0, 0, -EILSEQ, NULL);
+ test_rlimit_parse_format_one(RLIMIT_NOFILE, "5:4:3", 0, 0, -EINVAL, NULL);
+ test_rlimit_parse_format_one(RLIMIT_NICE, "20", 20, 20, 0, "20");
+ test_rlimit_parse_format_one(RLIMIT_NICE, "40", 40, 40, 0, "40");
+ test_rlimit_parse_format_one(RLIMIT_NICE, "41", 41, 41, -ERANGE, "41");
+ test_rlimit_parse_format_one(RLIMIT_NICE, "0", 0, 0, 0, "0");
+ test_rlimit_parse_format_one(RLIMIT_NICE, "-7", 27, 27, 0, "27");
+ test_rlimit_parse_format_one(RLIMIT_NICE, "-20", 40, 40, 0, "40");
+ test_rlimit_parse_format_one(RLIMIT_NICE, "-21", 41, 41, -ERANGE, "41");
+ test_rlimit_parse_format_one(RLIMIT_NICE, "-0", 20, 20, 0, "20");
+ test_rlimit_parse_format_one(RLIMIT_NICE, "+7", 13, 13, 0, "13");
+ test_rlimit_parse_format_one(RLIMIT_NICE, "+19", 1, 1, 0, "1");
+ test_rlimit_parse_format_one(RLIMIT_NICE, "+20", 0, 0, -ERANGE, "0");
+ test_rlimit_parse_format_one(RLIMIT_NICE, "+0", 20, 20, 0, "20");
+}
+
+TEST(rlimit_from_string) {
+ assert_se(rlimit_from_string("NOFILE") == RLIMIT_NOFILE);
+ assert_se(rlimit_from_string("LimitNOFILE") == -EINVAL);
+ assert_se(rlimit_from_string("RLIMIT_NOFILE") == -EINVAL);
+ assert_se(rlimit_from_string("xxxNOFILE") == -EINVAL);
+ assert_se(rlimit_from_string("DefaultLimitNOFILE") == -EINVAL);
+}
+
+TEST(rlimit_from_string_harder) {
+ assert_se(rlimit_from_string_harder("NOFILE") == RLIMIT_NOFILE);
+ assert_se(rlimit_from_string_harder("LimitNOFILE") == RLIMIT_NOFILE);
+ assert_se(rlimit_from_string_harder("RLIMIT_NOFILE") == RLIMIT_NOFILE);
+ assert_se(rlimit_from_string_harder("xxxNOFILE") == -EINVAL);
+ assert_se(rlimit_from_string_harder("DefaultLimitNOFILE") == -EINVAL);
+}
+
+TEST(rlimit_to_string_all) {
+ for (int i = 0; i < _RLIMIT_MAX; i++) {
+ _cleanup_free_ char *prefixed = NULL;
+ const char *p;
+
+ assert_se(p = rlimit_to_string(i));
+ log_info("%i = %s", i, p);
+
+ assert_se(rlimit_from_string(p) == i);
+ assert_se(rlimit_from_string_harder(p) == i);
+
+ assert_se(prefixed = strjoin("Limit", p));
+
+ assert_se(rlimit_from_string(prefixed) < 0);
+ assert_se(rlimit_from_string_harder(prefixed) == i);
+
+ prefixed = mfree(prefixed);
+ assert_se(prefixed = strjoin("RLIMIT_", p));
+
+ assert_se(rlimit_from_string(prefixed) < 0);
+ assert_se(rlimit_from_string_harder(prefixed) == i);
+ }
+}
+
+TEST(setrlimit) {
+ struct rlimit old, new, high;
+ struct rlimit err = {
+ .rlim_cur = 10,
+ .rlim_max = 5,
+ };
+
+ assert_se(drop_capability(CAP_SYS_RESOURCE) == 0);
+
+ assert_se(getrlimit(RLIMIT_NOFILE, &old) == 0);
+ new.rlim_cur = MIN(5U, old.rlim_max);
+ new.rlim_max = old.rlim_max;
+ assert_se(setrlimit(RLIMIT_NOFILE, &new) >= 0);
+
+ assert_se(streq_ptr(rlimit_to_string(RLIMIT_NOFILE), "NOFILE"));
+ assert_se(rlimit_to_string(-1) == NULL);
+
+ assert_se(getrlimit(RLIMIT_NOFILE, &old) == 0);
+ assert_se(setrlimit_closest(RLIMIT_NOFILE, &old) == 0);
+ assert_se(getrlimit(RLIMIT_NOFILE, &new) == 0);
+ assert_se(old.rlim_cur == new.rlim_cur);
+ assert_se(old.rlim_max == new.rlim_max);
+
+ assert_se(getrlimit(RLIMIT_NOFILE, &old) == 0);
+ high = RLIMIT_MAKE_CONST(old.rlim_max == RLIM_INFINITY ? old.rlim_max : old.rlim_max + 1);
+ assert_se(setrlimit_closest(RLIMIT_NOFILE, &high) == 0);
+ assert_se(getrlimit(RLIMIT_NOFILE, &new) == 0);
+ assert_se(new.rlim_max == old.rlim_max);
+ assert_se(new.rlim_cur == new.rlim_max);
+
+ assert_se(getrlimit(RLIMIT_NOFILE, &old) == 0);
+ assert_se(setrlimit_closest(RLIMIT_NOFILE, &err) == -EINVAL);
+ assert_se(getrlimit(RLIMIT_NOFILE, &new) == 0);
+ assert_se(old.rlim_cur == new.rlim_cur);
+ assert_se(old.rlim_max == new.rlim_max);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-rm-rf.c b/src/test/test-rm-rf.c
new file mode 100644
index 0000000..4c69bd2
--- /dev/null
+++ b/src/test/test-rm-rf.c
@@ -0,0 +1,114 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "process-util.h"
+#include "rm-rf.h"
+#include "string-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+static void test_rm_rf_chmod_inner(void) {
+ _cleanup_(rm_rf_physical_and_freep) char *d = NULL;
+ const char *a, *b, *x, *y;
+ struct stat st;
+
+ assert_se(getuid() != 0);
+
+ assert_se(mkdtemp_malloc("/tmp/test-rm-rf.XXXXXXX", &d) >= 0);
+ a = strjoina(d, "/a");
+ b = strjoina(a, "/b");
+ x = strjoina(d, "/x");
+ y = strjoina(x, "/y");
+
+ assert_se(mkdir(x, 0700) >= 0);
+ assert_se(mknod(y, S_IFREG | 0600, 0) >= 0);
+
+ assert_se(chmod(y, 0400) >= 0);
+ assert_se(chmod(x, 0500) >= 0);
+ assert_se(chmod(d, 0500) >= 0);
+
+ assert_se(rm_rf(d, REMOVE_PHYSICAL) == -EACCES);
+
+ assert_se(access(d, F_OK) >= 0);
+ assert_se(access(x, F_OK) >= 0);
+ assert_se(access(y, F_OK) >= 0);
+
+ assert_se(rm_rf(d, REMOVE_PHYSICAL|REMOVE_CHMOD) >= 0);
+
+ assert_se(access(d, F_OK) >= 0);
+ assert_se(access(x, F_OK) < 0 && errno == ENOENT);
+ assert_se(access(y, F_OK) < 0 && errno == ENOENT);
+
+ assert_se(mkdir(a, 0700) >= 0);
+ assert_se(mkdir(b, 0700) >= 0);
+ assert_se(mkdir(x, 0700) >= 0);
+ assert_se(mknod(y, S_IFREG | 0600, 0) >= 0);
+
+ assert_se(chmod(b, 0000) >= 0);
+ assert_se(chmod(a, 0000) >= 0);
+ assert_se(chmod(y, 0000) >= 0);
+ assert_se(chmod(x, 0000) >= 0);
+ assert_se(chmod(d, 0500) >= 0);
+
+ assert_se(rm_rf(d, REMOVE_PHYSICAL|REMOVE_CHMOD|REMOVE_CHMOD_RESTORE|REMOVE_ONLY_DIRECTORIES) == -ENOTEMPTY);
+
+ assert_se(access(a, F_OK) < 0 && errno == ENOENT);
+ assert_se(access(d, F_OK) >= 0);
+ assert_se(stat(d, &st) >= 0 && (st.st_mode & 07777) == 0500);
+ assert_se(access(x, F_OK) >= 0);
+ assert_se(stat(x, &st) >= 0 && (st.st_mode & 07777) == 0000);
+ assert_se(chmod(x, 0700) >= 0);
+ assert_se(access(y, F_OK) >= 0);
+ assert_se(stat(y, &st) >= 0 && (st.st_mode & 07777) == 0000);
+
+ assert_se(chmod(y, 0000) >= 0);
+ assert_se(chmod(x, 0000) >= 0);
+ assert_se(chmod(d, 0000) >= 0);
+
+ assert_se(rm_rf(d, REMOVE_PHYSICAL|REMOVE_CHMOD|REMOVE_CHMOD_RESTORE) >= 0);
+
+ assert_se(stat(d, &st) >= 0 && (st.st_mode & 07777) == 0000);
+ assert_se(access(d, F_OK) >= 0);
+ assert_se(chmod(d, 0700) >= 0);
+ assert_se(access(x, F_OK) < 0 && errno == ENOENT);
+
+ assert_se(mkdir(x, 0700) >= 0);
+ assert_se(mknod(y, S_IFREG | 0600, 0) >= 0);
+
+ assert_se(chmod(y, 0000) >= 0);
+ assert_se(chmod(x, 0000) >= 0);
+ assert_se(chmod(d, 0000) >= 0);
+
+ assert_se(rm_rf(d, REMOVE_PHYSICAL|REMOVE_CHMOD|REMOVE_ROOT) >= 0);
+
+ assert_se(access(d, F_OK) < 0 && errno == ENOENT);
+}
+
+TEST(rm_rf_chmod) {
+ int r;
+
+ if (getuid() == 0) {
+ /* This test only works unpriv (as only then the access mask for the owning user matters),
+ * hence drop privs here */
+
+ r = safe_fork("(setresuid)", FORK_DEATHSIG_SIGTERM|FORK_WAIT, NULL);
+ assert_se(r >= 0);
+
+ if (r == 0) {
+ /* child */
+
+ assert_se(setresuid(1, 1, 1) >= 0);
+
+ test_rm_rf_chmod_inner();
+ _exit(EXIT_SUCCESS);
+ }
+
+ return;
+ }
+
+ test_rm_rf_chmod_inner();
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-sbat.c b/src/test/test-sbat.c
new file mode 100644
index 0000000..d8546b1
--- /dev/null
+++ b/src/test/test-sbat.c
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+/* We include efi_config.h after undefining PROJECT_VERSION which is also defined in config.h. */
+#undef PROJECT_VERSION
+#include "efi_config.h"
+
+#include "build.h"
+#include "sbat.h"
+#include "tests.h"
+
+TEST(sbat_section_text) {
+ log_info("---SBAT-----------&<----------------------------------------\n"
+ "%s"
+ "%s"
+ "------------------>&-----------------------------------------",
+#ifdef SBAT_DISTRO
+ SBAT_BOOT_SECTION_TEXT,
+ SBAT_STUB_SECTION_TEXT
+#else
+ "(not defined)",
+ ""
+#endif
+ );
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-sched-prio.c b/src/test/test-sched-prio.c
new file mode 100644
index 0000000..3c3b8dc
--- /dev/null
+++ b/src/test/test-sched-prio.c
@@ -0,0 +1,81 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/***
+ Copyright © 2012 Holger Hans Peter Freyther
+***/
+
+#include <sched.h>
+
+#include "all-units.h"
+#include "macro.h"
+#include "manager.h"
+#include "rm-rf.h"
+#include "tests.h"
+
+int main(int argc, char *argv[]) {
+ _cleanup_(rm_rf_physical_and_freep) char *runtime_dir = NULL;
+ _cleanup_(manager_freep) Manager *m = NULL;
+ Unit *idle_ok, *idle_bad, *rr_ok, *rr_bad, *rr_sched;
+ Service *ser;
+ int r;
+
+ test_setup_logging(LOG_INFO);
+
+ r = enter_cgroup_subroot(NULL);
+ if (r == -ENOMEDIUM)
+ return log_tests_skipped("cgroupfs not available");
+
+ /* prepare the test */
+ _cleanup_free_ char *unit_dir = NULL;
+ assert_se(get_testdata_dir("units", &unit_dir) >= 0);
+ assert_se(set_unit_path(unit_dir) >= 0);
+ assert_se(runtime_dir = setup_fake_runtime_dir());
+
+ r = manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_BASIC, &m);
+ if (manager_errno_skip_test(r))
+ return log_tests_skipped_errno(r, "manager_new");
+ assert_se(r >= 0);
+ assert_se(manager_startup(m, NULL, NULL, NULL) >= 0);
+
+ /* load idle ok */
+ assert_se(manager_load_startable_unit_or_warn(m, "sched_idle_ok.service", NULL, &idle_ok) >= 0);
+ ser = SERVICE(idle_ok);
+ assert_se(ser->exec_context.cpu_sched_policy == SCHED_OTHER);
+ assert_se(ser->exec_context.cpu_sched_priority == 0);
+
+ /*
+ * load idle bad. This should print a warning but we have no way to look at it.
+ */
+ assert_se(manager_load_startable_unit_or_warn(m, "sched_idle_bad.service", NULL, &idle_bad) >= 0);
+ ser = SERVICE(idle_ok);
+ assert_se(ser->exec_context.cpu_sched_policy == SCHED_OTHER);
+ assert_se(ser->exec_context.cpu_sched_priority == 0);
+
+ /*
+ * load rr ok.
+ * Test that the default priority is moving from 0 to 1.
+ */
+ assert_se(manager_load_startable_unit_or_warn(m, "sched_rr_ok.service", NULL, &rr_ok) >= 0);
+ ser = SERVICE(rr_ok);
+ assert_se(ser->exec_context.cpu_sched_policy == SCHED_RR);
+ assert_se(ser->exec_context.cpu_sched_priority == 1);
+
+ /*
+ * load rr bad.
+ * Test that the value of 0 and 100 is ignored.
+ */
+ assert_se(manager_load_startable_unit_or_warn(m, "sched_rr_bad.service", NULL, &rr_bad) >= 0);
+ ser = SERVICE(rr_bad);
+ assert_se(ser->exec_context.cpu_sched_policy == SCHED_RR);
+ assert_se(ser->exec_context.cpu_sched_priority == 1);
+
+ /*
+ * load rr change.
+ * Test that anything between 1 and 99 can be set.
+ */
+ assert_se(manager_load_startable_unit_or_warn(m, "sched_rr_change.service", NULL, &rr_sched) >= 0);
+ ser = SERVICE(rr_sched);
+ assert_se(ser->exec_context.cpu_sched_policy == SCHED_RR);
+ assert_se(ser->exec_context.cpu_sched_priority == 99);
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/test/test-sd-hwdb.c b/src/test/test-sd-hwdb.c
new file mode 100644
index 0000000..ecb6118
--- /dev/null
+++ b/src/test/test-sd-hwdb.c
@@ -0,0 +1,85 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-hwdb.h"
+
+#include "alloc-util.h"
+#include "errno-util.h"
+#include "errno.h"
+#include "hwdb-internal.h"
+#include "nulstr-util.h"
+#include "tests.h"
+
+TEST(failed_enumerate) {
+ _cleanup_(sd_hwdb_unrefp) sd_hwdb *hwdb = NULL;
+ const char *key, *value;
+
+ assert_se(sd_hwdb_new(&hwdb) == 0);
+
+ assert_se(sd_hwdb_seek(hwdb, "no-such-modalias-should-exist") == 0);
+
+ assert_se(sd_hwdb_enumerate(hwdb, &key, &value) == 0);
+ assert_se(sd_hwdb_enumerate(hwdb, &key, NULL) == -EINVAL);
+ assert_se(sd_hwdb_enumerate(hwdb, NULL, &value) == -EINVAL);
+}
+
+#define DELL_MODALIAS \
+ "evdev:atkbd:dmi:bvnXXX:bvrYYY:bdZZZ:svnDellXXX:pnYYY:"
+
+TEST(basic_enumerate) {
+ _cleanup_(sd_hwdb_unrefp) sd_hwdb *hwdb = NULL;
+ const char *key, *value;
+ size_t len1 = 0, len2 = 0;
+ int r;
+
+ assert_se(sd_hwdb_new(&hwdb) == 0);
+
+ assert_se(sd_hwdb_seek(hwdb, DELL_MODALIAS) == 0);
+
+ for (;;) {
+ r = sd_hwdb_enumerate(hwdb, &key, &value);
+ assert_se(IN_SET(r, 0, 1));
+ if (r == 0)
+ break;
+ assert_se(key);
+ assert_se(value);
+ log_debug("A: \"%s\" → \"%s\"", key, value);
+ len1 += strlen(key) + strlen(value);
+ }
+
+ SD_HWDB_FOREACH_PROPERTY(hwdb, DELL_MODALIAS, key, value) {
+ log_debug("B: \"%s\" → \"%s\"", key, value);
+ len2 += strlen(key) + strlen(value);
+ }
+
+ assert_se(len1 == len2);
+}
+
+TEST(sd_hwdb_new_from_path) {
+ _cleanup_(sd_hwdb_unrefp) sd_hwdb *hwdb = NULL;
+ int r;
+
+ assert_se(sd_hwdb_new_from_path(NULL, &hwdb) == -EINVAL);
+ assert_se(sd_hwdb_new_from_path("", &hwdb) == -EINVAL);
+ assert_se(sd_hwdb_new_from_path("/path/that/should/not/exist", &hwdb) < 0);
+
+ NULSTR_FOREACH(hwdb_bin_path, hwdb_bin_paths) {
+ r = sd_hwdb_new_from_path(hwdb_bin_path, &hwdb);
+ if (r >= 0)
+ break;
+ }
+
+ assert_se(r >= 0);
+}
+
+static int intro(void) {
+ _cleanup_(sd_hwdb_unrefp) sd_hwdb *hwdb = NULL;
+ int r;
+
+ r = sd_hwdb_new(&hwdb);
+ if (r == -ENOENT || ERRNO_IS_PRIVILEGE(r))
+ return log_tests_skipped_errno(r, "cannot open hwdb");
+
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_DEBUG, intro);
diff --git a/src/test/test-sd-path.c b/src/test/test-sd-path.c
new file mode 100644
index 0000000..4f23e3b
--- /dev/null
+++ b/src/test/test-sd-path.c
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-path.h"
+
+#include "alloc-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+
+TEST(sd_path_lookup) {
+ for (uint64_t i = 0; i < _SD_PATH_MAX; i++) {
+ _cleanup_free_ char *t = NULL, *s = NULL;
+ int r;
+
+ r = sd_path_lookup(i, NULL, &t);
+ if (i == SD_PATH_USER_RUNTIME && r == -ENXIO)
+ continue;
+ assert_se(r == 0);
+ assert_se(t);
+ log_info("%02"PRIu64": \"%s\"", i, t);
+
+ assert_se(sd_path_lookup(i, "suffix", &s) == 0);
+ assert_se(s);
+ log_info("%02"PRIu64": \"%s\"", i, s);
+ assert_se(endswith(s, "/suffix"));
+ }
+
+ char *tt;
+ assert_se(sd_path_lookup(_SD_PATH_MAX, NULL, &tt) == -EOPNOTSUPP);
+}
+
+TEST(sd_path_lookup_strv) {
+ for (uint64_t i = 0; i < _SD_PATH_MAX; i++) {
+ _cleanup_strv_free_ char **t = NULL, **s = NULL;
+ int r;
+
+ r = sd_path_lookup_strv(i, NULL, &t);
+ if (i == SD_PATH_USER_RUNTIME && r == -ENXIO)
+ continue;
+ assert_se(r == 0);
+ assert_se(t);
+ log_info("%02"PRIu64":", i);
+ STRV_FOREACH(item, t)
+ log_debug(" %s", *item);
+
+ assert_se(sd_path_lookup_strv(i, "suffix", &s) == 0);
+ assert_se(s);
+ log_info("%02"PRIu64":", i);
+ STRV_FOREACH(item, s) {
+ assert_se(endswith(*item, "/suffix"));
+ log_debug(" %s", *item);
+ }
+ }
+
+ char *tt;
+ assert_se(sd_path_lookup(_SD_PATH_MAX, NULL, &tt) == -EOPNOTSUPP);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-seccomp.c b/src/test/test-seccomp.c
new file mode 100644
index 0000000..279a155
--- /dev/null
+++ b/src/test/test-seccomp.c
@@ -0,0 +1,1234 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <poll.h>
+#include <stdlib.h>
+#include <sys/eventfd.h>
+#include <sys/mman.h>
+#include <sys/personality.h>
+#include <sys/shm.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <unistd.h>
+#if HAVE_VALGRIND_VALGRIND_H
+#include <valgrind/valgrind.h>
+#endif
+
+#include "alloc-util.h"
+#include "capability-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "macro.h"
+#include "memory-util.h"
+#include "missing_sched.h"
+#include "missing_syscall.h"
+#include "nsflags.h"
+#include "nulstr-util.h"
+#include "process-util.h"
+#include "raw-clone.h"
+#include "rm-rf.h"
+#include "seccomp-util.h"
+#include "set.h"
+#include "string-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+#include "virt.h"
+
+/* __NR_socket may be invalid due to libseccomp */
+#if !defined(__NR_socket) || __NR_socket < 0 || defined(__i386__) || defined(__s390x__) || defined(__s390__) || defined(__powerpc64__) || defined(__powerpc__)
+/* On these archs, socket() is implemented via the socketcall() syscall multiplexer,
+ * and we can't restrict it hence via seccomp. */
+# define SECCOMP_RESTRICT_ADDRESS_FAMILIES_BROKEN 1
+#else
+# define SECCOMP_RESTRICT_ADDRESS_FAMILIES_BROKEN 0
+#endif
+
+static bool have_seccomp_privs(void) {
+ return geteuid() == 0 && have_effective_cap(CAP_SYS_ADMIN) > 0; /* If we are root but CAP_SYS_ADMIN we can't do caps (unless we also do NNP) */
+}
+
+TEST(parse_syscall_and_errno) {
+ _cleanup_free_ char *n = NULL;
+ int e;
+
+ assert_se(parse_syscall_and_errno("uname:EILSEQ", &n, &e) >= 0);
+ assert_se(streq(n, "uname"));
+ assert_se(e == errno_from_name("EILSEQ") && e >= 0);
+ n = mfree(n);
+
+ assert_se(parse_syscall_and_errno("uname:EINVAL", &n, &e) >= 0);
+ assert_se(streq(n, "uname"));
+ assert_se(e == errno_from_name("EINVAL") && e >= 0);
+ n = mfree(n);
+
+ assert_se(parse_syscall_and_errno("@sync:4095", &n, &e) >= 0);
+ assert_se(streq(n, "@sync"));
+ assert_se(e == 4095);
+ n = mfree(n);
+
+ /* If errno is omitted, then e is set to -1 */
+ assert_se(parse_syscall_and_errno("mount", &n, &e) >= 0);
+ assert_se(streq(n, "mount"));
+ assert_se(e == -1);
+ n = mfree(n);
+
+ /* parse_syscall_and_errno() does not check the syscall name is valid or not. */
+ assert_se(parse_syscall_and_errno("hoge:255", &n, &e) >= 0);
+ assert_se(streq(n, "hoge"));
+ assert_se(e == 255);
+ n = mfree(n);
+
+ /* 0 is also a valid errno. */
+ assert_se(parse_syscall_and_errno("hoge:0", &n, &e) >= 0);
+ assert_se(streq(n, "hoge"));
+ assert_se(e == 0);
+ n = mfree(n);
+
+ assert_se(parse_syscall_and_errno("hoge:kill", &n, &e) >= 0);
+ assert_se(streq(n, "hoge"));
+ assert_se(e == SECCOMP_ERROR_NUMBER_KILL);
+ n = mfree(n);
+
+ /* The function checks the syscall name is empty or not. */
+ assert_se(parse_syscall_and_errno("", &n, &e) == -EINVAL);
+ assert_se(parse_syscall_and_errno(":255", &n, &e) == -EINVAL);
+
+ /* errno must be a valid errno name or number between 0 and ERRNO_MAX == 4095, or "kill" */
+ assert_se(parse_syscall_and_errno("hoge:4096", &n, &e) == -ERANGE);
+ assert_se(parse_syscall_and_errno("hoge:-3", &n, &e) == -ERANGE);
+ assert_se(parse_syscall_and_errno("hoge:12.3", &n, &e) == -EINVAL);
+ assert_se(parse_syscall_and_errno("hoge:123junk", &n, &e) == -EINVAL);
+ assert_se(parse_syscall_and_errno("hoge:junk123", &n, &e) == -EINVAL);
+ assert_se(parse_syscall_and_errno("hoge:255:EILSEQ", &n, &e) == -EINVAL);
+ assert_se(parse_syscall_and_errno("hoge:-EINVAL", &n, &e) == -EINVAL);
+ assert_se(parse_syscall_and_errno("hoge:EINVALaaa", &n, &e) == -EINVAL);
+ assert_se(parse_syscall_and_errno("hoge:", &n, &e) == -EINVAL);
+}
+
+TEST(seccomp_arch_to_string) {
+ uint32_t a, b;
+ const char *name;
+
+ a = seccomp_arch_native();
+ assert_se(a > 0);
+ name = seccomp_arch_to_string(a);
+ assert_se(name);
+ assert_se(seccomp_arch_from_string(name, &b) >= 0);
+ assert_se(a == b);
+}
+
+TEST(architecture_table) {
+ const char *n2;
+
+ NULSTR_FOREACH(n,
+ "native\0"
+ "x86\0"
+ "x86-64\0"
+ "x32\0"
+ "arm\0"
+ "arm64\0"
+#ifdef SCMP_ARCH_LOONGARCH64
+ "loongarch64\0"
+#endif
+ "mips\0"
+ "mips64\0"
+ "mips64-n32\0"
+ "mips-le\0"
+ "mips64-le\0"
+ "mips64-le-n32\0"
+ "parisc\0"
+ "parisc64\0"
+ "ppc\0"
+ "ppc64\0"
+ "ppc64-le\0"
+#ifdef SCMP_ARCH_RISCV64
+ "riscv64\0"
+#endif
+ "s390\0"
+ "s390x\0") {
+ uint32_t c;
+
+ assert_se(seccomp_arch_from_string(n, &c) >= 0);
+ n2 = seccomp_arch_to_string(c);
+ log_info("seccomp-arch: %s → 0x%"PRIx32" → %s", n, c, n2);
+ assert_se(streq_ptr(n, n2));
+ }
+}
+
+TEST(syscall_filter_set_find) {
+ assert_se(!syscall_filter_set_find(NULL));
+ assert_se(!syscall_filter_set_find(""));
+ assert_se(!syscall_filter_set_find("quux"));
+ assert_se(!syscall_filter_set_find("@quux"));
+
+ assert_se(syscall_filter_set_find("@clock") == syscall_filter_sets + SYSCALL_FILTER_SET_CLOCK);
+ assert_se(syscall_filter_set_find("@default") == syscall_filter_sets + SYSCALL_FILTER_SET_DEFAULT);
+ assert_se(syscall_filter_set_find("@raw-io") == syscall_filter_sets + SYSCALL_FILTER_SET_RAW_IO);
+}
+
+TEST(filter_sets) {
+ if (!is_seccomp_available()) {
+ log_notice("Seccomp not available, skipping %s", __func__);
+ return;
+ }
+ if (!have_seccomp_privs()) {
+ log_notice("Not privileged, skipping %s", __func__);
+ return;
+ }
+
+ for (unsigned i = 0; i < _SYSCALL_FILTER_SET_MAX; i++) {
+ pid_t pid;
+
+#if HAVE_VALGRIND_VALGRIND_H
+ if (RUNNING_ON_VALGRIND && IN_SET(i, SYSCALL_FILTER_SET_DEFAULT, SYSCALL_FILTER_SET_BASIC_IO, SYSCALL_FILTER_SET_SIGNAL)) {
+ /* valgrind at least requires rt_sigprocmask(), read(), write(). */
+ log_info("Running on valgrind, skipping %s", syscall_filter_sets[i].name);
+ continue;
+ }
+#endif
+#if HAS_FEATURE_ADDRESS_SANITIZER
+ if (IN_SET(i, SYSCALL_FILTER_SET_DEFAULT, SYSCALL_FILTER_SET_BASIC_IO, SYSCALL_FILTER_SET_SIGNAL)) {
+ /* ASAN at least requires sigaltstack(), read(), write(). */
+ log_info("Running on address sanitizer, skipping %s", syscall_filter_sets[i].name);
+ continue;
+ }
+#endif
+
+ log_info("Testing %s", syscall_filter_sets[i].name);
+
+ pid = fork();
+ assert_se(pid >= 0);
+
+ if (pid == 0) { /* Child? */
+ int fd, r;
+
+ /* If we look at the default set (or one that includes it), allow-list instead of deny-list */
+ if (IN_SET(i, SYSCALL_FILTER_SET_DEFAULT,
+ SYSCALL_FILTER_SET_SYSTEM_SERVICE,
+ SYSCALL_FILTER_SET_KNOWN))
+ r = seccomp_load_syscall_filter_set(SCMP_ACT_ERRNO(EUCLEAN), syscall_filter_sets + i, SCMP_ACT_ALLOW, true);
+ else
+ r = seccomp_load_syscall_filter_set(SCMP_ACT_ALLOW, syscall_filter_sets + i, SCMP_ACT_ERRNO(EUCLEAN), true);
+ if (r < 0)
+ _exit(EXIT_FAILURE);
+
+ /* Test the sycall filter with one random system call */
+ fd = eventfd(0, EFD_NONBLOCK|EFD_CLOEXEC);
+ if (IN_SET(i, SYSCALL_FILTER_SET_IO_EVENT, SYSCALL_FILTER_SET_DEFAULT))
+ assert_se(fd < 0 && errno == EUCLEAN);
+ else {
+ assert_se(fd >= 0);
+ safe_close(fd);
+ }
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(wait_for_terminate_and_check(syscall_filter_sets[i].name, pid, WAIT_LOG) == EXIT_SUCCESS);
+ }
+}
+
+TEST(filter_sets_ordered) {
+ /* Ensure "@default" always remains at the beginning of the list */
+ assert_se(SYSCALL_FILTER_SET_DEFAULT == 0);
+ assert_se(streq(syscall_filter_sets[0].name, "@default"));
+
+ /* Ensure "@known" always remains at the end of the list */
+ assert_se(SYSCALL_FILTER_SET_KNOWN == _SYSCALL_FILTER_SET_MAX - 1);
+ assert_se(streq(syscall_filter_sets[SYSCALL_FILTER_SET_KNOWN].name, "@known"));
+
+ for (size_t i = 0; i < _SYSCALL_FILTER_SET_MAX; i++) {
+ const char *p = NULL;
+
+ /* Make sure each group has a description */
+ assert_se(!isempty(syscall_filter_sets[0].help));
+
+ /* Make sure the groups are ordered alphabetically, except for the first and last entries */
+ assert_se(i < 2 || i == _SYSCALL_FILTER_SET_MAX - 1 ||
+ strcmp(syscall_filter_sets[i-1].name, syscall_filter_sets[i].name) < 0);
+
+ NULSTR_FOREACH(k, syscall_filter_sets[i].value) {
+
+ /* Ensure each syscall list is in itself ordered, but groups before names */
+ assert_se(!p ||
+ (*p == '@' && *k != '@') ||
+ (((*p == '@' && *k == '@') ||
+ (*p != '@' && *k != '@')) &&
+ strcmp(p, k) < 0));
+
+ p = k;
+ }
+ }
+}
+
+TEST(restrict_namespace) {
+ char *s = NULL;
+ unsigned long ul;
+ pid_t pid;
+
+ if (!have_namespaces()) {
+ log_notice("Testing without namespaces, skipping %s", __func__);
+ return;
+ }
+
+ assert_se(namespace_flags_to_string(0, &s) == 0 && isempty(s));
+ s = mfree(s);
+ assert_se(namespace_flags_to_string(CLONE_NEWNS, &s) == 0 && streq(s, "mnt"));
+ s = mfree(s);
+ assert_se(namespace_flags_to_string(CLONE_NEWNS|CLONE_NEWIPC, &s) == 0 && streq(s, "ipc mnt"));
+ s = mfree(s);
+ assert_se(namespace_flags_to_string(CLONE_NEWCGROUP, &s) == 0 && streq(s, "cgroup"));
+ s = mfree(s);
+
+ assert_se(namespace_flags_from_string("mnt", &ul) == 0 && ul == CLONE_NEWNS);
+ assert_se(namespace_flags_from_string(NULL, &ul) == 0 && ul == 0);
+ assert_se(namespace_flags_from_string("", &ul) == 0 && ul == 0);
+ assert_se(namespace_flags_from_string("uts", &ul) == 0 && ul == CLONE_NEWUTS);
+ assert_se(namespace_flags_from_string("mnt uts ipc", &ul) == 0 && ul == (CLONE_NEWNS|CLONE_NEWUTS|CLONE_NEWIPC));
+
+ assert_se(namespace_flags_to_string(CLONE_NEWUTS, &s) == 0 && streq(s, "uts"));
+ assert_se(namespace_flags_from_string(s, &ul) == 0 && ul == CLONE_NEWUTS);
+ s = mfree(s);
+ assert_se(namespace_flags_from_string("ipc", &ul) == 0 && ul == CLONE_NEWIPC);
+ assert_se(namespace_flags_to_string(ul, &s) == 0 && streq(s, "ipc"));
+ s = mfree(s);
+
+ assert_se(namespace_flags_to_string(NAMESPACE_FLAGS_ALL, &s) == 0);
+ assert_se(streq(s, "cgroup ipc net mnt pid user uts"));
+ assert_se(namespace_flags_from_string(s, &ul) == 0 && ul == NAMESPACE_FLAGS_ALL);
+ s = mfree(s);
+
+ if (!is_seccomp_available()) {
+ log_notice("Seccomp not available, skipping remaining tests in %s", __func__);
+ return;
+ }
+ if (!have_seccomp_privs()) {
+ log_notice("Not privileged, skipping remaining tests in %s", __func__);
+ return;
+ }
+
+ pid = fork();
+ assert_se(pid >= 0);
+
+ if (pid == 0) {
+
+ assert_se(seccomp_restrict_namespaces(CLONE_NEWNS|CLONE_NEWNET) >= 0);
+
+ assert_se(unshare(CLONE_NEWNS) == 0);
+ assert_se(unshare(CLONE_NEWNET) == 0);
+ assert_se(unshare(CLONE_NEWUTS) == -1);
+ assert_se(errno == EPERM);
+ assert_se(unshare(CLONE_NEWIPC) == -1);
+ assert_se(errno == EPERM);
+ assert_se(unshare(CLONE_NEWNET|CLONE_NEWUTS) == -1);
+ assert_se(errno == EPERM);
+
+ /* We use fd 0 (stdin) here, which of course will fail with EINVAL on setns(). Except of course our
+ * seccomp filter worked, and hits first and makes it return EPERM */
+ assert_se(setns(0, CLONE_NEWNS) == -1);
+ assert_se(errno == EINVAL);
+ assert_se(setns(0, CLONE_NEWNET) == -1);
+ assert_se(errno == EINVAL);
+ assert_se(setns(0, CLONE_NEWUTS) == -1);
+ assert_se(errno == EPERM);
+ assert_se(setns(0, CLONE_NEWIPC) == -1);
+ assert_se(errno == EPERM);
+ assert_se(setns(0, CLONE_NEWNET|CLONE_NEWUTS) == -1);
+ assert_se(errno == EPERM);
+ assert_se(setns(0, 0) == -1);
+ assert_se(errno == EPERM);
+
+ pid = raw_clone(CLONE_NEWNS);
+ assert_se(pid >= 0);
+ if (pid == 0)
+ _exit(EXIT_SUCCESS);
+ pid = raw_clone(CLONE_NEWNET);
+ assert_se(pid >= 0);
+ if (pid == 0)
+ _exit(EXIT_SUCCESS);
+ pid = raw_clone(CLONE_NEWUTS);
+ assert_se(pid < 0);
+ assert_se(errno == EPERM);
+ pid = raw_clone(CLONE_NEWIPC);
+ assert_se(pid < 0);
+ assert_se(errno == EPERM);
+ pid = raw_clone(CLONE_NEWNET|CLONE_NEWUTS);
+ assert_se(pid < 0);
+ assert_se(errno == EPERM);
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(wait_for_terminate_and_check("nsseccomp", pid, WAIT_LOG) == EXIT_SUCCESS);
+}
+
+TEST(protect_sysctl) {
+ pid_t pid;
+ _cleanup_free_ char *seccomp = NULL;
+
+ if (!is_seccomp_available()) {
+ log_notice("Seccomp not available, skipping %s", __func__);
+ return;
+ }
+ if (!have_seccomp_privs()) {
+ log_notice("Not privileged, skipping %s", __func__);
+ return;
+ }
+
+ /* in containers _sysctl() is likely missing anyway */
+ if (detect_container() > 0) {
+ log_notice("Testing in container, skipping %s", __func__);
+ return;
+ }
+
+ assert_se(get_proc_field("/proc/self/status", "Seccomp", WHITESPACE, &seccomp) == 0);
+ if (!streq(seccomp, "0"))
+ log_warning("Warning: seccomp filter detected, results may be unreliable for %s", __func__);
+
+ pid = fork();
+ assert_se(pid >= 0);
+
+ if (pid == 0) {
+#if defined __NR__sysctl && __NR__sysctl >= 0
+ assert_se(syscall(__NR__sysctl, NULL) < 0);
+ assert_se(IN_SET(errno, EFAULT, ENOSYS));
+#endif
+
+ assert_se(seccomp_protect_sysctl() >= 0);
+
+#if HAVE_VALGRIND_VALGRIND_H
+ if (RUNNING_ON_VALGRIND) {
+ log_info("Running on valgrind, skipping syscall/EPERM test");
+ _exit(EXIT_SUCCESS);
+ }
+#endif
+
+#if defined __NR__sysctl && __NR__sysctl >= 0
+ assert_se(syscall(__NR__sysctl, 0, 0, 0) < 0);
+ assert_se(errno == EPERM);
+#endif
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(wait_for_terminate_and_check("sysctlseccomp", pid, WAIT_LOG) == EXIT_SUCCESS);
+}
+
+TEST(protect_syslog) {
+ pid_t pid;
+
+ if (!is_seccomp_available()) {
+ log_notice("Seccomp not available, skipping %s", __func__);
+ return;
+ }
+ if (!have_seccomp_privs()) {
+ log_notice("Not privileged, skipping %s", __func__);
+ return;
+ }
+
+ /* in containers syslog() is likely missing anyway */
+ if (detect_container() > 0) {
+ log_notice("Testing in container, skipping %s", __func__);
+ return;
+ }
+
+ pid = fork();
+ assert_se(pid >= 0);
+
+ if (pid == 0) {
+#if defined __NR_syslog && __NR_syslog >= 0
+ assert_se(syscall(__NR_syslog, -1, NULL, 0) < 0);
+ assert_se(errno == EINVAL);
+#endif
+
+ assert_se(seccomp_protect_syslog() >= 0);
+
+#if defined __NR_syslog && __NR_syslog >= 0
+ assert_se(syscall(__NR_syslog, 0, 0, 0) < 0);
+ assert_se(errno == EPERM);
+#endif
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(wait_for_terminate_and_check("syslogseccomp", pid, WAIT_LOG) == EXIT_SUCCESS);
+}
+
+TEST(restrict_address_families) {
+ pid_t pid;
+
+ if (!is_seccomp_available()) {
+ log_notice("Seccomp not available, skipping %s", __func__);
+ return;
+ }
+ if (!have_seccomp_privs()) {
+ log_notice("Not privileged, skipping %s", __func__);
+ return;
+ }
+
+ pid = fork();
+ assert_se(pid >= 0);
+
+ if (pid == 0) {
+ int fd;
+ Set *s;
+
+ fd = socket(AF_INET, SOCK_DGRAM, 0);
+ assert_se(fd >= 0);
+ safe_close(fd);
+
+ fd = socket(AF_UNIX, SOCK_DGRAM, 0);
+ assert_se(fd >= 0);
+ safe_close(fd);
+
+ fd = socket(AF_NETLINK, SOCK_DGRAM, 0);
+ assert_se(fd >= 0);
+ safe_close(fd);
+
+ assert_se(s = set_new(NULL));
+ assert_se(set_put(s, INT_TO_PTR(AF_UNIX)) >= 0);
+
+ assert_se(seccomp_restrict_address_families(s, false) >= 0);
+
+ fd = socket(AF_INET, SOCK_DGRAM, 0);
+ assert_se(fd >= 0);
+ safe_close(fd);
+
+ fd = socket(AF_UNIX, SOCK_DGRAM, 0);
+#if SECCOMP_RESTRICT_ADDRESS_FAMILIES_BROKEN
+ assert_se(fd >= 0);
+ safe_close(fd);
+#else
+ assert_se(fd < 0);
+ assert_se(errno == EAFNOSUPPORT);
+#endif
+
+ fd = socket(AF_NETLINK, SOCK_DGRAM, 0);
+ assert_se(fd >= 0);
+ safe_close(fd);
+
+ set_clear(s);
+
+ assert_se(set_put(s, INT_TO_PTR(AF_INET)) >= 0);
+
+ assert_se(seccomp_restrict_address_families(s, true) >= 0);
+
+ fd = socket(AF_INET, SOCK_DGRAM, 0);
+ assert_se(fd >= 0);
+ safe_close(fd);
+
+ fd = socket(AF_UNIX, SOCK_DGRAM, 0);
+#if SECCOMP_RESTRICT_ADDRESS_FAMILIES_BROKEN
+ assert_se(fd >= 0);
+ safe_close(fd);
+#else
+ assert_se(fd < 0);
+ assert_se(errno == EAFNOSUPPORT);
+#endif
+
+ fd = socket(AF_NETLINK, SOCK_DGRAM, 0);
+#if SECCOMP_RESTRICT_ADDRESS_FAMILIES_BROKEN
+ assert_se(fd >= 0);
+ safe_close(fd);
+#else
+ assert_se(fd < 0);
+ assert_se(errno == EAFNOSUPPORT);
+#endif
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(wait_for_terminate_and_check("socketseccomp", pid, WAIT_LOG) == EXIT_SUCCESS);
+}
+
+TEST(restrict_realtime) {
+ pid_t pid;
+
+ if (!is_seccomp_available()) {
+ log_notice("Seccomp not available, skipping %s", __func__);
+ return;
+ }
+ if (!have_seccomp_privs()) {
+ log_notice("Not privileged, skipping %s", __func__);
+ return;
+ }
+
+ /* in containers RT privs are likely missing anyway */
+ if (detect_container() > 0) {
+ log_notice("Testing in container, skipping %s", __func__);
+ return;
+ }
+
+ pid = fork();
+ assert_se(pid >= 0);
+
+ if (pid == 0) {
+ /* On some CI environments, the restriction may be already enabled. */
+ if (sched_setscheduler(0, SCHED_FIFO, &(struct sched_param) { .sched_priority = 1 }) < 0) {
+ log_full_errno(errno == EPERM ? LOG_DEBUG : LOG_WARNING, errno,
+ "Failed to set scheduler parameter for FIFO: %m");
+ assert(errno == EPERM);
+ }
+ if (sched_setscheduler(0, SCHED_RR, &(struct sched_param) { .sched_priority = 1 }) < 0) {
+ log_full_errno(errno == EPERM ? LOG_DEBUG : LOG_WARNING, errno,
+ "Failed to set scheduler parameter for RR: %m");
+ assert(errno == EPERM);
+ }
+
+ assert_se(sched_setscheduler(0, SCHED_IDLE, &(struct sched_param) { .sched_priority = 0 }) >= 0);
+ assert_se(sched_setscheduler(0, SCHED_BATCH, &(struct sched_param) { .sched_priority = 0 }) >= 0);
+ assert_se(sched_setscheduler(0, SCHED_OTHER, &(struct sched_param) {}) >= 0);
+
+ assert_se(seccomp_restrict_realtime_full(ENOANO) >= 0);
+
+ assert_se(sched_setscheduler(0, SCHED_IDLE, &(struct sched_param) { .sched_priority = 0 }) >= 0);
+ assert_se(sched_setscheduler(0, SCHED_BATCH, &(struct sched_param) { .sched_priority = 0 }) >= 0);
+ assert_se(sched_setscheduler(0, SCHED_OTHER, &(struct sched_param) {}) >= 0);
+
+ assert_se(sched_setscheduler(0, SCHED_FIFO, &(struct sched_param) { .sched_priority = 1 }) < 0);
+ assert_se(errno == ENOANO);
+ assert_se(sched_setscheduler(0, SCHED_RR, &(struct sched_param) { .sched_priority = 1 }) < 0);
+ assert_se(errno == ENOANO);
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(wait_for_terminate_and_check("realtimeseccomp", pid, WAIT_LOG) == EXIT_SUCCESS);
+}
+
+TEST(memory_deny_write_execute_mmap) {
+ pid_t pid;
+
+ if (!is_seccomp_available()) {
+ log_notice("Seccomp not available, skipping %s", __func__);
+ return;
+ }
+ if (!have_seccomp_privs()) {
+ log_notice("Not privileged, skipping %s", __func__);
+ return;
+ }
+#if HAVE_VALGRIND_VALGRIND_H
+ if (RUNNING_ON_VALGRIND) {
+ log_notice("Running on valgrind, skipping %s", __func__);
+ return;
+ }
+#endif
+#if HAS_FEATURE_ADDRESS_SANITIZER
+ log_notice("Running on address sanitizer, skipping %s", __func__);
+ return;
+#endif
+
+ pid = fork();
+ assert_se(pid >= 0);
+
+ if (pid == 0) {
+ void *p;
+
+ p = mmap(NULL, page_size(), PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1,0);
+ assert_se(p != MAP_FAILED);
+ assert_se(munmap(p, page_size()) >= 0);
+
+ p = mmap(NULL, page_size(), PROT_WRITE|PROT_READ, MAP_PRIVATE|MAP_ANONYMOUS, -1,0);
+ assert_se(p != MAP_FAILED);
+ assert_se(munmap(p, page_size()) >= 0);
+
+ assert_se(seccomp_memory_deny_write_execute() >= 0);
+
+ p = mmap(NULL, page_size(), PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1,0);
+#if defined(__x86_64__) || defined(__i386__) || defined(__powerpc64__) || defined(__arm__) || defined(__aarch64__) || defined(__loongarch_lp64)
+ assert_se(p == MAP_FAILED);
+ assert_se(errno == EPERM);
+#endif
+ /* Depending on kernel, libseccomp, and glibc versions, other architectures
+ * might fail or not. Let's not assert success. */
+ if (p != MAP_FAILED)
+ assert_se(munmap(p, page_size()) == 0);
+
+ p = mmap(NULL, page_size(), PROT_WRITE|PROT_READ, MAP_PRIVATE|MAP_ANONYMOUS, -1,0);
+ assert_se(p != MAP_FAILED);
+ assert_se(munmap(p, page_size()) >= 0);
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(wait_for_terminate_and_check("memoryseccomp-mmap", pid, WAIT_LOG) == EXIT_SUCCESS);
+}
+
+TEST(memory_deny_write_execute_shmat) {
+ int shmid;
+ pid_t pid;
+ uint32_t arch;
+
+ SECCOMP_FOREACH_LOCAL_ARCH(arch) {
+ log_debug("arch %s: SCMP_SYS(mmap) = %d", seccomp_arch_to_string(arch), SCMP_SYS(mmap));
+ log_debug("arch %s: SCMP_SYS(mmap2) = %d", seccomp_arch_to_string(arch), SCMP_SYS(mmap2));
+ log_debug("arch %s: SCMP_SYS(shmget) = %d", seccomp_arch_to_string(arch), SCMP_SYS(shmget));
+ log_debug("arch %s: SCMP_SYS(shmat) = %d", seccomp_arch_to_string(arch), SCMP_SYS(shmat));
+ log_debug("arch %s: SCMP_SYS(shmdt) = %d", seccomp_arch_to_string(arch), SCMP_SYS(shmdt));
+ }
+
+ if (!is_seccomp_available()) {
+ log_notice("Seccomp not available, skipping %s", __func__);
+ return;
+ }
+ if (!have_seccomp_privs() || have_effective_cap(CAP_IPC_OWNER) <= 0) {
+ log_notice("Not privileged, skipping %s", __func__);
+ return;
+ }
+#if HAVE_VALGRIND_VALGRIND_H
+ if (RUNNING_ON_VALGRIND) {
+ log_notice("Running on valgrind, skipping %s", __func__);
+ return;
+ }
+#endif
+#if HAS_FEATURE_ADDRESS_SANITIZER
+ log_notice("Running on address sanitizer, skipping %s", __func__);
+ return;
+#endif
+
+ shmid = shmget(IPC_PRIVATE, page_size(), 0);
+ assert_se(shmid >= 0);
+
+ pid = fork();
+ assert_se(pid >= 0);
+
+ if (pid == 0) {
+ void *p;
+
+ p = shmat(shmid, NULL, 0);
+ assert_se(p != MAP_FAILED);
+ assert_se(shmdt(p) == 0);
+
+ p = shmat(shmid, NULL, SHM_EXEC);
+ assert_se(p != MAP_FAILED);
+ assert_se(shmdt(p) == 0);
+
+ assert_se(seccomp_memory_deny_write_execute() >= 0);
+
+ p = shmat(shmid, NULL, SHM_EXEC);
+ log_debug_errno(p == MAP_FAILED ? errno : 0, "shmat(SHM_EXEC): %m");
+#if defined(__x86_64__) || defined(__arm__) || defined(__aarch64__) || defined(__loongarch_lp64)
+ assert_se(p == MAP_FAILED);
+ assert_se(errno == EPERM);
+#endif
+ /* Depending on kernel, libseccomp, and glibc versions, other architectures
+ * might fail or not. Let's not assert success. */
+ if (p != MAP_FAILED)
+ assert_se(shmdt(p) == 0);
+
+ p = shmat(shmid, NULL, 0);
+ log_debug_errno(p == MAP_FAILED ? errno : 0, "shmat(0): %m");
+ assert_se(p != MAP_FAILED);
+ assert_se(shmdt(p) == 0);
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(wait_for_terminate_and_check("memoryseccomp-shmat", pid, WAIT_LOG) == EXIT_SUCCESS);
+}
+
+TEST(restrict_archs) {
+ pid_t pid;
+
+ if (!is_seccomp_available()) {
+ log_notice("Seccomp not available, skipping %s", __func__);
+ return;
+ }
+ if (!have_seccomp_privs()) {
+ log_notice("Not privileged, skipping %s", __func__);
+ return;
+ }
+
+ pid = fork();
+ assert_se(pid >= 0);
+
+ if (pid == 0) {
+ _cleanup_set_free_ Set *s = NULL;
+
+ assert_se(access("/", F_OK) >= 0);
+
+ assert_se(s = set_new(NULL));
+
+#ifdef __x86_64__
+ assert_se(set_put(s, UINT32_TO_PTR(SCMP_ARCH_X86+1)) >= 0);
+#endif
+ assert_se(seccomp_restrict_archs(s) >= 0);
+
+ assert_se(access("/", F_OK) >= 0);
+ assert_se(seccomp_restrict_archs(NULL) >= 0);
+
+ assert_se(access("/", F_OK) >= 0);
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(wait_for_terminate_and_check("archseccomp", pid, WAIT_LOG) == EXIT_SUCCESS);
+}
+
+TEST(load_syscall_filter_set_raw) {
+ pid_t pid;
+
+ if (!is_seccomp_available()) {
+ log_notice("Seccomp not available, skipping %s", __func__);
+ return;
+ }
+ if (!have_seccomp_privs()) {
+ log_notice("Not privileged, skipping %s", __func__);
+ return;
+ }
+
+ pid = fork();
+ assert_se(pid >= 0);
+
+ if (pid == 0) {
+ _cleanup_hashmap_free_ Hashmap *s = NULL;
+
+ assert_se(access("/", F_OK) >= 0);
+ assert_se(poll(NULL, 0, 0) == 0);
+
+ assert_se(seccomp_load_syscall_filter_set_raw(SCMP_ACT_ALLOW, NULL, scmp_act_kill_process(), true) >= 0);
+ assert_se(access("/", F_OK) >= 0);
+ assert_se(poll(NULL, 0, 0) == 0);
+
+ assert_se(s = hashmap_new(NULL));
+#if defined __NR_access && __NR_access >= 0
+ assert_se(hashmap_put(s, UINT32_TO_PTR(__NR_access + 1), INT_TO_PTR(-1)) >= 0);
+ log_debug("has access()");
+#endif
+#if defined __NR_faccessat && __NR_faccessat >= 0
+ assert_se(hashmap_put(s, UINT32_TO_PTR(__NR_faccessat + 1), INT_TO_PTR(-1)) >= 0);
+ log_debug("has faccessat()");
+#endif
+#if defined __NR_faccessat2 && __NR_faccessat2 >= 0
+ assert_se(hashmap_put(s, UINT32_TO_PTR(__NR_faccessat2 + 1), INT_TO_PTR(-1)) >= 0);
+ log_debug("has faccessat2()");
+#endif
+
+ assert_se(!hashmap_isempty(s));
+ assert_se(seccomp_load_syscall_filter_set_raw(SCMP_ACT_ALLOW, s, SCMP_ACT_ERRNO(EUCLEAN), true) >= 0);
+
+ assert_se(access("/", F_OK) < 0);
+ assert_se(errno == EUCLEAN);
+
+ assert_se(poll(NULL, 0, 0) == 0);
+
+ hashmap_clear(s);
+#if defined __NR_access && __NR_access >= 0
+ assert_se(hashmap_put(s, UINT32_TO_PTR(__NR_access + 1), INT_TO_PTR(EILSEQ)) >= 0);
+#endif
+#if defined __NR_faccessat && __NR_faccessat >= 0
+ assert_se(hashmap_put(s, UINT32_TO_PTR(__NR_faccessat + 1), INT_TO_PTR(EILSEQ)) >= 0);
+#endif
+#if defined __NR_faccessat2 && __NR_faccessat2 >= 0
+ assert_se(hashmap_put(s, UINT32_TO_PTR(__NR_faccessat2 + 1), INT_TO_PTR(EILSEQ)) >= 0);
+#endif
+
+ assert_se(seccomp_load_syscall_filter_set_raw(SCMP_ACT_ALLOW, s, SCMP_ACT_ERRNO(EUCLEAN), true) >= 0);
+
+ assert_se(access("/", F_OK) < 0);
+ assert_se(errno == EILSEQ);
+
+ assert_se(poll(NULL, 0, 0) == 0);
+
+ hashmap_clear(s);
+#if defined __NR_poll && __NR_poll >= 0
+ assert_se(hashmap_put(s, UINT32_TO_PTR(__NR_poll + 1), INT_TO_PTR(-1)) >= 0);
+ log_debug("has poll()");
+#endif
+#if defined __NR_ppoll && __NR_ppoll >= 0
+ assert_se(hashmap_put(s, UINT32_TO_PTR(__NR_ppoll + 1), INT_TO_PTR(-1)) >= 0);
+ log_debug("has ppoll()");
+#endif
+#if defined __NR_ppoll_time64 && __NR_ppoll_time64 >= 0
+ assert_se(hashmap_put(s, UINT32_TO_PTR(__NR_ppoll_time64 + 1), INT_TO_PTR(-1)) >= 0);
+ log_debug("has ppoll_time64()");
+#endif
+
+ assert_se(!hashmap_isempty(s));
+ assert_se(seccomp_load_syscall_filter_set_raw(SCMP_ACT_ALLOW, s, SCMP_ACT_ERRNO(EUNATCH), true) >= 0);
+
+ assert_se(access("/", F_OK) < 0);
+ assert_se(errno == EILSEQ);
+
+ assert_se(poll(NULL, 0, 0) < 0);
+ assert_se(errno == EUNATCH);
+
+ hashmap_clear(s);
+#if defined __NR_poll && __NR_poll >= 0
+ assert_se(hashmap_put(s, UINT32_TO_PTR(__NR_poll + 1), INT_TO_PTR(EILSEQ)) >= 0);
+#endif
+#if defined __NR_ppoll && __NR_ppoll >= 0
+ assert_se(hashmap_put(s, UINT32_TO_PTR(__NR_ppoll + 1), INT_TO_PTR(EILSEQ)) >= 0);
+#endif
+#if defined __NR_ppoll_time64 && __NR_ppoll_time64 >= 0
+ assert_se(hashmap_put(s, UINT32_TO_PTR(__NR_ppoll_time64 + 1), INT_TO_PTR(EILSEQ)) >= 0);
+#endif
+
+ assert_se(seccomp_load_syscall_filter_set_raw(SCMP_ACT_ALLOW, s, SCMP_ACT_ERRNO(EUNATCH), true) >= 0);
+
+ assert_se(access("/", F_OK) < 0);
+ assert_se(errno == EILSEQ);
+
+ assert_se(poll(NULL, 0, 0) < 0);
+ assert_se(errno == EILSEQ);
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(wait_for_terminate_and_check("syscallrawseccomp", pid, WAIT_LOG) == EXIT_SUCCESS);
+}
+
+TEST(native_syscalls_filtered) {
+ pid_t pid;
+
+ if (!is_seccomp_available()) {
+ log_notice("Seccomp not available, skipping %s", __func__);
+ return;
+ }
+ if (!have_seccomp_privs()) {
+ log_notice("Not privileged, skipping %s", __func__);
+ return;
+ }
+
+ pid = fork();
+ assert_se(pid >= 0);
+
+ if (pid == 0) {
+ _cleanup_set_free_ Set *arch_s = NULL;
+ _cleanup_hashmap_free_ Hashmap *s = NULL;
+
+ /* Passing "native" or an empty set is equivalent, just do both here. */
+ assert_se(arch_s = set_new(NULL));
+ assert_se(seccomp_restrict_archs(arch_s) >= 0);
+ assert_se(set_put(arch_s, SCMP_ARCH_NATIVE) >= 0);
+ assert_se(seccomp_restrict_archs(arch_s) >= 0);
+
+ assert_se(access("/", F_OK) >= 0);
+ assert_se(poll(NULL, 0, 0) == 0);
+
+ assert_se(seccomp_load_syscall_filter_set_raw(SCMP_ACT_ALLOW, NULL, scmp_act_kill_process(), true) >= 0);
+ assert_se(access("/", F_OK) >= 0);
+ assert_se(poll(NULL, 0, 0) == 0);
+
+ assert_se(s = hashmap_new(NULL));
+#if defined __NR_access && __NR_access >= 0
+ assert_se(hashmap_put(s, UINT32_TO_PTR(__NR_access + 1), INT_TO_PTR(-1)) >= 0);
+ log_debug("has access()");
+#endif
+#if defined __NR_faccessat && __NR_faccessat >= 0
+ assert_se(hashmap_put(s, UINT32_TO_PTR(__NR_faccessat + 1), INT_TO_PTR(-1)) >= 0);
+ log_debug("has faccessat()");
+#endif
+#if defined __NR_faccessat2 && __NR_faccessat2 >= 0
+ assert_se(hashmap_put(s, UINT32_TO_PTR(__NR_faccessat2 + 1), INT_TO_PTR(-1)) >= 0);
+ log_debug("has faccessat2()");
+#endif
+
+ assert_se(!hashmap_isempty(s));
+ assert_se(seccomp_load_syscall_filter_set_raw(SCMP_ACT_ALLOW, s, SCMP_ACT_ERRNO(EUCLEAN), true) >= 0);
+
+ assert_se(access("/", F_OK) < 0);
+ assert_se(errno == EUCLEAN);
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(wait_for_terminate_and_check("nativeseccomp", pid, WAIT_LOG) == EXIT_SUCCESS);
+}
+
+TEST(lock_personality) {
+ unsigned long current;
+ pid_t pid;
+
+ if (!is_seccomp_available()) {
+ log_notice("Seccomp not available, skipping %s", __func__);
+ return;
+ }
+ if (!have_seccomp_privs()) {
+ log_notice("Not privileged, skipping %s", __func__);
+ return;
+ }
+
+ assert_se(opinionated_personality(&current) >= 0);
+ /* On ppc64le sanitizers disable ASLR (i.e. by setting ADDR_NO_RANDOMIZE),
+ * which opinionated_personality() doesn't return. Let's tweak the current
+ * personality ourselves in such cases.
+ * See: https://github.com/llvm/llvm-project/commit/78f7a6eaa601bfdd6ae70ffd3da2254c21ff77f9
+ */
+ if (FLAGS_SET(safe_personality(PERSONALITY_INVALID), ADDR_NO_RANDOMIZE))
+ current |= ADDR_NO_RANDOMIZE;
+
+ log_info("current personality=0x%lX", current);
+
+ pid = fork();
+ assert_se(pid >= 0);
+
+ if (pid == 0) {
+ assert_se(seccomp_lock_personality(current) >= 0);
+
+ assert_se((unsigned long) safe_personality(current) == current);
+
+ /* Note, we also test that safe_personality() works correctly, by checking whether errno is properly
+ * set, in addition to the return value */
+ errno = 0;
+ assert_se(safe_personality(PER_LINUX | MMAP_PAGE_ZERO) == -EPERM);
+ assert_se(errno == EPERM);
+
+ if (!FLAGS_SET(current, ADDR_NO_RANDOMIZE))
+ assert_se(safe_personality(PER_LINUX | ADDR_NO_RANDOMIZE) == -EPERM);
+ assert_se(safe_personality(PER_LINUX | ADDR_COMPAT_LAYOUT) == -EPERM);
+ assert_se(safe_personality(PER_LINUX | READ_IMPLIES_EXEC) == -EPERM);
+ assert_se(safe_personality(PER_LINUX_32BIT) == -EPERM);
+ assert_se(safe_personality(PER_SVR4) == -EPERM);
+ assert_se(safe_personality(PER_BSD) == -EPERM);
+ assert_se(safe_personality(current == PER_LINUX ? PER_LINUX32 : PER_LINUX) == -EPERM);
+ assert_se(safe_personality(PER_LINUX32_3GB) == -EPERM);
+ assert_se(safe_personality(PER_UW7) == -EPERM);
+ assert_se(safe_personality(0x42) == -EPERM);
+
+ assert_se(safe_personality(PERSONALITY_INVALID) == -EPERM); /* maybe remove this later */
+
+ assert_se((unsigned long) personality(current) == current);
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(wait_for_terminate_and_check("lockpersonalityseccomp", pid, WAIT_LOG) == EXIT_SUCCESS);
+}
+
+static int real_open(const char *path, int flags, mode_t mode) {
+ /* glibc internally calls openat() when open() is requested. Let's hence define our own wrapper for
+ * testing purposes that calls the real syscall, on architectures where SYS_open is defined. On
+ * other architectures, let's just fall back to the glibc call. */
+
+#if defined __NR_open && __NR_open >= 0
+ return (int) syscall(__NR_open, path, flags, mode);
+#else
+ return open(path, flags, mode);
+#endif
+}
+
+static int try_fchmodat2(int dirfd, const char *path, mode_t mode, int flags) {
+ int r;
+
+ /* glibc does not provide a direct wrapper for fchmodat2(). Let's hence define our own wrapper for
+ * testing purposes that calls the real syscall, on architectures and in environments where
+ * SYS_fchmodat2 is defined. Otherwise, let's just fall back to the glibc fchmodat() call. */
+
+ /* Not supported by fchmodat() */
+ assert_se(!FLAGS_SET(flags, AT_EMPTY_PATH));
+
+ r = RET_NERRNO(fchmodat2(dirfd, path, mode, flags));
+ if (r != -ENOSYS)
+ return r;
+
+ /* The syscall might still be unsupported by kernel or libseccomp. */
+ return RET_NERRNO(fchmodat(dirfd, path, mode, flags));
+}
+
+TEST(restrict_suid_sgid) {
+ pid_t pid;
+
+ if (!is_seccomp_available()) {
+ log_notice("Seccomp not available, skipping %s", __func__);
+ return;
+ }
+ if (!have_seccomp_privs()) {
+ log_notice("Not privileged, skipping %s", __func__);
+ return;
+ }
+
+ pid = fork();
+ assert_se(pid >= 0);
+
+ if (pid == 0) {
+ char path[] = "/tmp/suidsgidXXXXXX", dir[] = "/tmp/suidsgiddirXXXXXX";
+ int fd = -EBADF, k = -EBADF;
+ const char *z;
+
+ fd = mkostemp_safe(path);
+ assert_se(fd >= 0);
+
+ assert_se(mkdtemp(dir));
+ z = strjoina(dir, "/test");
+
+ assert_se(chmod(path, 0755 | S_ISUID) >= 0);
+ assert_se(chmod(path, 0755 | S_ISGID) >= 0);
+ assert_se(chmod(path, 0755 | S_ISGID | S_ISUID) >= 0);
+ assert_se(chmod(path, 0755) >= 0);
+
+ assert_se(fchmod(fd, 0755 | S_ISUID) >= 0);
+ assert_se(fchmod(fd, 0755 | S_ISGID) >= 0);
+ assert_se(fchmod(fd, 0755 | S_ISGID | S_ISUID) >= 0);
+ assert_se(fchmod(fd, 0755) >= 0);
+
+ assert_se(fchmodat(AT_FDCWD, path, 0755 | S_ISUID, 0) >= 0);
+ assert_se(fchmodat(AT_FDCWD, path, 0755 | S_ISGID, 0) >= 0);
+ assert_se(fchmodat(AT_FDCWD, path, 0755 | S_ISGID | S_ISUID, 0) >= 0);
+ assert_se(fchmodat(AT_FDCWD, path, 0755, 0) >= 0);
+
+ assert_se(try_fchmodat2(AT_FDCWD, path, 0755 | S_ISUID, 0) >= 0);
+ assert_se(try_fchmodat2(AT_FDCWD, path, 0755 | S_ISGID, 0) >= 0);
+ assert_se(try_fchmodat2(AT_FDCWD, path, 0755 | S_ISGID | S_ISUID, 0) >= 0);
+ assert_se(try_fchmodat2(AT_FDCWD, path, 0755, 0) >= 0);
+
+ k = real_open(z, O_CREAT|O_RDWR|O_CLOEXEC|O_EXCL, 0644 | S_ISUID);
+ k = safe_close(k);
+ assert_se(unlink(z) >= 0);
+
+ k = real_open(z, O_CREAT|O_RDWR|O_CLOEXEC|O_EXCL, 0644 | S_ISGID);
+ k = safe_close(k);
+ assert_se(unlink(z) >= 0);
+
+ k = real_open(z, O_CREAT|O_RDWR|O_CLOEXEC|O_EXCL, 0644 | S_ISUID | S_ISGID);
+ k = safe_close(k);
+ assert_se(unlink(z) >= 0);
+
+ k = real_open(z, O_CREAT|O_RDWR|O_CLOEXEC|O_EXCL, 0644);
+ k = safe_close(k);
+ assert_se(unlink(z) >= 0);
+
+ k = creat(z, 0644 | S_ISUID);
+ k = safe_close(k);
+ assert_se(unlink(z) >= 0);
+
+ k = creat(z, 0644 | S_ISGID);
+ k = safe_close(k);
+ assert_se(unlink(z) >= 0);
+
+ k = creat(z, 0644 | S_ISUID | S_ISGID);
+ k = safe_close(k);
+ assert_se(unlink(z) >= 0);
+
+ k = creat(z, 0644);
+ k = safe_close(k);
+ assert_se(unlink(z) >= 0);
+
+ k = openat(AT_FDCWD, z, O_CREAT|O_RDWR|O_CLOEXEC|O_EXCL, 0644 | S_ISUID);
+ k = safe_close(k);
+ assert_se(unlink(z) >= 0);
+
+ k = openat(AT_FDCWD, z, O_CREAT|O_RDWR|O_CLOEXEC|O_EXCL, 0644 | S_ISGID);
+ k = safe_close(k);
+ assert_se(unlink(z) >= 0);
+
+ k = openat(AT_FDCWD, z, O_CREAT|O_RDWR|O_CLOEXEC|O_EXCL, 0644 | S_ISUID | S_ISGID);
+ k = safe_close(k);
+ assert_se(unlink(z) >= 0);
+
+ k = openat(AT_FDCWD, z, O_CREAT|O_RDWR|O_CLOEXEC|O_EXCL, 0644);
+ k = safe_close(k);
+ assert_se(unlink(z) >= 0);
+
+ assert_se(mkdir(z, 0755 | S_ISUID) >= 0);
+ assert_se(rmdir(z) >= 0);
+ assert_se(mkdir(z, 0755 | S_ISGID) >= 0);
+ assert_se(rmdir(z) >= 0);
+ assert_se(mkdir(z, 0755 | S_ISUID | S_ISGID) >= 0);
+ assert_se(rmdir(z) >= 0);
+ assert_se(mkdir(z, 0755) >= 0);
+ assert_se(rmdir(z) >= 0);
+
+ assert_se(mkdirat(AT_FDCWD, z, 0755 | S_ISUID) >= 0);
+ assert_se(rmdir(z) >= 0);
+ assert_se(mkdirat(AT_FDCWD, z, 0755 | S_ISGID) >= 0);
+ assert_se(rmdir(z) >= 0);
+ assert_se(mkdirat(AT_FDCWD, z, 0755 | S_ISUID | S_ISGID) >= 0);
+ assert_se(rmdir(z) >= 0);
+ assert_se(mkdirat(AT_FDCWD, z, 0755) >= 0);
+ assert_se(rmdir(z) >= 0);
+
+ assert_se(mknod(z, S_IFREG | 0755 | S_ISUID, 0) >= 0);
+ assert_se(unlink(z) >= 0);
+ assert_se(mknod(z, S_IFREG | 0755 | S_ISGID, 0) >= 0);
+ assert_se(unlink(z) >= 0);
+ assert_se(mknod(z, S_IFREG | 0755 | S_ISUID | S_ISGID, 0) >= 0);
+ assert_se(unlink(z) >= 0);
+ assert_se(mknod(z, S_IFREG | 0755, 0) >= 0);
+ assert_se(unlink(z) >= 0);
+
+ assert_se(mknodat(AT_FDCWD, z, S_IFREG | 0755 | S_ISUID, 0) >= 0);
+ assert_se(unlink(z) >= 0);
+ assert_se(mknodat(AT_FDCWD, z, S_IFREG | 0755 | S_ISGID, 0) >= 0);
+ assert_se(unlink(z) >= 0);
+ assert_se(mknodat(AT_FDCWD, z, S_IFREG | 0755 | S_ISUID | S_ISGID, 0) >= 0);
+ assert_se(unlink(z) >= 0);
+ assert_se(mknodat(AT_FDCWD, z, S_IFREG | 0755, 0) >= 0);
+ assert_se(unlink(z) >= 0);
+
+ assert_se(seccomp_restrict_suid_sgid() >= 0);
+
+ assert_se(chmod(path, 0775 | S_ISUID) < 0 && errno == EPERM);
+ assert_se(chmod(path, 0775 | S_ISGID) < 0 && errno == EPERM);
+ assert_se(chmod(path, 0775 | S_ISGID | S_ISUID) < 0 && errno == EPERM);
+ assert_se(chmod(path, 0775) >= 0);
+
+ assert_se(fchmod(fd, 0775 | S_ISUID) < 0 && errno == EPERM);
+ assert_se(fchmod(fd, 0775 | S_ISGID) < 0 && errno == EPERM);
+ assert_se(fchmod(fd, 0775 | S_ISGID | S_ISUID) < 0 && errno == EPERM);
+ assert_se(fchmod(fd, 0775) >= 0);
+
+ assert_se(fchmodat(AT_FDCWD, path, 0755 | S_ISUID, 0) < 0 && errno == EPERM);
+ assert_se(fchmodat(AT_FDCWD, path, 0755 | S_ISGID, 0) < 0 && errno == EPERM);
+ assert_se(fchmodat(AT_FDCWD, path, 0755 | S_ISGID | S_ISUID, 0) < 0 && errno == EPERM);
+ assert_se(fchmodat(AT_FDCWD, path, 0755, 0) >= 0);
+
+ assert_se(try_fchmodat2(AT_FDCWD, path, 0755 | S_ISUID, 0) < 0 && errno == EPERM);
+ assert_se(try_fchmodat2(AT_FDCWD, path, 0755 | S_ISGID, 0) < 0 && errno == EPERM);
+ assert_se(try_fchmodat2(AT_FDCWD, path, 0755 | S_ISGID | S_ISUID, 0) < 0 && errno == EPERM);
+ assert_se(try_fchmodat2(AT_FDCWD, path, 0755, 0) >= 0);
+
+ assert_se(real_open(z, O_CREAT|O_RDWR|O_CLOEXEC|O_EXCL, 0644 | S_ISUID) < 0 && errno == EPERM);
+ assert_se(real_open(z, O_CREAT|O_RDWR|O_CLOEXEC|O_EXCL, 0644 | S_ISGID) < 0 && errno == EPERM);
+ assert_se(real_open(z, O_CREAT|O_RDWR|O_CLOEXEC|O_EXCL, 0644 | S_ISUID | S_ISGID) < 0 && errno == EPERM);
+ k = real_open(z, O_CREAT|O_RDWR|O_CLOEXEC|O_EXCL, 0644);
+ k = safe_close(k);
+ assert_se(unlink(z) >= 0);
+
+ assert_se(creat(z, 0644 | S_ISUID) < 0 && errno == EPERM);
+ assert_se(creat(z, 0644 | S_ISGID) < 0 && errno == EPERM);
+ assert_se(creat(z, 0644 | S_ISUID | S_ISGID) < 0 && errno == EPERM);
+ k = creat(z, 0644);
+ k = safe_close(k);
+ assert_se(unlink(z) >= 0);
+
+ assert_se(openat(AT_FDCWD, z, O_CREAT|O_RDWR|O_CLOEXEC|O_EXCL, 0644 | S_ISUID) < 0 && errno == EPERM);
+ assert_se(openat(AT_FDCWD, z, O_CREAT|O_RDWR|O_CLOEXEC|O_EXCL, 0644 | S_ISGID) < 0 && errno == EPERM);
+ assert_se(openat(AT_FDCWD, z, O_CREAT|O_RDWR|O_CLOEXEC|O_EXCL, 0644 | S_ISUID | S_ISGID) < 0 && errno == EPERM);
+ k = openat(AT_FDCWD, z, O_CREAT|O_RDWR|O_CLOEXEC|O_EXCL, 0644);
+ k = safe_close(k);
+ assert_se(unlink(z) >= 0);
+
+ assert_se(mkdir(z, 0755 | S_ISUID) < 0 && errno == EPERM);
+ assert_se(mkdir(z, 0755 | S_ISGID) < 0 && errno == EPERM);
+ assert_se(mkdir(z, 0755 | S_ISUID | S_ISGID) < 0 && errno == EPERM);
+ assert_se(mkdir(z, 0755) >= 0);
+ assert_se(rmdir(z) >= 0);
+
+ assert_se(mkdirat(AT_FDCWD, z, 0755 | S_ISUID) < 0 && errno == EPERM);
+ assert_se(mkdirat(AT_FDCWD, z, 0755 | S_ISGID) < 0 && errno == EPERM);
+ assert_se(mkdirat(AT_FDCWD, z, 0755 | S_ISUID | S_ISGID) < 0 && errno == EPERM);
+ assert_se(mkdirat(AT_FDCWD, z, 0755) >= 0);
+ assert_se(rmdir(z) >= 0);
+
+ assert_se(mknod(z, S_IFREG | 0755 | S_ISUID, 0) < 0 && errno == EPERM);
+ assert_se(mknod(z, S_IFREG | 0755 | S_ISGID, 0) < 0 && errno == EPERM);
+ assert_se(mknod(z, S_IFREG | 0755 | S_ISUID | S_ISGID, 0) < 0 && errno == EPERM);
+ assert_se(mknod(z, S_IFREG | 0755, 0) >= 0);
+ assert_se(unlink(z) >= 0);
+
+ assert_se(mknodat(AT_FDCWD, z, S_IFREG | 0755 | S_ISUID, 0) < 0 && errno == EPERM);
+ assert_se(mknodat(AT_FDCWD, z, S_IFREG | 0755 | S_ISGID, 0) < 0 && errno == EPERM);
+ assert_se(mknodat(AT_FDCWD, z, S_IFREG | 0755 | S_ISUID | S_ISGID, 0) < 0 && errno == EPERM);
+ assert_se(mknodat(AT_FDCWD, z, S_IFREG | 0755, 0) >= 0);
+ assert_se(unlink(z) >= 0);
+
+ assert_se(unlink(path) >= 0);
+ assert_se(rm_rf(dir, REMOVE_ROOT|REMOVE_PHYSICAL) >= 0);
+
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(wait_for_terminate_and_check("suidsgidseccomp", pid, WAIT_LOG) == EXIT_SUCCESS);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-secure-bits.c b/src/test/test-secure-bits.c
new file mode 100644
index 0000000..27e6a20
--- /dev/null
+++ b/src/test/test-secure-bits.c
@@ -0,0 +1,97 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+
+#include "securebits-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "unit-file.h"
+
+static const char * const string_bits[] = {
+ "keep-caps",
+ "keep-caps-locked",
+ "no-setuid-fixup",
+ "no-setuid-fixup-locked",
+ "noroot",
+ "noroot-locked",
+ NULL
+};
+
+TEST(secure_bits_basic) {
+ _cleanup_free_ char *joined = NULL, *str = NULL;
+ int r;
+
+ /* Check if converting each bit from string and back to string yields
+ * the same value */
+ STRV_FOREACH(bit, string_bits) {
+ _cleanup_free_ char *s = NULL;
+
+ r = secure_bits_from_string(*bit);
+ assert_se(r > 0);
+ assert_se(secure_bits_is_valid(r));
+ assert_se(secure_bits_to_string_alloc(r, &s) >= 0);
+ printf("%s = 0x%x = %s\n", *bit, (unsigned)r, s);
+ assert_se(streq(*bit, s));
+ }
+
+ /* Ditto, but with all bits at once */
+ joined = strv_join((char**)string_bits, " ");
+ assert_se(joined);
+ r = secure_bits_from_string(joined);
+ assert_se(r > 0);
+ assert_se(secure_bits_is_valid(r));
+ assert_se(secure_bits_to_string_alloc(r, &str) >= 0);
+ printf("%s = 0x%x = %s\n", joined, (unsigned)r, str);
+ assert_se(streq(joined, str));
+
+ str = mfree(str);
+
+ /* Empty string */
+ assert_se(secure_bits_from_string("") == 0);
+ assert_se(secure_bits_from_string(" ") == 0);
+
+ /* Only invalid entries */
+ assert_se(secure_bits_from_string("foo bar baz") == 0);
+
+ /* Empty secure bits */
+ assert_se(secure_bits_to_string_alloc(0, &str) >= 0);
+ assert_se(isempty(str));
+
+ str = mfree(str);
+
+ /* Bits to string with check */
+ assert_se(secure_bits_to_string_alloc_with_check(INT_MAX, &str) == -EINVAL);
+ assert_se(str == NULL);
+ assert_se(secure_bits_to_string_alloc_with_check(
+ (1 << SECURE_KEEP_CAPS) | (1 << SECURE_KEEP_CAPS_LOCKED),
+ &str) >= 0);
+ assert_se(streq(str, "keep-caps keep-caps-locked"));
+}
+
+TEST(secure_bits_mix) {
+ static struct sbit_table {
+ const char *input;
+ const char *expected;
+ } sbit_table[] = {
+ { "keep-caps keep-caps keep-caps", "keep-caps" },
+ { "keep-caps noroot keep-caps", "keep-caps noroot" },
+ { "noroot foo bar baz noroot", "noroot" },
+ { "noroot \"foo\" \"bar keep-caps", "noroot" },
+ { "\"noroot foo\" bar keep-caps", "keep-caps" },
+ {}
+ };
+
+ for (const struct sbit_table *s = sbit_table; s->input; s++) {
+ _cleanup_free_ char *str = NULL;
+ int r;
+
+ r = secure_bits_from_string(s->input);
+ assert_se(r > 0);
+ assert_se(secure_bits_is_valid(r));
+ assert_se(secure_bits_to_string_alloc(r, &str) >= 0);
+ printf("%s = 0x%x = %s\n", s->input, (unsigned)r, str);
+ assert_se(streq(s->expected, str));
+ }
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-selinux.c b/src/test/test-selinux.c
new file mode 100644
index 0000000..04b5ba1
--- /dev/null
+++ b/src/test/test-selinux.c
@@ -0,0 +1,104 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/stat.h>
+
+#include "alloc-util.h"
+#include "fd-util.h"
+#include "log.h"
+#include "selinux-util.h"
+#include "string-util.h"
+#include "tests.h"
+#include "time-util.h"
+
+static void test_testing(void) {
+ bool b;
+
+ log_info("============ %s ==========", __func__);
+
+ b = mac_selinux_use();
+ log_info("mac_selinux_use → %s", yes_no(b));
+
+ b = mac_selinux_use();
+ log_info("mac_selinux_use → %s", yes_no(b));
+
+ mac_selinux_retest();
+
+ b = mac_selinux_use();
+ log_info("mac_selinux_use → %s", yes_no(b));
+
+ b = mac_selinux_use();
+ log_info("mac_selinux_use → %s", yes_no(b));
+}
+
+static void test_loading(void) {
+ usec_t n1, n2;
+ int r;
+
+ log_info("============ %s ==========", __func__);
+
+ n1 = now(CLOCK_MONOTONIC);
+ r = mac_selinux_init();
+ n2 = now(CLOCK_MONOTONIC);
+ log_info_errno(r, "mac_selinux_init → %d %.2fs (%m)", r, (n2 - n1)/1e6);
+}
+
+static void test_cleanup(void) {
+ usec_t n1, n2;
+
+ log_info("============ %s ==========", __func__);
+
+ n1 = now(CLOCK_MONOTONIC);
+ mac_selinux_finish();
+ n2 = now(CLOCK_MONOTONIC);
+ log_info("mac_selinux_finish → %.2fs", (n2 - n1)/1e6);
+}
+
+static void test_misc(const char* fname) {
+ _cleanup_(mac_selinux_freep) char *label = NULL, *label2 = NULL, *label3 = NULL;
+ int r;
+ _cleanup_close_ int fd = -EBADF;
+
+ log_info("============ %s ==========", __func__);
+
+ r = mac_selinux_get_our_label(&label);
+ log_info_errno(r, "mac_selinux_get_our_label → %d, \"%s\" (%m)",
+ r, strnull(label));
+
+ r = mac_selinux_get_create_label_from_exe(fname, &label2);
+ log_info_errno(r, "mac_selinux_create_label_from_exe → %d, \"%s\" (%m)",
+ r, strnull(label2));
+
+ fd = socket(AF_INET, SOCK_DGRAM, 0);
+ assert_se(fd >= 0);
+
+ r = mac_selinux_get_child_mls_label(fd, fname, label2, &label3);
+ log_info_errno(r, "mac_selinux_get_child_mls_label → %d, \"%s\" (%m)",
+ r, strnull(label3));
+}
+
+static void test_create_file_prepare(const char* fname) {
+ int r;
+
+ log_info("============ %s ==========", __func__);
+
+ r = mac_selinux_create_file_prepare(fname, S_IRWXU);
+ log_info_errno(r, "mac_selinux_create_file_prepare → %d (%m)", r);
+
+ mac_selinux_create_file_clear();
+}
+
+int main(int argc, char **argv) {
+ const char *path = SYSTEMD_BINARY_PATH;
+ if (argc >= 2)
+ path = argv[1];
+
+ test_setup_logging(LOG_DEBUG);
+
+ test_testing();
+ test_loading();
+ test_misc(path);
+ test_create_file_prepare(path);
+ test_cleanup();
+
+ return 0;
+}
diff --git a/src/test/test-serialize.c b/src/test/test-serialize.c
new file mode 100644
index 0000000..8f74472
--- /dev/null
+++ b/src/test/test-serialize.c
@@ -0,0 +1,265 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "escape.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "log.h"
+#include "serialize.h"
+#include "strv.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+static char long_string[LONG_LINE_MAX+1];
+
+TEST(serialize_item) {
+ _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-serialize.XXXXXX";
+ _cleanup_fclose_ FILE *f = NULL;
+
+ assert_se(fmkostemp_safe(fn, "r+", &f) == 0);
+ log_info("/* %s (%s) */", __func__, fn);
+
+ assert_se(serialize_item(f, "a", NULL) == 0);
+ assert_se(serialize_item(f, "a", "bbb") == 1);
+ assert_se(serialize_item(f, "a", "bbb") == 1);
+ assert_se(serialize_bool_elide(f, "c", true) == 1);
+ assert_se(serialize_bool_elide(f, "d", false) == 0);
+ assert_se(serialize_item(f, "a", long_string) == -EINVAL);
+ assert_se(serialize_item(f, long_string, "a") == -EINVAL);
+ assert_se(serialize_item(f, long_string, long_string) == -EINVAL);
+
+ rewind(f);
+
+ _cleanup_free_ char *line1 = NULL, *line2 = NULL, *line3 = NULL, *line4 = NULL;
+ assert_se(read_line(f, LONG_LINE_MAX, &line1) > 0);
+ assert_se(streq(line1, "a=bbb"));
+ assert_se(read_line(f, LONG_LINE_MAX, &line2) > 0);
+ assert_se(streq(line2, "a=bbb"));
+ assert_se(read_line(f, LONG_LINE_MAX, &line3) > 0);
+ assert_se(streq(line3, "c=yes"));
+ assert_se(read_line(f, LONG_LINE_MAX, &line4) == 0);
+ assert_se(streq(line4, ""));
+}
+
+TEST(serialize_item_escaped) {
+ _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-serialize.XXXXXX";
+ _cleanup_fclose_ FILE *f = NULL;
+
+ assert_se(fmkostemp_safe(fn, "r+", &f) == 0);
+ log_info("/* %s (%s) */", __func__, fn);
+
+ assert_se(serialize_item_escaped(f, "a", NULL) == 0);
+ assert_se(serialize_item_escaped(f, "a", "bbb") == 1);
+ assert_se(serialize_item_escaped(f, "a", "bbb") == 1);
+ assert_se(serialize_item_escaped(f, "a", long_string) == -EINVAL);
+ assert_se(serialize_item_escaped(f, long_string, "a") == -EINVAL);
+ assert_se(serialize_item_escaped(f, long_string, long_string) == -EINVAL);
+
+ rewind(f);
+
+ _cleanup_free_ char *line1 = NULL, *line2 = NULL, *line3 = NULL;
+ assert_se(read_line(f, LONG_LINE_MAX, &line1) > 0);
+ assert_se(streq(line1, "a=bbb"));
+ assert_se(read_line(f, LONG_LINE_MAX, &line2) > 0);
+ assert_se(streq(line2, "a=bbb"));
+ assert_se(read_line(f, LONG_LINE_MAX, &line3) == 0);
+ assert_se(streq(line3, ""));
+}
+
+TEST(serialize_usec) {
+ _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-serialize.XXXXXX";
+ _cleanup_fclose_ FILE *f = NULL;
+
+ assert_se(fmkostemp_safe(fn, "r+", &f) == 0);
+ log_info("/* %s (%s) */", __func__, fn);
+
+ assert_se(serialize_usec(f, "usec1", USEC_INFINITY) == 0);
+ assert_se(serialize_usec(f, "usec2", 0) == 1);
+ assert_se(serialize_usec(f, "usec3", USEC_INFINITY-1) == 1);
+
+ rewind(f);
+
+ _cleanup_free_ char *line1 = NULL, *line2 = NULL;
+ usec_t x;
+
+ assert_se(read_line(f, LONG_LINE_MAX, &line1) > 0);
+ assert_se(streq(line1, "usec2=0"));
+ assert_se(deserialize_usec(line1 + 6, &x) == 0);
+ assert_se(x == 0);
+
+ assert_se(read_line(f, LONG_LINE_MAX, &line2) > 0);
+ assert_se(startswith(line2, "usec3="));
+ assert_se(deserialize_usec(line2 + 6, &x) == 0);
+ assert_se(x == USEC_INFINITY-1);
+}
+
+TEST(serialize_strv) {
+ _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-serialize.XXXXXX";
+ _cleanup_fclose_ FILE *f = NULL;
+
+ char **strv = STRV_MAKE("a", "b", "foo foo",
+ "nasty1 \"",
+ "\"nasty2 ",
+ "nasty3 '",
+ "\"nasty4 \"",
+ "nasty5\n",
+ "\nnasty5\nfoo=bar",
+ "\nnasty5\nfoo=bar");
+
+ assert_se(fmkostemp_safe(fn, "r+", &f) == 0);
+ log_info("/* %s (%s) */", __func__, fn);
+
+ assert_se(serialize_strv(f, "strv1", NULL) == 0);
+ assert_se(serialize_strv(f, "strv2", STRV_MAKE_EMPTY) == 0);
+ assert_se(serialize_strv(f, "strv3", strv) == 1);
+ assert_se(serialize_strv(f, "strv4", STRV_MAKE(long_string)) == -EINVAL);
+
+ rewind(f);
+
+ _cleanup_strv_free_ char **strv2 = NULL;
+ for (;;) {
+ _cleanup_free_ char *line = NULL;
+ int r;
+
+ r = read_line(f, LONG_LINE_MAX, &line);
+ if (r == 0)
+ break;
+ assert_se(r > 0);
+
+ const char *t = startswith(line, "strv3=");
+ assert_se(t);
+ assert_se(deserialize_strv(t, &strv2) >= 0);
+ }
+
+ assert_se(strv_equal(strv, strv2));
+}
+
+TEST(deserialize_environment) {
+ _cleanup_strv_free_ char **env;
+
+ assert_se(env = strv_new("A=1"));
+
+ assert_se(deserialize_environment("B=2", &env) >= 0);
+ assert_se(deserialize_environment("FOO%%=a\\177b\\nc\\td e", &env) >= 0);
+
+ assert_se(strv_equal(env, STRV_MAKE("A=1", "B=2", "FOO%%=a\177b\nc\td e")));
+
+ assert_se(deserialize_environment("foo\\", &env) < 0);
+ assert_se(deserialize_environment("bar\\_baz", &env) < 0);
+}
+
+TEST(serialize_environment) {
+ _cleanup_strv_free_ char **env = NULL, **env2 = NULL;
+ _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-env-util.XXXXXXX";
+ _cleanup_fclose_ FILE *f = NULL;
+ int r;
+
+ assert_se(fmkostemp_safe(fn, "r+", &f) == 0);
+ log_info("/* %s (%s) */", __func__, fn);
+
+ assert_se(env = strv_new("A=1",
+ "B=2",
+ "C=ąęółń",
+ "D=D=a\\x0Ab",
+ "FOO%%=a\177b\nc\td e"));
+
+ assert_se(serialize_strv(f, "env", env) == 1);
+ assert_se(fflush_and_check(f) == 0);
+
+ rewind(f);
+
+ for (;;) {
+ _cleanup_free_ char *line = NULL;
+ const char *l;
+
+ r = read_line(f, LONG_LINE_MAX, &line);
+ assert_se(r >= 0);
+
+ if (r == 0)
+ break;
+
+ l = strstrip(line);
+
+ assert_se(startswith(l, "env="));
+
+ r = deserialize_environment(l+4, &env2);
+ assert_se(r >= 0);
+ }
+ assert_se(feof(f));
+
+ assert_se(strv_equal(env, env2));
+}
+
+TEST(serialize_item_hexmem) {
+ _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-serialize.XXXXXX";
+ _cleanup_fclose_ FILE *f = NULL;
+
+ assert_se(fmkostemp_safe(fn, "r+", &f) == 0);
+ log_info("/* %s (%s) */", __func__, fn);
+
+ assert_se(serialize_item_hexmem(f, "a", NULL, 0) == 0);
+ assert_se(serialize_item_hexmem(f, "a", (uint8_t []){0xff, 0xff, 0xff}, sizeof(uint8_t) * 3) == 1);
+
+ rewind(f);
+
+ _cleanup_free_ char *line = NULL;
+ assert_se(read_line(f, LONG_LINE_MAX, &line) > 0);
+ assert_se(streq(line, "a=ffffff"));
+
+}
+
+TEST(serialize_item_base64mem) {
+ _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-serialize.XXXXXX";
+ _cleanup_fclose_ FILE *f = NULL;
+
+ assert_se(fmkostemp_safe(fn, "r+", &f) == 0);
+ log_info("/* %s (%s) */", __func__, fn);
+
+ assert_se(serialize_item_base64mem(f, "a", NULL, 0) == 0);
+ assert_se(serialize_item_base64mem(f, "a", (uint8_t []){0xff, 0xff, 0xff}, sizeof(uint8_t) * 3) == 1);
+
+ rewind(f);
+
+ _cleanup_free_ char *line = NULL;
+ assert_se(read_line(f, LONG_LINE_MAX, &line) > 0);
+ assert_se(streq(line, "a=////"));
+}
+
+TEST(serialize_string_set) {
+ _cleanup_(unlink_tempfilep) char fn[] = "/tmp/test-serialize.XXXXXX";
+ _cleanup_fclose_ FILE *f = NULL;
+ _cleanup_set_free_free_ Set *s = NULL;
+ _cleanup_free_ char *line1 = NULL, *line2 = NULL;
+ char *p, *q;
+
+ assert_se(fmkostemp_safe(fn, "r+", &f) == 0);
+ log_info("/* %s (%s) */", __func__, fn);
+
+ assert_se(set_ensure_allocated(&s, &string_hash_ops) >= 0);
+
+ assert_se(serialize_string_set(f, "a", s) == 0);
+
+ assert_se(set_put_strsplit(s, "abc def,ghi jkl", ",", 0) >= 0);
+
+ assert_se(serialize_string_set(f, "a", s) == 1);
+
+ rewind(f);
+
+ assert_se(read_line(f, LONG_LINE_MAX, &line1) > 0);
+ assert_se((p = startswith(line1, "a=")));
+
+ assert_se(read_line(f, LONG_LINE_MAX, &line2) > 0);
+ assert_se((q = startswith(line2, "a=")));
+
+ assert_se(!streq(p, q));
+ assert_se(STR_IN_SET(p, "abc def", "ghi jkl"));
+ assert_se(STR_IN_SET(q, "abc def", "ghi jkl"));
+}
+
+static int intro(void) {
+ memset(long_string, 'x', sizeof(long_string)-1);
+ char_array_0(long_string);
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_INFO, intro);
diff --git a/src/test/test-set-disable-mempool.c b/src/test/test-set-disable-mempool.c
new file mode 100644
index 0000000..91244b2
--- /dev/null
+++ b/src/test/test-set-disable-mempool.c
@@ -0,0 +1,58 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <pthread.h>
+
+#include "mempool.h"
+#include "process-util.h"
+#include "set.h"
+#include "tests.h"
+
+#define NUM 100
+
+static void* thread(void *p) {
+ Set **s = p;
+
+ assert_se(s);
+ assert_se(*s);
+
+ assert_se(!is_main_thread());
+ assert_se(mempool_enabled);
+ assert_se(!mempool_enabled());
+
+ assert_se(set_size(*s) == NUM);
+ *s = set_free(*s);
+
+ return NULL;
+}
+
+static void test_one(const char *val) {
+ pthread_t t;
+ int x[NUM] = {};
+ unsigned i;
+ Set *s;
+
+ log_info("Testing with SYSTEMD_MEMPOOL=%s", val);
+ assert_se(setenv("SYSTEMD_MEMPOOL", val, true) == 0);
+
+ assert_se(is_main_thread());
+ assert_se(mempool_enabled); /* It is a weak symbol, but we expect it to be available */
+ assert_se(!mempool_enabled());
+
+ assert_se(s = set_new(NULL));
+ for (i = 0; i < NUM; i++)
+ assert_se(set_put(s, &x[i]));
+
+ assert_se(pthread_create(&t, NULL, thread, &s) == 0);
+ assert_se(pthread_join(t, NULL) == 0);
+
+ assert_se(!s);
+}
+
+TEST(disable_mempool) {
+ test_one("0");
+ /* The value $SYSTEMD_MEMPOOL= is cached. So the following
+ * test should also succeed. */
+ test_one("1");
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-set.c b/src/test/test-set.c
new file mode 100644
index 0000000..0d5a6a1
--- /dev/null
+++ b/src/test/test-set.c
@@ -0,0 +1,403 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "random-util.h"
+#include "set.h"
+#include "strv.h"
+#include "tests.h"
+
+TEST(set_steal_first) {
+ _cleanup_set_free_ Set *m = NULL;
+ int seen[3] = {};
+ char *val;
+
+ m = set_new(&string_hash_ops);
+ assert_se(m);
+
+ assert_se(set_put(m, (void*) "1") == 1);
+ assert_se(set_put(m, (void*) "22") == 1);
+ assert_se(set_put(m, (void*) "333") == 1);
+
+ while ((val = set_steal_first(m)))
+ seen[strlen(val) - 1]++;
+
+ assert_se(seen[0] == 1 && seen[1] == 1 && seen[2] == 1);
+
+ assert_se(set_isempty(m));
+}
+
+typedef struct Item {
+ int seen;
+} Item;
+static void item_seen(Item *item) {
+ item->seen++;
+}
+
+TEST(set_free_with_destructor) {
+ Set *m;
+ struct Item items[4] = {};
+
+ assert_se(m = set_new(NULL));
+ for (size_t i = 0; i < ELEMENTSOF(items) - 1; i++)
+ assert_se(set_put(m, items + i) == 1);
+
+ m = set_free_with_destructor(m, item_seen);
+ assert_se(items[0].seen == 1);
+ assert_se(items[1].seen == 1);
+ assert_se(items[2].seen == 1);
+ assert_se(items[3].seen == 0);
+}
+
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(item_hash_ops, void, trivial_hash_func, trivial_compare_func, Item, item_seen);
+
+TEST(set_free_with_hash_ops) {
+ Set *m;
+ struct Item items[4] = {};
+
+ assert_se(m = set_new(&item_hash_ops));
+ for (size_t i = 0; i < ELEMENTSOF(items) - 1; i++)
+ assert_se(set_put(m, items + i) == 1);
+
+ m = set_free(m);
+ assert_se(items[0].seen == 1);
+ assert_se(items[1].seen == 1);
+ assert_se(items[2].seen == 1);
+ assert_se(items[3].seen == 0);
+}
+
+TEST(set_put) {
+ _cleanup_set_free_ Set *m = NULL;
+
+ m = set_new(&string_hash_ops);
+ assert_se(m);
+
+ assert_se(set_put(m, (void*) "1") == 1);
+ assert_se(set_put(m, (void*) "22") == 1);
+ assert_se(set_put(m, (void*) "333") == 1);
+ assert_se(set_put(m, (void*) "333") == 0);
+ assert_se(set_remove(m, (void*) "333"));
+ assert_se(set_put(m, (void*) "333") == 1);
+ assert_se(set_put(m, (void*) "333") == 0);
+ assert_se(set_put(m, (void*) "22") == 0);
+
+ _cleanup_free_ char **t = set_get_strv(m);
+ assert_se(strv_contains(t, "1"));
+ assert_se(strv_contains(t, "22"));
+ assert_se(strv_contains(t, "333"));
+ assert_se(strv_length(t) == 3);
+}
+
+TEST(set_put_strndup) {
+ _cleanup_set_free_ Set *m = NULL;
+
+ assert_se(set_put_strndup(&m, "12345", 0) == 1);
+ assert_se(set_put_strndup(&m, "12345", 1) == 1);
+ assert_se(set_put_strndup(&m, "12345", 2) == 1);
+ assert_se(set_put_strndup(&m, "12345", 3) == 1);
+ assert_se(set_put_strndup(&m, "12345", 4) == 1);
+ assert_se(set_put_strndup(&m, "12345", 5) == 1);
+ assert_se(set_put_strndup(&m, "12345", 6) == 0);
+
+ assert_se(set_contains(m, ""));
+ assert_se(set_contains(m, "1"));
+ assert_se(set_contains(m, "12"));
+ assert_se(set_contains(m, "123"));
+ assert_se(set_contains(m, "1234"));
+ assert_se(set_contains(m, "12345"));
+
+ assert_se(set_size(m) == 6);
+}
+
+TEST(set_put_strdup) {
+ _cleanup_set_free_ Set *m = NULL;
+
+ assert_se(set_put_strdup(&m, "aaa") == 1);
+ assert_se(set_put_strdup(&m, "aaa") == 0);
+ assert_se(set_put_strdup(&m, "bbb") == 1);
+ assert_se(set_put_strdup(&m, "bbb") == 0);
+ assert_se(set_put_strdup(&m, "aaa") == 0);
+
+ assert_se(set_contains(m, "aaa"));
+ assert_se(set_contains(m, "bbb"));
+
+ assert_se(set_size(m) == 2);
+}
+
+TEST(set_put_strdupv) {
+ _cleanup_set_free_ Set *m = NULL;
+
+ assert_se(set_put_strdupv(&m, STRV_MAKE("aaa", "aaa", "bbb", "bbb", "aaa")) == 2);
+ assert_se(set_put_strdupv(&m, STRV_MAKE("aaa", "aaa", "bbb", "bbb", "ccc")) == 1);
+
+ assert_se(set_contains(m, "aaa"));
+ assert_se(set_contains(m, "bbb"));
+ assert_se(set_contains(m, "ccc"));
+
+ assert_se(set_size(m) == 3);
+}
+
+TEST(set_ensure_allocated) {
+ _cleanup_set_free_ Set *m = NULL;
+
+ assert_se(set_ensure_allocated(&m, &string_hash_ops) == 1);
+ assert_se(set_ensure_allocated(&m, &string_hash_ops) == 0);
+ assert_se(set_ensure_allocated(&m, NULL) == 0);
+ assert_se(set_size(m) == 0);
+}
+
+TEST(set_copy) {
+ _cleanup_set_free_ Set *s = NULL;
+ _cleanup_set_free_free_ Set *copy = NULL;
+ char *key1, *key2, *key3, *key4;
+
+ key1 = strdup("key1");
+ assert_se(key1);
+ key2 = strdup("key2");
+ assert_se(key2);
+ key3 = strdup("key3");
+ assert_se(key3);
+ key4 = strdup("key4");
+ assert_se(key4);
+
+ s = set_new(&string_hash_ops);
+ assert_se(s);
+
+ assert_se(set_put(s, key1) >= 0);
+ assert_se(set_put(s, key2) >= 0);
+ assert_se(set_put(s, key3) >= 0);
+ assert_se(set_put(s, key4) >= 0);
+
+ copy = set_copy(s);
+ assert_se(copy);
+
+ assert_se(set_equal(s, copy));
+}
+
+TEST(set_ensure_put) {
+ _cleanup_set_free_ Set *m = NULL;
+
+ assert_se(set_ensure_put(&m, &string_hash_ops, "a") == 1);
+ assert_se(set_ensure_put(&m, &string_hash_ops, "a") == 0);
+ assert_se(set_ensure_put(&m, NULL, "a") == 0);
+ assert_se(set_ensure_put(&m, &string_hash_ops, "b") == 1);
+ assert_se(set_ensure_put(&m, &string_hash_ops, "b") == 0);
+ assert_se(set_ensure_put(&m, &string_hash_ops, "a") == 0);
+ assert_se(set_size(m) == 2);
+}
+
+TEST(set_ensure_consume) {
+ _cleanup_set_free_ Set *m = NULL;
+ char *s, *t;
+
+ assert_se(s = strdup("a"));
+ assert_se(set_ensure_consume(&m, &string_hash_ops_free, s) == 1);
+
+ assert_se(t = strdup("a"));
+ assert_se(set_ensure_consume(&m, &string_hash_ops_free, t) == 0);
+
+ assert_se(t = strdup("a"));
+ assert_se(set_ensure_consume(&m, &string_hash_ops_free, t) == 0);
+
+ assert_se(t = strdup("b"));
+ assert_se(set_ensure_consume(&m, &string_hash_ops_free, t) == 1);
+
+ assert_se(t = strdup("b"));
+ assert_se(set_ensure_consume(&m, &string_hash_ops_free, t) == 0);
+
+ assert_se(set_size(m) == 2);
+}
+
+TEST(set_strjoin) {
+ _cleanup_set_free_ Set *m = NULL;
+ _cleanup_free_ char *joined = NULL;
+
+ /* Empty set */
+ assert_se(set_strjoin(m, NULL, false, &joined) >= 0);
+ assert_se(!joined);
+ assert_se(set_strjoin(m, "", false, &joined) >= 0);
+ assert_se(!joined);
+ assert_se(set_strjoin(m, " ", false, &joined) >= 0);
+ assert_se(!joined);
+ assert_se(set_strjoin(m, "xxx", false, &joined) >= 0);
+ assert_se(!joined);
+ assert_se(set_strjoin(m, NULL, true, &joined) >= 0);
+ assert_se(!joined);
+ assert_se(set_strjoin(m, "", true, &joined) >= 0);
+ assert_se(!joined);
+ assert_se(set_strjoin(m, " ", true, &joined) >= 0);
+ assert_se(!joined);
+ assert_se(set_strjoin(m, "xxx", true, &joined) >= 0);
+ assert_se(!joined);
+
+ /* Single entry */
+ assert_se(set_put_strdup(&m, "aaa") == 1);
+ assert_se(set_strjoin(m, NULL, false, &joined) >= 0);
+ assert_se(streq(joined, "aaa"));
+ joined = mfree(joined);
+ assert_se(set_strjoin(m, "", false, &joined) >= 0);
+ assert_se(streq(joined, "aaa"));
+ joined = mfree(joined);
+ assert_se(set_strjoin(m, " ", false, &joined) >= 0);
+ assert_se(streq(joined, "aaa"));
+ joined = mfree(joined);
+ assert_se(set_strjoin(m, "xxx", false, &joined) >= 0);
+ assert_se(streq(joined, "aaa"));
+ joined = mfree(joined);
+ assert_se(set_strjoin(m, NULL, true, &joined) >= 0);
+ assert_se(streq(joined, "aaa"));
+ joined = mfree(joined);
+ assert_se(set_strjoin(m, "", true, &joined) >= 0);
+ assert_se(streq(joined, "aaa"));
+ joined = mfree(joined);
+ assert_se(set_strjoin(m, " ", true, &joined) >= 0);
+ assert_se(streq(joined, " aaa "));
+ joined = mfree(joined);
+ assert_se(set_strjoin(m, "xxx", true, &joined) >= 0);
+ assert_se(streq(joined, "xxxaaaxxx"));
+
+ /* Two entries */
+ assert_se(set_put_strdup(&m, "bbb") == 1);
+ assert_se(set_put_strdup(&m, "aaa") == 0);
+ joined = mfree(joined);
+ assert_se(set_strjoin(m, NULL, false, &joined) >= 0);
+ assert_se(STR_IN_SET(joined, "aaabbb", "bbbaaa"));
+ joined = mfree(joined);
+ assert_se(set_strjoin(m, "", false, &joined) >= 0);
+ assert_se(STR_IN_SET(joined, "aaabbb", "bbbaaa"));
+ joined = mfree(joined);
+ assert_se(set_strjoin(m, " ", false, &joined) >= 0);
+ assert_se(STR_IN_SET(joined, "aaa bbb", "bbb aaa"));
+ joined = mfree(joined);
+ assert_se(set_strjoin(m, "xxx", false, &joined) >= 0);
+ assert_se(STR_IN_SET(joined, "aaaxxxbbb", "bbbxxxaaa"));
+ joined = mfree(joined);
+ assert_se(set_strjoin(m, NULL, true, &joined) >= 0);
+ assert_se(STR_IN_SET(joined, "aaabbb", "bbbaaa"));
+ joined = mfree(joined);
+ assert_se(set_strjoin(m, "", true, &joined) >= 0);
+ assert_se(STR_IN_SET(joined, "aaabbb", "bbbaaa"));
+ joined = mfree(joined);
+ assert_se(set_strjoin(m, " ", true, &joined) >= 0);
+ assert_se(STR_IN_SET(joined, " aaa bbb ", " bbb aaa "));
+ joined = mfree(joined);
+ assert_se(set_strjoin(m, "xxx", true, &joined) >= 0);
+ assert_se(STR_IN_SET(joined, "xxxaaaxxxbbbxxx", "xxxbbbxxxaaaxxx"));
+}
+
+TEST(set_equal) {
+ _cleanup_set_free_ Set *a = NULL, *b = NULL;
+ void *p;
+ int r;
+
+ assert_se(a = set_new(NULL));
+ assert_se(b = set_new(NULL));
+
+ assert_se(set_equal(a, a));
+ assert_se(set_equal(b, b));
+ assert_se(set_equal(a, b));
+ assert_se(set_equal(b, a));
+ assert_se(set_equal(NULL, a));
+ assert_se(set_equal(NULL, b));
+ assert_se(set_equal(a, NULL));
+ assert_se(set_equal(b, NULL));
+ assert_se(set_equal(NULL, NULL));
+
+ for (unsigned i = 0; i < 333; i++) {
+ p = INT32_TO_PTR(1 + (random_u32() & 0xFFFU));
+
+ r = set_put(a, p);
+ assert_se(r >= 0 || r == -EEXIST);
+ }
+
+ assert_se(set_put(a, INT32_TO_PTR(0x1000U)) >= 0);
+
+ assert_se(set_size(a) >= 2);
+ assert_se(set_size(a) <= 334);
+
+ assert_se(!set_equal(a, b));
+ assert_se(!set_equal(b, a));
+ assert_se(!set_equal(a, NULL));
+
+ SET_FOREACH(p, a)
+ assert_se(set_put(b, p) >= 0);
+
+ assert_se(set_equal(a, b));
+ assert_se(set_equal(b, a));
+
+ assert_se(set_remove(a, INT32_TO_PTR(0x1000U)) == INT32_TO_PTR(0x1000U));
+
+ assert_se(!set_equal(a, b));
+ assert_se(!set_equal(b, a));
+
+ assert_se(set_remove(b, INT32_TO_PTR(0x1000U)) == INT32_TO_PTR(0x1000U));
+
+ assert_se(set_equal(a, b));
+ assert_se(set_equal(b, a));
+
+ assert_se(set_put(b, INT32_TO_PTR(0x1001U)) >= 0);
+
+ assert_se(!set_equal(a, b));
+ assert_se(!set_equal(b, a));
+
+ assert_se(set_put(a, INT32_TO_PTR(0x1001U)) >= 0);
+
+ assert_se(set_equal(a, b));
+ assert_se(set_equal(b, a));
+
+ set_clear(a);
+
+ assert_se(!set_equal(a, b));
+ assert_se(!set_equal(b, a));
+
+ set_clear(b);
+
+ assert_se(set_equal(a, b));
+ assert_se(set_equal(b, a));
+}
+
+TEST(set_fnmatch) {
+ _cleanup_set_free_ Set *match = NULL, *nomatch = NULL;
+
+ assert_se(set_put_strdup(&match, "aaa") >= 0);
+ assert_se(set_put_strdup(&match, "bbb*") >= 0);
+ assert_se(set_put_strdup(&match, "*ccc") >= 0);
+
+ assert_se(set_put_strdup(&nomatch, "a*") >= 0);
+ assert_se(set_put_strdup(&nomatch, "bbb") >= 0);
+ assert_se(set_put_strdup(&nomatch, "ccc*") >= 0);
+
+ assert_se(set_fnmatch(NULL, NULL, ""));
+ assert_se(set_fnmatch(NULL, NULL, "hoge"));
+
+ assert_se(set_fnmatch(match, NULL, "aaa"));
+ assert_se(set_fnmatch(match, NULL, "bbb"));
+ assert_se(set_fnmatch(match, NULL, "bbbXXX"));
+ assert_se(set_fnmatch(match, NULL, "ccc"));
+ assert_se(set_fnmatch(match, NULL, "XXXccc"));
+ assert_se(!set_fnmatch(match, NULL, ""));
+ assert_se(!set_fnmatch(match, NULL, "aaaa"));
+ assert_se(!set_fnmatch(match, NULL, "XXbbb"));
+ assert_se(!set_fnmatch(match, NULL, "cccXX"));
+
+ assert_se(set_fnmatch(NULL, nomatch, ""));
+ assert_se(set_fnmatch(NULL, nomatch, "Xa"));
+ assert_se(set_fnmatch(NULL, nomatch, "bbbb"));
+ assert_se(set_fnmatch(NULL, nomatch, "XXXccc"));
+ assert_se(!set_fnmatch(NULL, nomatch, "a"));
+ assert_se(!set_fnmatch(NULL, nomatch, "aXXXX"));
+ assert_se(!set_fnmatch(NULL, nomatch, "bbb"));
+ assert_se(!set_fnmatch(NULL, nomatch, "ccc"));
+ assert_se(!set_fnmatch(NULL, nomatch, "cccXXX"));
+
+ assert_se(set_fnmatch(match, nomatch, "bbbbb"));
+ assert_se(set_fnmatch(match, nomatch, "XXccc"));
+ assert_se(!set_fnmatch(match, nomatch, ""));
+ assert_se(!set_fnmatch(match, nomatch, "a"));
+ assert_se(!set_fnmatch(match, nomatch, "aaa"));
+ assert_se(!set_fnmatch(match, nomatch, "b"));
+ assert_se(!set_fnmatch(match, nomatch, "bbb"));
+ assert_se(!set_fnmatch(match, nomatch, "ccc"));
+ assert_se(!set_fnmatch(match, nomatch, "ccccc"));
+ assert_se(!set_fnmatch(match, nomatch, "cccXX"));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-sha256.c b/src/test/test-sha256.c
new file mode 100644
index 0000000..f168e4c
--- /dev/null
+++ b/src/test/test-sha256.c
@@ -0,0 +1,50 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "hexdecoct.h"
+#include "sha256.h"
+#include "string-util.h"
+#include "tests.h"
+
+static void sha256_process_string(const char *key, struct sha256_ctx *ctx) {
+ sha256_process_bytes(key, strlen(key), ctx);
+}
+
+static void test_sha256_one(const char *key, const char *expect) {
+ uint8_t result[SHA256_DIGEST_SIZE + 3];
+ _cleanup_free_ char *str = NULL;
+ struct sha256_ctx ctx;
+
+ log_debug("\"%s\" → %s", key, expect);
+
+ assert_se(str = new(char, strlen(key) + 4));
+
+ /* This tests unaligned buffers. */
+
+ for (size_t i = 0; i < 4; i++) {
+ strcpy(str + i, key);
+
+ for (size_t j = 0; j < 4; j++) {
+ _cleanup_free_ char *hex_result = NULL;
+
+ sha256_init_ctx(&ctx);
+ sha256_process_string(str + i, &ctx);
+ sha256_finish_ctx(&ctx, result + j);
+
+ hex_result = hexmem(result + j, SHA256_DIGEST_SIZE);
+ assert_se(streq_ptr(hex_result, expect));
+ }
+ }
+}
+
+TEST(sha256) {
+ /* Results compared with output of 'echo -n "<input>" | sha256sum -' */
+
+ test_sha256_one("abcdefghijklmnopqrstuvwxyz",
+ "71c480df93d6ae2f1efad1447c66c9525e316218cf51fc8d9ed832f2daf18b73");
+ test_sha256_one("ほげほげあっちょんぶりけ",
+ "ce7225683653be3b74861c5a4323b6baf3c3ceb361413ca99e3a5b52c04411bd");
+ test_sha256_one("0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789",
+ "9cfe7faff7054298ca87557e15a10262de8d3eee77827417fbdfea1c41b9ec23");
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-sigbus.c b/src/test/test-sigbus.c
new file mode 100644
index 0000000..299463c
--- /dev/null
+++ b/src/test/test-sigbus.c
@@ -0,0 +1,62 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#if HAVE_VALGRIND_VALGRIND_H
+# include <valgrind/valgrind.h>
+#endif
+
+#include "fd-util.h"
+#include "fs-util.h"
+#include "memory-util.h"
+#include "sigbus.h"
+#include "tests.h"
+
+int main(int argc, char *argv[]) {
+ _cleanup_close_ int fd = -EBADF;
+ char template[] = "/tmp/sigbus-test-XXXXXX";
+ void *addr = NULL;
+ uint8_t *p;
+
+ test_setup_logging(LOG_INFO);
+
+#if HAS_FEATURE_ADDRESS_SANITIZER
+ return log_tests_skipped("address-sanitizer is enabled");
+#endif
+#if HAVE_VALGRIND_VALGRIND_H
+ if (RUNNING_ON_VALGRIND)
+ return log_tests_skipped("This test cannot run on valgrind");
+#endif
+
+ sigbus_install();
+
+ assert_se(sigbus_pop(&addr) == 0);
+
+ assert_se((fd = mkostemp(template, O_RDWR|O_CREAT|O_EXCL)) >= 0);
+ assert_se(unlink(template) >= 0);
+ assert_se(posix_fallocate_loop(fd, 0, page_size() * 8) >= 0);
+
+ p = mmap(NULL, page_size() * 16, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
+ assert_se(p != MAP_FAILED);
+
+ assert_se(sigbus_pop(&addr) == 0);
+
+ p[0] = 0xFF;
+ assert_se(sigbus_pop(&addr) == 0);
+
+ p[page_size()] = 0xFF;
+ assert_se(sigbus_pop(&addr) == 0);
+
+ p[page_size()*8] = 0xFF;
+ p[page_size()*8+1] = 0xFF;
+ p[page_size()*10] = 0xFF;
+ assert_se(sigbus_pop(&addr) > 0);
+ assert_se(addr == p + page_size() * 8);
+ assert_se(sigbus_pop(&addr) > 0);
+ assert_se(addr == p + page_size() * 10);
+ assert_se(sigbus_pop(&addr) == 0);
+
+ sigbus_reset();
+}
diff --git a/src/test/test-signal-util.c b/src/test/test-signal-util.c
new file mode 100644
index 0000000..335066a
--- /dev/null
+++ b/src/test/test-signal-util.c
@@ -0,0 +1,175 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <unistd.h>
+
+#include "log.h"
+#include "macro.h"
+#include "signal-util.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "tests.h"
+#include "process-util.h"
+
+#define info(sig) log_info(#sig " = " STRINGIFY(sig) " = %d", sig)
+
+TEST(rt_signals) {
+ info(SIGRTMIN);
+ info(SIGRTMAX);
+
+ /* We use signals SIGRTMIN+0 to SIGRTMIN+24 unconditionally */
+ assert_se(SIGRTMAX - SIGRTMIN >= 24);
+}
+
+static void test_signal_to_string_one(int val) {
+ const char *p;
+
+ assert_se(p = signal_to_string(val));
+
+ assert_se(signal_from_string(p) == val);
+
+ p = strjoina("SIG", p);
+ assert_se(signal_from_string(p) == val);
+}
+
+static void test_signal_from_string_one(const char *s, int val) {
+ const char *p;
+
+ assert_se(signal_from_string(s) == val);
+
+ p = strjoina("SIG", s);
+ assert_se(signal_from_string(p) == val);
+}
+
+static void test_signal_from_string_number(const char *s, int val) {
+ const char *p;
+
+ assert_se(signal_from_string(s) == val);
+
+ p = strjoina("SIG", s);
+ assert_se(signal_from_string(p) == -EINVAL);
+}
+
+TEST(signal_from_string) {
+ char buf[STRLEN("RTMIN+") + DECIMAL_STR_MAX(int) + 1];
+
+ test_signal_to_string_one(SIGHUP);
+ test_signal_to_string_one(SIGTERM);
+ test_signal_to_string_one(SIGRTMIN);
+ test_signal_to_string_one(SIGRTMIN+3);
+ test_signal_to_string_one(SIGRTMAX-4);
+
+ test_signal_from_string_one("RTMIN", SIGRTMIN);
+ test_signal_from_string_one("RTMAX", SIGRTMAX);
+
+ xsprintf(buf, "RTMIN+%d", SIGRTMAX-SIGRTMIN);
+ test_signal_from_string_one(buf, SIGRTMAX);
+
+ xsprintf(buf, "RTMIN+%d", INT_MAX);
+ test_signal_from_string_one(buf, -ERANGE);
+
+ xsprintf(buf, "RTMAX-%d", SIGRTMAX-SIGRTMIN);
+ test_signal_from_string_one(buf, SIGRTMIN);
+
+ xsprintf(buf, "RTMAX-%d", INT_MAX);
+ test_signal_from_string_one(buf, -ERANGE);
+
+ test_signal_from_string_one("", -EINVAL);
+ test_signal_from_string_one("hup", -EINVAL);
+ test_signal_from_string_one("HOGEHOGE", -EINVAL);
+
+ test_signal_from_string_one("RTMIN-5", -EINVAL);
+ test_signal_from_string_one("RTMIN- 5", -EINVAL);
+ test_signal_from_string_one("RTMIN -5", -EINVAL);
+ test_signal_from_string_one("RTMIN+ 5", -EINVAL);
+ test_signal_from_string_one("RTMIN +5", -EINVAL);
+ test_signal_from_string_one("RTMIN+100", -ERANGE);
+ test_signal_from_string_one("RTMIN+-3", -EINVAL);
+ test_signal_from_string_one("RTMIN++3", -EINVAL);
+ test_signal_from_string_one("RTMIN+HUP", -EINVAL);
+ test_signal_from_string_one("RTMIN3", -EINVAL);
+
+ test_signal_from_string_one("RTMAX+5", -EINVAL);
+ test_signal_from_string_one("RTMAX+ 5", -EINVAL);
+ test_signal_from_string_one("RTMAX +5", -EINVAL);
+ test_signal_from_string_one("RTMAX- 5", -EINVAL);
+ test_signal_from_string_one("RTMAX -5", -EINVAL);
+ test_signal_from_string_one("RTMAX-100", -ERANGE);
+ test_signal_from_string_one("RTMAX-+3", -EINVAL);
+ test_signal_from_string_one("RTMAX--3", -EINVAL);
+ test_signal_from_string_one("RTMAX-HUP", -EINVAL);
+
+ test_signal_from_string_number("3", 3);
+ test_signal_from_string_number("+5", 5);
+ test_signal_from_string_number(" +5", 5);
+ test_signal_from_string_number("10000", -ERANGE);
+ test_signal_from_string_number("-2", -ERANGE);
+}
+
+TEST(block_signals) {
+ assert_se(signal_is_blocked(SIGUSR1) == 0);
+ assert_se(signal_is_blocked(SIGALRM) == 0);
+ assert_se(signal_is_blocked(SIGVTALRM) == 0);
+
+ {
+ BLOCK_SIGNALS(SIGUSR1, SIGVTALRM);
+
+ assert_se(signal_is_blocked(SIGUSR1) > 0);
+ assert_se(signal_is_blocked(SIGALRM) == 0);
+ assert_se(signal_is_blocked(SIGVTALRM) > 0);
+ }
+
+ assert_se(signal_is_blocked(SIGUSR1) == 0);
+ assert_se(signal_is_blocked(SIGALRM) == 0);
+ assert_se(signal_is_blocked(SIGVTALRM) == 0);
+}
+
+TEST(ignore_signals) {
+ assert_se(ignore_signals(SIGINT) >= 0);
+ assert_se(kill(getpid_cached(), SIGINT) >= 0);
+ assert_se(ignore_signals(SIGUSR1, SIGUSR2, SIGTERM, SIGPIPE) >= 0);
+ assert_se(kill(getpid_cached(), SIGUSR1) >= 0);
+ assert_se(kill(getpid_cached(), SIGUSR2) >= 0);
+ assert_se(kill(getpid_cached(), SIGTERM) >= 0);
+ assert_se(kill(getpid_cached(), SIGPIPE) >= 0);
+ assert_se(default_signals(SIGINT, SIGUSR1, SIGUSR2, SIGTERM, SIGPIPE) >= 0);
+}
+
+TEST(pop_pending_signal) {
+
+ assert_se(signal_is_blocked(SIGUSR1) == 0);
+ assert_se(signal_is_blocked(SIGUSR2) == 0);
+ assert_se(pop_pending_signal(SIGUSR1) == 0);
+ assert_se(pop_pending_signal(SIGUSR2) == 0);
+
+ {
+ BLOCK_SIGNALS(SIGUSR1, SIGUSR2);
+
+ assert_se(signal_is_blocked(SIGUSR1) > 0);
+ assert_se(signal_is_blocked(SIGUSR2) > 0);
+
+ assert_se(pop_pending_signal(SIGUSR1) == 0);
+ assert_se(pop_pending_signal(SIGUSR2) == 0);
+
+ assert_se(raise(SIGUSR1) >= 0);
+
+ assert_se(pop_pending_signal(SIGUSR2) == 0);
+ assert_se(pop_pending_signal(SIGUSR1) == SIGUSR1);
+ assert_se(pop_pending_signal(SIGUSR1) == 0);
+
+ assert_se(raise(SIGUSR1) >= 0);
+ assert_se(raise(SIGUSR2) >= 0);
+
+ assert_cc(SIGUSR1 < SIGUSR2);
+
+ assert_se(pop_pending_signal(SIGUSR1, SIGUSR2) == SIGUSR1);
+ assert_se(pop_pending_signal(SIGUSR1, SIGUSR2) == SIGUSR2);
+ assert_se(pop_pending_signal(SIGUSR1, SIGUSR2) == 0);
+ }
+
+ assert_se(signal_is_blocked(SIGUSR1) == 0);
+ assert_se(signal_is_blocked(SIGUSR2) == 0);
+ assert_se(pop_pending_signal(SIGUSR1) == 0);
+ assert_se(pop_pending_signal(SIGUSR2) == 0);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-siphash24.c b/src/test/test-siphash24.c
new file mode 100644
index 0000000..de91eb2
--- /dev/null
+++ b/src/test/test-siphash24.c
@@ -0,0 +1,108 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "memory-util.h"
+#include "siphash24.h"
+#include "tests.h"
+
+#define ITERATIONS 10000000ULL
+
+static void test_alignment_one(const uint8_t *in, size_t len, const uint8_t *key) {
+ struct siphash state = {};
+ uint64_t out;
+ unsigned i, j;
+
+ out = siphash24(in, len, key);
+ assert_se(out == 0xa129ca6149be45e5);
+
+ /* verify the internal state as given in the above paper */
+ siphash24_init(&state, key);
+ assert_se(state.v0 == 0x7469686173716475);
+ assert_se(state.v1 == 0x6b617f6d656e6665);
+ assert_se(state.v2 == 0x6b7f62616d677361);
+ assert_se(state.v3 == 0x7b6b696e727e6c7b);
+ siphash24_compress(in, len, &state);
+ assert_se(state.v0 == 0x4a017198de0a59e0);
+ assert_se(state.v1 == 0x0d52f6f62a4f59a4);
+ assert_se(state.v2 == 0x634cb3577b01fd3d);
+ assert_se(state.v3 == 0xa5224d6f55c7d9c8);
+ out = siphash24_finalize(&state);
+ assert_se(out == 0xa129ca6149be45e5);
+ assert_se(state.v0 == 0xf6bcd53893fecff1);
+ assert_se(state.v1 == 0x54b9964c7ea0d937);
+ assert_se(state.v2 == 0x1b38329c099bb55a);
+ assert_se(state.v3 == 0x1814bb89ad7be679);
+
+ /* verify that decomposing the input in three chunks gives the
+ same result */
+ for (i = 0; i < len; i++) {
+ for (j = i; j < len; j++) {
+ siphash24_init(&state, key);
+ siphash24_compress(in, i, &state);
+ siphash24_compress(&in[i], j - i, &state);
+ siphash24_compress(&in[j], len - j, &state);
+ out = siphash24_finalize(&state);
+ assert_se(out == 0xa129ca6149be45e5);
+ }
+ }
+}
+
+TEST(alignment) {
+ const uint8_t in[15] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e };
+ const uint8_t key[16] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f};
+ uint8_t in_buf[20];
+
+ /* Test with same input but different alignments. */
+ memcpy(in_buf, in, sizeof(in));
+ test_alignment_one(in_buf, sizeof(in), key);
+ memcpy(in_buf + 1, in, sizeof(in));
+ test_alignment_one(in_buf + 1, sizeof(in), key);
+ memcpy(in_buf + 2, in, sizeof(in));
+ test_alignment_one(in_buf + 2, sizeof(in), key);
+ memcpy(in_buf + 4, in, sizeof(in));
+ test_alignment_one(in_buf + 4, sizeof(in), key);
+}
+
+TEST(short_hashes) {
+ const uint8_t one[] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+ 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16 };
+ const uint8_t key[16] = { 0x22, 0x24, 0x41, 0x22, 0x55, 0x77, 0x88, 0x07,
+ 0x23, 0x09, 0x23, 0x14, 0x0c, 0x33, 0x0e, 0x0f};
+ uint8_t two[sizeof one] = {};
+
+ struct siphash state1 = {}, state2 = {};
+ unsigned i, j;
+
+ siphash24_init(&state1, key);
+ siphash24_init(&state2, key);
+
+ /* hashing 1, 2, 3, 4, 5, ..., 16 bytes, with the byte after the buffer different */
+ for (i = 1; i <= sizeof one; i++) {
+ siphash24_compress(one, i, &state1);
+
+ two[i-1] = one[i-1];
+ siphash24_compress(two, i, &state2);
+
+ assert_se(memcmp(&state1, &state2, sizeof state1) == 0);
+ }
+
+ /* hashing n and 1, n and 2, n and 3, ..., n-1 and 1, n-2 and 2, ... */
+ for (i = sizeof one; i > 0; i--) {
+ zero(two);
+
+ for (j = 1; j <= sizeof one; j++) {
+ siphash24_compress(one, i, &state1);
+ siphash24_compress(one, j, &state1);
+
+ siphash24_compress(one, i, &state2);
+ two[j-1] = one[j-1];
+ siphash24_compress(two, j, &state2);
+
+ assert_se(memcmp(&state1, &state2, sizeof state1) == 0);
+ }
+ }
+}
+
+/* see https://131002.net/siphash/siphash.pdf, Appendix A */
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-sizeof.c b/src/test/test-sizeof.c
new file mode 100644
index 0000000..ea0c587
--- /dev/null
+++ b/src/test/test-sizeof.c
@@ -0,0 +1,130 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sched.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <sys/socket.h>
+#include <sys/timex.h>
+#include <sys/types.h>
+
+#define __STDC_WANT_IEC_60559_TYPES_EXT__
+#include <float.h>
+
+#include "time-util.h"
+
+/* Print information about various types. Useful when diagnosing
+ * gcc diagnostics on an unfamiliar architecture. */
+
+DISABLE_WARNING_TYPE_LIMITS;
+
+#define info_no_sign(t) \
+ printf("%s → %zu bits, %zu byte alignment\n", STRINGIFY(t), \
+ sizeof(t)*CHAR_BIT, \
+ alignof(t))
+
+#define info(t) \
+ printf("%s → %zu bits%s, %zu byte alignment\n", STRINGIFY(t), \
+ sizeof(t)*CHAR_BIT, \
+ strstr(STRINGIFY(t), "signed") ? "" : \
+ (t)-1 < (t)0 ? ", signed" : ", unsigned", \
+ alignof(t))
+
+#define check_no_sign(t, size) \
+ do { \
+ info_no_sign(t); \
+ assert_se(sizeof(t) == size); \
+ } while (false)
+
+#define check(t, size) \
+ do { \
+ info(t); \
+ assert_se(sizeof(t) == size); \
+ } while (false)
+
+enum Enum {
+ enum_value,
+};
+
+enum BigEnum {
+ big_enum_value = UINT64_C(1),
+};
+
+enum BigEnum2 {
+ big_enum2_pos = UINT64_C(1),
+ big_enum2_neg = UINT64_C(-1),
+};
+
+int main(void) {
+ int (*function_pointer)(void);
+
+ check_no_sign(dev_t, SIZEOF_DEV_T);
+ check_no_sign(ino_t, SIZEOF_INO_T);
+ check_no_sign(rlim_t, SIZEOF_RLIM_T);
+ check(time_t, SIZEOF_TIME_T);
+ check(typeof(((struct timex *)0)->freq), SIZEOF_TIMEX_MEMBER);
+
+ info_no_sign(typeof(function_pointer));
+ info_no_sign(void*);
+ info(char*);
+
+ info(char);
+ info(signed char);
+ info(unsigned char);
+ info(short unsigned);
+ info(unsigned);
+ info(unsigned long);
+ info(unsigned long long);
+#ifdef __GLIBC__
+ info(__syscall_ulong_t);
+ info(__syscall_slong_t);
+#endif
+ info(intmax_t);
+ info(uintmax_t);
+
+ info(float);
+ info(double);
+ info(long double);
+
+#ifdef FLT128_MAX
+ info(_Float128);
+ info(_Float64);
+ info(_Float64x);
+ info(_Float32);
+ info(_Float32x);
+#endif
+
+ info(size_t);
+ info(ssize_t);
+ info(usec_t);
+#ifdef __GLIBC__
+ info(__time_t);
+#endif
+ info(pid_t);
+ info(uid_t);
+ info(gid_t);
+ info(socklen_t);
+
+#ifdef __GLIBC__
+ info(__cpu_mask);
+#endif
+
+ info(enum Enum);
+ info(enum BigEnum);
+ info(enum BigEnum2);
+ assert_cc(sizeof(enum BigEnum2) == 8);
+ printf("big_enum2_pos → %zu\n", sizeof(big_enum2_pos));
+ printf("big_enum2_neg → %zu\n", sizeof(big_enum2_neg));
+
+ printf("timeval: %zu\n", sizeof(struct timeval));
+ printf("timespec: %zu\n", sizeof(struct timespec));
+
+ void *x = malloc(100);
+
+ printf("local variable: %p\n", &function_pointer);
+ printf("glibc function: %p\n", memcpy);
+ printf("heap allocation: %p\n", x);
+ free(x);
+
+ return 0;
+}
diff --git a/src/test/test-sleep-config.c b/src/test/test-sleep-config.c
new file mode 100644
index 0000000..112fec6
--- /dev/null
+++ b/src/test/test-sleep-config.c
@@ -0,0 +1,79 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <unistd.h>
+
+#include "efivars.h"
+#include "errno-util.h"
+#include "log.h"
+#include "sleep-config.h"
+#include "strv.h"
+#include "tests.h"
+
+TEST(parse_sleep_config) {
+ _cleanup_(sleep_config_freep) SleepConfig *sleep_config = NULL;
+
+ assert_se(parse_sleep_config(&sleep_config) == 0);
+
+ _cleanup_free_ char *sum = NULL, *sus = NULL, *him = NULL, *his = NULL, *hym = NULL, *hys = NULL;
+
+ sum = strv_join(sleep_config->modes[SLEEP_SUSPEND], ", ");
+ sus = strv_join(sleep_config->states[SLEEP_SUSPEND], ", ");
+ him = strv_join(sleep_config->modes[SLEEP_HIBERNATE], ", ");
+ his = strv_join(sleep_config->states[SLEEP_HIBERNATE], ", ");
+ hym = strv_join(sleep_config->modes[SLEEP_HYBRID_SLEEP], ", ");
+ hys = strv_join(sleep_config->states[SLEEP_HYBRID_SLEEP], ", ");
+ log_debug(" allow_suspend: %s", yes_no(sleep_config->allow[SLEEP_SUSPEND]));
+ log_debug(" allow_hibernate: %s", yes_no(sleep_config->allow[SLEEP_HIBERNATE]));
+ log_debug(" allow_s2h: %s", yes_no(sleep_config->allow[SLEEP_SUSPEND_THEN_HIBERNATE]));
+ log_debug(" allow_hybrid_sleep: %s", yes_no(sleep_config->allow[SLEEP_HYBRID_SLEEP]));
+ log_debug(" suspend modes: %s", sum);
+ log_debug(" states: %s", sus);
+ log_debug(" hibernate modes: %s", him);
+ log_debug(" states: %s", his);
+ log_debug(" hybrid modes: %s", hym);
+ log_debug(" states: %s", hys);
+}
+
+TEST(sleep_supported) {
+ _cleanup_strv_free_ char
+ **standby = strv_new("standby"),
+ **mem = strv_new("mem"),
+ **disk = strv_new("disk"),
+ **suspend = strv_new("suspend"),
+ **reboot = strv_new("reboot"),
+ **platform = strv_new("platform"),
+ **shutdown = strv_new("shutdown"),
+ **freeze = strv_new("freeze");
+ int r;
+
+ printf("Secure boot: %sd\n", enable_disable(is_efi_secure_boot()));
+
+ log_info("/= individual sleep modes =/");
+ log_info("Standby configured: %s", yes_no(sleep_state_supported(standby) > 0));
+ log_info("Suspend configured: %s", yes_no(sleep_state_supported(mem) > 0));
+ log_info("Hibernate configured: %s", yes_no(sleep_state_supported(disk) > 0));
+ log_info("Hibernate+Suspend (Hybrid-Sleep) configured: %s", yes_no(sleep_mode_supported(suspend) > 0));
+ log_info("Hibernate+Reboot configured: %s", yes_no(sleep_mode_supported(reboot) > 0));
+ log_info("Hibernate+Platform configured: %s", yes_no(sleep_mode_supported(platform) > 0));
+ log_info("Hibernate+Shutdown configured: %s", yes_no(sleep_mode_supported(shutdown) > 0));
+ log_info("Freeze configured: %s", yes_no(sleep_state_supported(freeze) > 0));
+
+ log_info("/= high-level sleep verbs =/");
+ r = sleep_supported(SLEEP_SUSPEND);
+ log_info("Suspend configured and possible: %s", r >= 0 ? yes_no(r) : STRERROR(r));
+ r = sleep_supported(SLEEP_HIBERNATE);
+ log_info("Hibernation configured and possible: %s", r >= 0 ? yes_no(r) : STRERROR(r));
+ r = sleep_supported(SLEEP_HYBRID_SLEEP);
+ log_info("Hybrid-sleep configured and possible: %s", r >= 0 ? yes_no(r) : STRERROR(r));
+ r = sleep_supported(SLEEP_SUSPEND_THEN_HIBERNATE);
+ log_info("Suspend-then-Hibernate configured and possible: %s", r >= 0 ? yes_no(r) : STRERROR(r));
+}
+
+static int intro(void) {
+ if (getuid() != 0)
+ log_warning("This program is unlikely to work for unprivileged users");
+
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_DEBUG, intro);
diff --git a/src/test/test-socket-bind.c b/src/test/test-socket-bind.c
new file mode 100644
index 0000000..84a8978
--- /dev/null
+++ b/src/test/test-socket-bind.c
@@ -0,0 +1,149 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "bpf-socket-bind.h"
+#include "load-fragment.h"
+#include "manager.h"
+#include "process-util.h"
+#include "rlimit-util.h"
+#include "rm-rf.h"
+#include "service.h"
+#include "strv.h"
+#include "tests.h"
+#include "unit.h"
+#include "virt.h"
+
+static int find_netcat_executable(char **ret_path) {
+ char **candidates = STRV_MAKE("ncat", "nc", "netcat");
+ int r = 0;
+
+ STRV_FOREACH(c, candidates) {
+ r = find_executable(*c, ret_path);
+ if (r == 0)
+ break;
+ }
+
+ return r;
+}
+
+static int test_socket_bind(
+ Manager *m,
+ const char *unit_name,
+ const char *netcat_path,
+ const char *port,
+ char **allow_rules,
+ char **deny_rules) {
+ _cleanup_free_ char *exec_start = NULL;
+ _cleanup_(unit_freep) Unit *u = NULL;
+ CGroupContext *cc = NULL;
+ int cld_code, r;
+
+ assert_se(u = unit_new(m, sizeof(Service)));
+ assert_se(unit_add_name(u, unit_name) == 0);
+ assert_se(cc = unit_get_cgroup_context(u));
+
+ STRV_FOREACH(rule, allow_rules) {
+ r = config_parse_cgroup_socket_bind(
+ u->id, "filename", 1, "Service", 1, "SocketBindAllow", 0,
+ *rule, &cc->socket_bind_allow, u);
+ if (r < 0)
+ return log_unit_error_errno(u, r, "Failed to parse SocketBindAllow: %m");
+ }
+
+ fprintf(stderr, "SocketBindAllow: ");
+ cgroup_context_dump_socket_bind_items(cc->socket_bind_allow, stderr);
+ fputc('\n', stderr);
+
+ STRV_FOREACH(rule, deny_rules) {
+ r = config_parse_cgroup_socket_bind(
+ u->id, "filename", 1, "Service", 1, "SocketBindDeny", 0,
+ *rule, &cc->socket_bind_deny, u);
+ if (r < 0)
+ return log_unit_error_errno(u, r, "Failed to parse SocketBindDeny: %m");
+ }
+
+ fprintf(stderr, "SocketBindDeny: ");
+ cgroup_context_dump_socket_bind_items(cc->socket_bind_deny, stderr);
+ fputc('\n', stderr);
+
+ exec_start = strjoin("-timeout --preserve-status -sSIGTERM 1s ", netcat_path, " -l ", port, " -vv");
+ assert_se(exec_start != NULL);
+
+ r = config_parse_exec(u->id, "filename", 1, "Service", 1, "ExecStart",
+ SERVICE_EXEC_START, exec_start, SERVICE(u)->exec_command, u);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse ExecStart");
+
+ SERVICE(u)->type = SERVICE_ONESHOT;
+ u->load_state = UNIT_LOADED;
+
+ r = unit_start(u, NULL);
+ if (r < 0)
+ return log_error_errno(r, "Unit start failed %m");
+
+ while (!IN_SET(SERVICE(u)->state, SERVICE_DEAD, SERVICE_FAILED)) {
+ r = sd_event_run(m->event, UINT64_MAX);
+ if (r < 0)
+ return log_error_errno(errno, "Event run failed %m");
+ }
+
+ cld_code = SERVICE(u)->exec_command[SERVICE_EXEC_START]->exec_status.code;
+ if (cld_code != CLD_EXITED)
+ return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "ExecStart didn't exited, code='%s'", sigchld_code_to_string(cld_code));
+
+ if (SERVICE(u)->state != SERVICE_DEAD)
+ return log_error_errno(SYNTHETIC_ERRNO(EBUSY), "Service is not dead");
+
+ return 0;
+}
+
+int main(int argc, char *argv[]) {
+ _cleanup_free_ char *unit_dir = NULL, *netcat_path = NULL;
+ _cleanup_(rm_rf_physical_and_freep) char *runtime_dir = NULL;
+ _cleanup_(manager_freep) Manager *m = NULL;
+ struct rlimit rl;
+ int r;
+
+ test_setup_logging(LOG_DEBUG);
+
+ if (detect_container() > 0)
+ return log_tests_skipped("test-socket-bind fails inside LXC and Docker containers: https://github.com/systemd/systemd/issues/9666");
+
+ assert_se(getrlimit(RLIMIT_MEMLOCK, &rl) >= 0);
+ rl.rlim_cur = rl.rlim_max = MAX(rl.rlim_max, CAN_MEMLOCK_SIZE);
+ (void) setrlimit_closest(RLIMIT_MEMLOCK, &rl);
+
+ if (!can_memlock())
+ return log_tests_skipped("Can't use mlock()");
+
+ r = bpf_socket_bind_supported();
+ if (r <= 0)
+ return log_tests_skipped("socket-bind is not supported");
+
+ if (find_netcat_executable(&netcat_path) != 0)
+ return log_tests_skipped("Cannot find netcat executable");
+
+ r = enter_cgroup_subroot(NULL);
+ if (r == -ENOMEDIUM)
+ return log_tests_skipped("cgroupfs not available");
+
+ assert_se(get_testdata_dir("units", &unit_dir) >= 0);
+ assert_se(set_unit_path(unit_dir) >= 0);
+ assert_se(runtime_dir = setup_fake_runtime_dir());
+
+ assert_se(manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_BASIC, &m) >= 0);
+ assert_se(manager_startup(m, NULL, NULL, NULL) >= 0);
+
+ assert_se(test_socket_bind(m, "socket_bind_test.service", netcat_path, "2000", STRV_MAKE("2000"), STRV_MAKE("any")) >= 0);
+ assert_se(test_socket_bind(m, "socket_bind_test.service", netcat_path, "2000", STRV_MAKE("ipv6:2001-2002"), STRV_MAKE("any")) >= 0);
+ assert_se(test_socket_bind(m, "socket_bind_test.service", netcat_path, "6666", STRV_MAKE("ipv4:6666", "6667"), STRV_MAKE("any")) >= 0);
+ assert_se(test_socket_bind(m, "socket_bind_test.service", netcat_path, "6666", STRV_MAKE("6667", "6668", ""), STRV_MAKE("any")) >= 0);
+ assert_se(test_socket_bind(m, "socket_bind_test.service", netcat_path, "7777", STRV_MAKE_EMPTY, STRV_MAKE_EMPTY) >= 0);
+ assert_se(test_socket_bind(m, "socket_bind_test.service", netcat_path, "8888", STRV_MAKE("any"), STRV_MAKE("any")) >= 0);
+ assert_se(test_socket_bind(m, "socket_bind_test.service", netcat_path, "8888", STRV_MAKE("ipv6:tcp:8888-8889"), STRV_MAKE("any")) >= 0);
+ assert_se(test_socket_bind(m, "socket_bind_test.service", netcat_path, "10000", STRV_MAKE("ipv6:udp:9999-10000"), STRV_MAKE("any")) >= 0);
+ assert_se(test_socket_bind(m, "socket_bind_test.service", netcat_path, "6666", STRV_MAKE("ipv4:tcp:6666"), STRV_MAKE("any")) >= 0);
+ assert_se(test_socket_bind(m, "socket_bind_test.service", netcat_path, "6666", STRV_MAKE("ipv4:udp:6666"), STRV_MAKE("any")) >= 0);
+ assert_se(test_socket_bind(m, "socket_bind_test.service", netcat_path, "6666", STRV_MAKE("tcp:6666"), STRV_MAKE("any")) >= 0);
+
+ return 0;
+}
diff --git a/src/test/test-socket-netlink.c b/src/test/test-socket-netlink.c
new file mode 100644
index 0000000..6dbd50f
--- /dev/null
+++ b/src/test/test-socket-netlink.c
@@ -0,0 +1,372 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "missing_network.h"
+#include "tests.h"
+#include "socket-netlink.h"
+#include "string-util.h"
+
+static void test_socket_address_parse_one(const char *in, int ret, int family, const char *expected) {
+ SocketAddress a;
+ _cleanup_free_ char *out = NULL;
+ int r;
+
+ r = socket_address_parse(&a, in);
+ if (r >= 0) {
+ r = socket_address_print(&a, &out);
+ if (r < 0)
+ log_error_errno(r, "Printing failed for \"%s\": %m", in);
+ assert_se(r >= 0);
+ assert_se(a.type == 0);
+ }
+
+ log_info("\"%s\" → %s %d → \"%s\" (expect %d / \"%s\")",
+ in,
+ r >= 0 ? "✓" : "✗", r,
+ empty_to_dash(out),
+ ret,
+ ret >= 0 ? expected ?: in : "-");
+ assert_se(r == ret);
+ if (r >= 0) {
+ assert_se(a.sockaddr.sa.sa_family == family);
+ assert_se(streq(out, expected ?: in));
+ }
+}
+
+TEST(socket_address_parse) {
+ test_socket_address_parse_one("junk", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("192.168.1.1", -EINVAL, 0, NULL);
+ test_socket_address_parse_one(".168.1.1", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("989.168.1.1", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("192.168.1.1:65536", -ERANGE, 0, NULL);
+ test_socket_address_parse_one("192.168.1.1:0", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("0", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("65536", -ERANGE, 0, NULL);
+
+ const int default_family = socket_ipv6_is_supported() ? AF_INET6 : AF_INET;
+
+ test_socket_address_parse_one("65535", 0, default_family,
+ default_family == AF_INET6 ? "[::]:65535": "0.0.0.0:65535");
+
+ /* The checks below will pass even if ipv6 is disabled in
+ * kernel. The underlying glibc's inet_pton() is just a string
+ * parser and doesn't make any syscalls. */
+
+ test_socket_address_parse_one("[::1]", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("[::1]8888", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("::1", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("[::1]:0", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("[::1]:65536", -ERANGE, 0, NULL);
+ test_socket_address_parse_one("[a:b:1]:8888", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("[::1]%lo:1234", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("[::1]%lo:0", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("[::1]%lo", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("[::1]%lo%lo:1234", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("[::1]% lo:1234", -EINVAL, 0, NULL);
+
+ test_socket_address_parse_one("8888", 0, default_family,
+ default_family == AF_INET6 ? "[::]:8888": "0.0.0.0:8888");
+ test_socket_address_parse_one("[2001:0db8:0000:85a3:0000:0000:ac1f:8001]:8888", 0, AF_INET6,
+ "[2001:db8:0:85a3::ac1f:8001]:8888");
+ test_socket_address_parse_one("[::1]:8888", 0, AF_INET6, NULL);
+ test_socket_address_parse_one("[::1]:1234%lo", 0, AF_INET6, NULL);
+ test_socket_address_parse_one("[::1]:0%lo", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("[::1]%lo", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("[::1]:1234%lo%lo", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("[::1]:1234%xxxxasdf", -ENODEV, 0, NULL);
+ test_socket_address_parse_one("192.168.1.254:8888", 0, AF_INET, NULL);
+ test_socket_address_parse_one("/foo/bar", 0, AF_UNIX, NULL);
+ test_socket_address_parse_one("/", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("@abstract", 0, AF_UNIX, NULL);
+
+ {
+ char aaa[SUN_PATH_LEN + 1] = "@";
+
+ memset(aaa + 1, 'a', SUN_PATH_LEN - 1);
+ char_array_0(aaa);
+
+ test_socket_address_parse_one(aaa, -EINVAL, 0, NULL);
+
+ aaa[SUN_PATH_LEN - 1] = '\0';
+ test_socket_address_parse_one(aaa, 0, AF_UNIX, NULL);
+ }
+
+ test_socket_address_parse_one("vsock:2:1234", 0, AF_VSOCK, NULL);
+ test_socket_address_parse_one("vsock::1234", 0, AF_VSOCK, NULL);
+ test_socket_address_parse_one("vsock:2:1234x", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("vsock:2x:1234", -EINVAL, 0, NULL);
+ test_socket_address_parse_one("vsock:2", -EINVAL, 0, NULL);
+}
+
+TEST(socket_address_parse_netlink) {
+ SocketAddress a;
+
+ assert_se(socket_address_parse_netlink(&a, "junk") < 0);
+ assert_se(socket_address_parse_netlink(&a, "") < 0);
+
+ assert_se(socket_address_parse_netlink(&a, "route") >= 0);
+ assert_se(a.sockaddr.nl.nl_family == AF_NETLINK);
+ assert_se(a.sockaddr.nl.nl_groups == 0);
+ assert_se(a.protocol == NETLINK_ROUTE);
+ assert_se(socket_address_parse_netlink(&a, "route") >= 0);
+ assert_se(socket_address_parse_netlink(&a, "route 10") >= 0);
+ assert_se(a.sockaddr.nl.nl_family == AF_NETLINK);
+ assert_se(a.sockaddr.nl.nl_groups == 10);
+ assert_se(a.protocol == NETLINK_ROUTE);
+
+ /* With spaces and tabs */
+ assert_se(socket_address_parse_netlink(&a, " kobject-uevent ") >= 0);
+ assert_se(a.sockaddr.nl.nl_family == AF_NETLINK);
+ assert_se(a.sockaddr.nl.nl_groups == 0);
+ assert_se(a.protocol == NETLINK_KOBJECT_UEVENT);
+ assert_se(socket_address_parse_netlink(&a, " \t kobject-uevent \t 10") >= 0);
+ assert_se(a.sockaddr.nl.nl_family == AF_NETLINK);
+ assert_se(a.sockaddr.nl.nl_groups == 10);
+ assert_se(a.protocol == NETLINK_KOBJECT_UEVENT);
+ assert_se(socket_address_parse_netlink(&a, "kobject-uevent\t10") >= 0);
+ assert_se(a.sockaddr.nl.nl_family == AF_NETLINK);
+ assert_se(a.sockaddr.nl.nl_groups == 10);
+ assert_se(a.protocol == NETLINK_KOBJECT_UEVENT);
+
+ /* trailing space is not supported */
+ assert_se(socket_address_parse_netlink(&a, "kobject-uevent\t10 ") < 0);
+
+ /* Group must be unsigned */
+ assert_se(socket_address_parse_netlink(&a, "kobject-uevent -1") < 0);
+
+ /* oss-fuzz #6884 */
+ assert_se(socket_address_parse_netlink(&a, "\xff") < 0);
+}
+
+TEST(socket_address_equal) {
+ SocketAddress a, b;
+
+ assert_se(socket_address_parse(&a, "192.168.1.1:8888") >= 0);
+ assert_se(socket_address_parse(&b, "192.168.1.1:888") >= 0);
+ assert_se(!socket_address_equal(&a, &b));
+
+ assert_se(socket_address_parse(&a, "192.168.1.1:8888") >= 0);
+ assert_se(socket_address_parse(&b, "192.16.1.1:8888") >= 0);
+ assert_se(!socket_address_equal(&a, &b));
+
+ assert_se(socket_address_parse(&a, "192.168.1.1:8888") >= 0);
+ assert_se(socket_address_parse(&b, "8888") >= 0);
+ assert_se(!socket_address_equal(&a, &b));
+
+ assert_se(socket_address_parse(&a, "192.168.1.1:8888") >= 0);
+ assert_se(socket_address_parse(&b, "/foo/bar/") >= 0);
+ assert_se(!socket_address_equal(&a, &b));
+
+ assert_se(socket_address_parse(&a, "192.168.1.1:8888") >= 0);
+ assert_se(socket_address_parse(&b, "192.168.1.1:8888") >= 0);
+ assert_se(socket_address_equal(&a, &b));
+
+ assert_se(socket_address_parse(&a, "/foo/bar") >= 0);
+ assert_se(socket_address_parse(&b, "/foo/bar") >= 0);
+ assert_se(socket_address_equal(&a, &b));
+
+ assert_se(socket_address_parse(&a, "[::1]:8888") >= 0);
+ assert_se(socket_address_parse(&b, "[::1]:8888") >= 0);
+ assert_se(socket_address_equal(&a, &b));
+
+ assert_se(socket_address_parse(&a, "@abstract") >= 0);
+ assert_se(socket_address_parse(&b, "@abstract") >= 0);
+ assert_se(socket_address_equal(&a, &b));
+
+ assert_se(socket_address_parse_netlink(&a, "firewall") >= 0);
+ assert_se(socket_address_parse_netlink(&b, "firewall") >= 0);
+ assert_se(socket_address_equal(&a, &b));
+
+ assert_se(socket_address_parse(&a, "vsock:2:1234") >= 0);
+ assert_se(socket_address_parse(&b, "vsock:2:1234") >= 0);
+ assert_se(socket_address_equal(&a, &b));
+ assert_se(socket_address_parse(&b, "vsock:2:1235") >= 0);
+ assert_se(!socket_address_equal(&a, &b));
+ assert_se(socket_address_parse(&b, "vsock:3:1234") >= 0);
+ assert_se(!socket_address_equal(&a, &b));
+}
+
+TEST(socket_address_get_path) {
+ SocketAddress a;
+
+ assert_se(socket_address_parse(&a, "192.168.1.1:8888") >= 0);
+ assert_se(!socket_address_get_path(&a));
+
+ assert_se(socket_address_parse(&a, "@abstract") >= 0);
+ assert_se(!socket_address_get_path(&a));
+
+ assert_se(socket_address_parse(&a, "[::1]:8888") >= 0);
+ assert_se(!socket_address_get_path(&a));
+
+ assert_se(socket_address_parse(&a, "/foo/bar") >= 0);
+ assert_se(streq(socket_address_get_path(&a), "/foo/bar"));
+
+ assert_se(socket_address_parse(&a, "vsock:2:1234") >= 0);
+ assert_se(!socket_address_get_path(&a));
+}
+
+TEST(socket_address_is) {
+ SocketAddress a;
+
+ assert_se(socket_address_parse(&a, "192.168.1.1:8888") >= 0);
+ assert_se( socket_address_is(&a, "192.168.1.1:8888", 0 /* unspecified yet */));
+ assert_se(!socket_address_is(&a, "route", 0));
+ assert_se(!socket_address_is(&a, "route", SOCK_STREAM));
+ assert_se(!socket_address_is(&a, "192.168.1.1:8888", SOCK_RAW));
+ assert_se(!socket_address_is(&a, "192.168.1.1:8888", SOCK_STREAM));
+ a.type = SOCK_STREAM;
+ assert_se( socket_address_is(&a, "192.168.1.1:8888", SOCK_STREAM));
+}
+
+TEST(socket_address_is_netlink) {
+ SocketAddress a;
+
+ assert_se(socket_address_parse_netlink(&a, "route 10") >= 0);
+ assert_se( socket_address_is_netlink(&a, "route 10"));
+ assert_se(!socket_address_is_netlink(&a, "192.168.1.1:8888"));
+ assert_se(!socket_address_is_netlink(&a, "route 1"));
+}
+
+static void test_in_addr_ifindex_to_string_one(int f, const char *a, int ifindex, const char *b) {
+ _cleanup_free_ char *r = NULL;
+ union in_addr_union ua, uuaa;
+ int ff, ifindex2;
+
+ assert_se(in_addr_from_string(f, a, &ua) >= 0);
+ assert_se(in_addr_ifindex_to_string(f, &ua, ifindex, &r) >= 0);
+ printf("test_in_addr_ifindex_to_string_one: %s == %s\n", b, r);
+ assert_se(streq(b, r));
+
+ assert_se(in_addr_ifindex_from_string_auto(b, &ff, &uuaa, &ifindex2) >= 0);
+ assert_se(ff == f);
+ assert_se(in_addr_equal(f, &ua, &uuaa));
+ assert_se(ifindex2 == ifindex || ifindex2 == 0);
+}
+
+TEST(in_addr_ifindex_to_string) {
+ test_in_addr_ifindex_to_string_one(AF_INET, "192.168.0.1", 7, "192.168.0.1");
+ test_in_addr_ifindex_to_string_one(AF_INET, "10.11.12.13", 9, "10.11.12.13");
+ test_in_addr_ifindex_to_string_one(AF_INET6, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", 10, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+ test_in_addr_ifindex_to_string_one(AF_INET6, "::1", 11, "::1");
+ test_in_addr_ifindex_to_string_one(AF_INET6, "fe80::", 12, "fe80::%12");
+ test_in_addr_ifindex_to_string_one(AF_INET6, "fe80::", 0, "fe80::");
+ test_in_addr_ifindex_to_string_one(AF_INET6, "fe80::14", 12, "fe80::14%12");
+ test_in_addr_ifindex_to_string_one(AF_INET6, "fe80::15", -7, "fe80::15");
+ test_in_addr_ifindex_to_string_one(AF_INET6, "fe80::16", LOOPBACK_IFINDEX, "fe80::16%1");
+}
+
+TEST(in_addr_ifindex_from_string_auto) {
+ int family, ifindex;
+ union in_addr_union ua;
+
+ /* Most in_addr_ifindex_from_string_auto() invocations have already been tested above, but let's test some more */
+
+ assert_se(in_addr_ifindex_from_string_auto("fe80::17", &family, &ua, &ifindex) >= 0);
+ assert_se(family == AF_INET6);
+ assert_se(ifindex == 0);
+
+ assert_se(in_addr_ifindex_from_string_auto("fe80::18%19", &family, &ua, &ifindex) >= 0);
+ assert_se(family == AF_INET6);
+ assert_se(ifindex == 19);
+
+ assert_se(in_addr_ifindex_from_string_auto("fe80::18%lo", &family, &ua, &ifindex) >= 0);
+ assert_se(family == AF_INET6);
+ assert_se(ifindex == LOOPBACK_IFINDEX);
+
+ assert_se(in_addr_ifindex_from_string_auto("fe80::19%thisinterfacecantexist", &family, &ua, &ifindex) == -ENODEV);
+}
+
+static void test_in_addr_ifindex_name_from_string_auto_one(const char *a, const char *expected) {
+ int family, ifindex;
+ union in_addr_union ua;
+ _cleanup_free_ char *server_name = NULL;
+
+ assert_se(in_addr_ifindex_name_from_string_auto(a, &family, &ua, &ifindex, &server_name) >= 0);
+ assert_se(streq_ptr(server_name, expected));
+}
+
+TEST(in_addr_ifindex_name_from_string_auto) {
+ test_in_addr_ifindex_name_from_string_auto_one("192.168.0.1", NULL);
+ test_in_addr_ifindex_name_from_string_auto_one("192.168.0.1#test.com", "test.com");
+ test_in_addr_ifindex_name_from_string_auto_one("fe80::18%19", NULL);
+ test_in_addr_ifindex_name_from_string_auto_one("fe80::18%19#another.test.com", "another.test.com");
+}
+
+static void test_in_addr_port_ifindex_name_from_string_auto_one(const char *str, int family, uint16_t port, int ifindex,
+ const char *server_name, const char *str_repr) {
+ union in_addr_union a;
+ uint16_t p;
+ int f, i;
+ char *fake;
+
+ log_info("%s: %s", __func__, str);
+
+ {
+ _cleanup_free_ char *name = NULL, *x = NULL;
+ assert_se(in_addr_port_ifindex_name_from_string_auto(str, &f, &a, &p, &i, &name) == 0);
+ assert_se(family == f);
+ assert_se(port == p);
+ assert_se(ifindex == i);
+ assert_se(streq_ptr(server_name, name));
+ assert_se(in_addr_port_ifindex_name_to_string(f, &a, p, i, name, &x) >= 0);
+ assert_se(streq(str_repr ?: str, x));
+ }
+
+ if (port > 0)
+ assert_se(in_addr_port_ifindex_name_from_string_auto(str, &f, &a, NULL, &i, &fake) == -EINVAL);
+ else {
+ _cleanup_free_ char *name = NULL, *x = NULL;
+ assert_se(in_addr_port_ifindex_name_from_string_auto(str, &f, &a, NULL, &i, &name) == 0);
+ assert_se(family == f);
+ assert_se(ifindex == i);
+ assert_se(streq_ptr(server_name, name));
+ assert_se(in_addr_port_ifindex_name_to_string(f, &a, 0, i, name, &x) >= 0);
+ assert_se(streq(str_repr ?: str, x));
+ }
+
+ if (ifindex > 0)
+ assert_se(in_addr_port_ifindex_name_from_string_auto(str, &f, &a, &p, NULL, &fake) == -EINVAL);
+ else {
+ _cleanup_free_ char *name = NULL, *x = NULL;
+ assert_se(in_addr_port_ifindex_name_from_string_auto(str, &f, &a, &p, NULL, &name) == 0);
+ assert_se(family == f);
+ assert_se(port == p);
+ assert_se(streq_ptr(server_name, name));
+ assert_se(in_addr_port_ifindex_name_to_string(f, &a, p, 0, name, &x) >= 0);
+ assert_se(streq(str_repr ?: str, x));
+ }
+
+ if (server_name)
+ assert_se(in_addr_port_ifindex_name_from_string_auto(str, &f, &a, &p, &i, NULL) == -EINVAL);
+ else {
+ _cleanup_free_ char *x = NULL;
+ assert_se(in_addr_port_ifindex_name_from_string_auto(str, &f, &a, &p, &i, NULL) == 0);
+ assert_se(family == f);
+ assert_se(port == p);
+ assert_se(ifindex == i);
+ assert_se(in_addr_port_ifindex_name_to_string(f, &a, p, i, NULL, &x) >= 0);
+ assert_se(streq(str_repr ?: str, x));
+ }
+}
+
+TEST(in_addr_port_ifindex_name_from_string_auto) {
+ test_in_addr_port_ifindex_name_from_string_auto_one("192.168.0.1", AF_INET, 0, 0, NULL, NULL);
+ test_in_addr_port_ifindex_name_from_string_auto_one("192.168.0.1#test.com", AF_INET, 0, 0, "test.com", NULL);
+ test_in_addr_port_ifindex_name_from_string_auto_one("192.168.0.1:53", AF_INET, 53, 0, NULL, NULL);
+ test_in_addr_port_ifindex_name_from_string_auto_one("192.168.0.1:53#example.com", AF_INET, 53, 0, "example.com", NULL);
+ test_in_addr_port_ifindex_name_from_string_auto_one("fe80::18", AF_INET6, 0, 0, NULL, NULL);
+ test_in_addr_port_ifindex_name_from_string_auto_one("fe80::18#hoge.com", AF_INET6, 0, 0, "hoge.com", NULL);
+ test_in_addr_port_ifindex_name_from_string_auto_one("fe80::18%19", AF_INET6, 0, 19, NULL, NULL);
+ test_in_addr_port_ifindex_name_from_string_auto_one("fe80::18%lo", AF_INET6, 0, 1, NULL, "fe80::18%1");
+ test_in_addr_port_ifindex_name_from_string_auto_one("[fe80::18]:53", AF_INET6, 53, 0, NULL, NULL);
+ test_in_addr_port_ifindex_name_from_string_auto_one("[fe80::18]:53%19", AF_INET6, 53, 19, NULL, NULL);
+ test_in_addr_port_ifindex_name_from_string_auto_one("[fe80::18]:53%lo", AF_INET6, 53, 1, NULL, "[fe80::18]:53%1");
+ test_in_addr_port_ifindex_name_from_string_auto_one("fe80::18%19#hoge.com", AF_INET6, 0, 19, "hoge.com", NULL);
+ test_in_addr_port_ifindex_name_from_string_auto_one("[fe80::18]:53#hoge.com", AF_INET6, 53, 0, "hoge.com", NULL);
+ test_in_addr_port_ifindex_name_from_string_auto_one("[fe80::18]:53%19", AF_INET6, 53, 19, NULL, NULL);
+ test_in_addr_port_ifindex_name_from_string_auto_one("[fe80::18]:53%19#hoge.com", AF_INET6, 53, 19, "hoge.com", NULL);
+ test_in_addr_port_ifindex_name_from_string_auto_one("[fe80::18]:53%lo", AF_INET6, 53, 1, NULL, "[fe80::18]:53%1");
+ test_in_addr_port_ifindex_name_from_string_auto_one("[fe80::18]:53%lo#hoge.com", AF_INET6, 53, 1, "hoge.com", "[fe80::18]:53%1#hoge.com");
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-socket-util.c b/src/test/test-socket-util.c
new file mode 100644
index 0000000..e9c776a
--- /dev/null
+++ b/src/test/test-socket-util.c
@@ -0,0 +1,593 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <grp.h>
+#include <net/if_arp.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "async.h"
+#include "escape.h"
+#include "exit-status.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "in-addr-util.h"
+#include "iovec-util.h"
+#include "log.h"
+#include "macro.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "random-util.h"
+#include "rm-rf.h"
+#include "socket-util.h"
+#include "string-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+assert_cc(SUN_PATH_LEN == 108);
+
+TEST(ifname_valid) {
+ assert_se( ifname_valid("foo"));
+ assert_se( ifname_valid("eth0"));
+
+ assert_se(!ifname_valid("0"));
+ assert_se(!ifname_valid("99"));
+ assert_se( ifname_valid("a99"));
+ assert_se( ifname_valid("99a"));
+
+ assert_se(!ifname_valid(NULL));
+ assert_se(!ifname_valid(""));
+ assert_se(!ifname_valid(" "));
+ assert_se(!ifname_valid(" foo"));
+ assert_se(!ifname_valid("bar\n"));
+ assert_se(!ifname_valid("."));
+ assert_se(!ifname_valid(".."));
+ assert_se(ifname_valid("foo.bar"));
+ assert_se(!ifname_valid("x:y"));
+
+ assert_se( ifname_valid_full("xxxxxxxxxxxxxxx", 0));
+ assert_se(!ifname_valid_full("xxxxxxxxxxxxxxxx", 0));
+ assert_se( ifname_valid_full("xxxxxxxxxxxxxxxx", IFNAME_VALID_ALTERNATIVE));
+ assert_se( ifname_valid_full("xxxxxxxxxxxxxxxx", IFNAME_VALID_ALTERNATIVE));
+ assert_se(!ifname_valid_full("999", IFNAME_VALID_ALTERNATIVE));
+ assert_se( ifname_valid_full("999", IFNAME_VALID_ALTERNATIVE | IFNAME_VALID_NUMERIC));
+ assert_se(!ifname_valid_full("0", IFNAME_VALID_ALTERNATIVE | IFNAME_VALID_NUMERIC));
+}
+
+static void test_socket_print_unix_one(const char *in, size_t len_in, const char *expected) {
+ _cleanup_free_ char *out = NULL, *c = NULL;
+
+ assert_se(len_in <= SUN_PATH_LEN);
+ SocketAddress a = { .sockaddr = { .un = { .sun_family = AF_UNIX } },
+ .size = offsetof(struct sockaddr_un, sun_path) + len_in,
+ .type = SOCK_STREAM,
+ };
+ memcpy(a.sockaddr.un.sun_path, in, len_in);
+
+ assert_se(socket_address_print(&a, &out) >= 0);
+ assert_se(c = cescape(in));
+ log_info("\"%s\" → \"%s\" (expect \"%s\")", in, out, expected);
+ assert_se(streq(out, expected));
+}
+
+TEST(socket_print_unix) {
+ /* Some additional tests for abstract addresses which we don't parse */
+
+ test_socket_print_unix_one("\0\0\0\0", 4, "@\\000\\000\\000");
+ test_socket_print_unix_one("@abs", 5, "@abs");
+ test_socket_print_unix_one("\n", 2, "\\n");
+ test_socket_print_unix_one("", 1, "<unnamed>");
+ test_socket_print_unix_one("\0", 1, "<unnamed>");
+ test_socket_print_unix_one("\0_________________________there's 108 characters in this string_____________________________________________", 108,
+ "@_________________________there\\'s 108 characters in this string_____________________________________________");
+ test_socket_print_unix_one("////////////////////////////////////////////////////////////////////////////////////////////////////////////", 108,
+ "////////////////////////////////////////////////////////////////////////////////////////////////////////////");
+ test_socket_print_unix_one("\0\a\b\n\255", 6, "@\\a\\b\\n\\255\\000");
+}
+
+TEST(sockaddr_equal) {
+ union sockaddr_union a = {
+ .in.sin_family = AF_INET,
+ .in.sin_port = 0,
+ .in.sin_addr.s_addr = htobe32(INADDR_ANY),
+ };
+ union sockaddr_union b = {
+ .in.sin_family = AF_INET,
+ .in.sin_port = 0,
+ .in.sin_addr.s_addr = htobe32(INADDR_ANY),
+ };
+ union sockaddr_union c = {
+ .in.sin_family = AF_INET,
+ .in.sin_port = 0,
+ .in.sin_addr.s_addr = htobe32(1234),
+ };
+ union sockaddr_union d = {
+ .in6.sin6_family = AF_INET6,
+ .in6.sin6_port = 0,
+ .in6.sin6_addr = IN6ADDR_ANY_INIT,
+ };
+ union sockaddr_union e = {
+ .vm.svm_family = AF_VSOCK,
+ .vm.svm_port = 0,
+ .vm.svm_cid = VMADDR_CID_ANY,
+ };
+
+ assert_se(sockaddr_equal(&a, &a));
+ assert_se(sockaddr_equal(&a, &b));
+ assert_se(sockaddr_equal(&d, &d));
+ assert_se(sockaddr_equal(&e, &e));
+ assert_se(!sockaddr_equal(&a, &c));
+ assert_se(!sockaddr_equal(&b, &c));
+ assert_se(!sockaddr_equal(&a, &e));
+}
+
+TEST(sockaddr_un_len) {
+ static const struct sockaddr_un fs = {
+ .sun_family = AF_UNIX,
+ .sun_path = "/foo/bar/waldo",
+ };
+
+ static const struct sockaddr_un abstract = {
+ .sun_family = AF_UNIX,
+ .sun_path = "\0foobar",
+ };
+
+ assert_se(SOCKADDR_UN_LEN(fs) == offsetof(struct sockaddr_un, sun_path) + strlen(fs.sun_path) + 1);
+ assert_se(SOCKADDR_UN_LEN(abstract) == offsetof(struct sockaddr_un, sun_path) + 1 + strlen(abstract.sun_path + 1));
+}
+
+TEST(in_addr_is_multicast) {
+ union in_addr_union a, b;
+ int f;
+
+ assert_se(in_addr_from_string_auto("192.168.3.11", &f, &a) >= 0);
+ assert_se(in_addr_is_multicast(f, &a) == 0);
+
+ assert_se(in_addr_from_string_auto("224.0.0.1", &f, &a) >= 0);
+ assert_se(in_addr_is_multicast(f, &a) == 1);
+
+ assert_se(in_addr_from_string_auto("FF01:0:0:0:0:0:0:1", &f, &b) >= 0);
+ assert_se(in_addr_is_multicast(f, &b) == 1);
+
+ assert_se(in_addr_from_string_auto("2001:db8::c:69b:aeff:fe53:743e", &f, &b) >= 0);
+ assert_se(in_addr_is_multicast(f, &b) == 0);
+}
+
+TEST(getpeercred_getpeergroups) {
+ int r;
+
+ r = safe_fork("(getpeercred)", FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT, NULL);
+ assert_se(r >= 0);
+
+ if (r == 0) {
+ static const gid_t gids[] = { 3, 4, 5, 6, 7 };
+ gid_t *test_gids;
+ size_t n_test_gids;
+ uid_t test_uid;
+ gid_t test_gid;
+ struct ucred ucred;
+ int pair[2];
+
+ if (geteuid() == 0) {
+ test_uid = 1;
+ test_gid = 2;
+ test_gids = (gid_t*) gids;
+ n_test_gids = ELEMENTSOF(gids);
+
+ assert_se(setgroups(n_test_gids, test_gids) >= 0);
+ assert_se(setresgid(test_gid, test_gid, test_gid) >= 0);
+ assert_se(setresuid(test_uid, test_uid, test_uid) >= 0);
+
+ } else {
+ long ngroups_max;
+
+ test_uid = getuid();
+ test_gid = getgid();
+
+ ngroups_max = sysconf(_SC_NGROUPS_MAX);
+ assert_se(ngroups_max > 0);
+
+ test_gids = newa(gid_t, ngroups_max);
+
+ r = getgroups(ngroups_max, test_gids);
+ assert_se(r >= 0);
+ n_test_gids = (size_t) r;
+ }
+
+ assert_se(socketpair(AF_UNIX, SOCK_STREAM, 0, pair) >= 0);
+
+ assert_se(getpeercred(pair[0], &ucred) >= 0);
+
+ assert_se(ucred.uid == test_uid);
+ assert_se(ucred.gid == test_gid);
+ assert_se(ucred.pid == getpid_cached());
+
+ {
+ _cleanup_free_ gid_t *peer_groups = NULL;
+
+ r = getpeergroups(pair[0], &peer_groups);
+ assert_se(r >= 0 || IN_SET(r, -EOPNOTSUPP, -ENOPROTOOPT));
+
+ if (r >= 0) {
+ assert_se((size_t) r == n_test_gids);
+ assert_se(memcmp(peer_groups, test_gids, sizeof(gid_t) * n_test_gids) == 0);
+ }
+ }
+
+ safe_close_pair(pair);
+ _exit(EXIT_SUCCESS);
+ }
+}
+
+TEST(passfd_read) {
+ static const char file_contents[] = "test contents for passfd";
+ _cleanup_close_pair_ int pair[2];
+ int r;
+
+ assert_se(socketpair(AF_UNIX, SOCK_DGRAM, 0, pair) >= 0);
+
+ r = safe_fork("(passfd_read)", FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT, NULL);
+ assert_se(r >= 0);
+
+ if (r == 0) {
+ /* Child */
+ pair[0] = safe_close(pair[0]);
+
+ char tmpfile[] = "/tmp/test-socket-util-passfd-read-XXXXXX";
+ assert_se(write_tmpfile(tmpfile, file_contents) == 0);
+
+ _cleanup_close_ int tmpfd = open(tmpfile, O_RDONLY);
+ assert_se(tmpfd >= 0);
+ assert_se(unlink(tmpfile) == 0);
+
+ assert_se(send_one_fd(pair[1], tmpfd, MSG_DONTWAIT) == 0);
+ _exit(EXIT_SUCCESS);
+ }
+
+ /* Parent */
+ char buf[64];
+ struct iovec iov = IOVEC_MAKE(buf, sizeof(buf)-1);
+ _cleanup_close_ int fd;
+
+ pair[1] = safe_close(pair[1]);
+
+ assert_se(receive_one_fd_iov(pair[0], &iov, 1, MSG_DONTWAIT, &fd) == 0);
+
+ assert_se(fd >= 0);
+ r = read(fd, buf, sizeof(buf)-1);
+ assert_se(r >= 0);
+ buf[r] = 0;
+ assert_se(streq(buf, file_contents));
+}
+
+TEST(passfd_contents_read) {
+ _cleanup_close_pair_ int pair[2];
+ static const char file_contents[] = "test contents in the file";
+ static const char wire_contents[] = "test contents on the wire";
+ int r;
+
+ assert_se(socketpair(AF_UNIX, SOCK_DGRAM, 0, pair) >= 0);
+
+ r = safe_fork("(passfd_contents_read)", FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT, NULL);
+ assert_se(r >= 0);
+
+ if (r == 0) {
+ /* Child */
+ struct iovec iov = IOVEC_MAKE_STRING(wire_contents);
+ char tmpfile[] = "/tmp/test-socket-util-passfd-contents-read-XXXXXX";
+
+ pair[0] = safe_close(pair[0]);
+
+ assert_se(write_tmpfile(tmpfile, file_contents) == 0);
+
+ _cleanup_close_ int tmpfd = open(tmpfile, O_RDONLY);
+ assert_se(tmpfd >= 0);
+ assert_se(unlink(tmpfile) == 0);
+
+ assert_se(send_one_fd_iov(pair[1], tmpfd, &iov, 1, MSG_DONTWAIT) > 0);
+ _exit(EXIT_SUCCESS);
+ }
+
+ /* Parent */
+ char buf[64];
+ struct iovec iov = IOVEC_MAKE(buf, sizeof(buf)-1);
+ _cleanup_close_ int fd;
+ ssize_t k;
+
+ pair[1] = safe_close(pair[1]);
+
+ k = receive_one_fd_iov(pair[0], &iov, 1, MSG_DONTWAIT, &fd);
+ assert_se(k > 0);
+ buf[k] = 0;
+ assert_se(streq(buf, wire_contents));
+
+ assert_se(fd >= 0);
+ r = read(fd, buf, sizeof(buf)-1);
+ assert_se(r >= 0);
+ buf[r] = 0;
+ assert_se(streq(buf, file_contents));
+}
+
+TEST(pass_many_fds_contents_read) {
+ _cleanup_close_pair_ int pair[2];
+ static const char file_contents[][STRLEN("test contents in the fileX") + 1] = {
+ "test contents in the file0",
+ "test contents in the file1",
+ "test contents in the file2"
+ };
+ static const char wire_contents[] = "test contents on the wire";
+ int r;
+
+ assert_se(socketpair(AF_UNIX, SOCK_DGRAM, 0, pair) >= 0);
+
+ r = safe_fork("(passfd_contents_read)", FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT, NULL);
+ assert_se(r >= 0);
+
+ if (r == 0) {
+ /* Child */
+ struct iovec iov = IOVEC_MAKE_STRING(wire_contents);
+ char tmpfile[][STRLEN("/tmp/test-socket-util-passfd-contents-read-XXXXXX") + 1] = {
+ "/tmp/test-socket-util-passfd-contents-read-XXXXXX",
+ "/tmp/test-socket-util-passfd-contents-read-XXXXXX",
+ "/tmp/test-socket-util-passfd-contents-read-XXXXXX"
+ };
+ int tmpfds[3] = EBADF_TRIPLET;
+
+ pair[0] = safe_close(pair[0]);
+
+ for (size_t i = 0; i < 3; ++i) {
+ assert_se(write_tmpfile(tmpfile[i], file_contents[i]) == 0);
+ tmpfds[i] = open(tmpfile[i], O_RDONLY);
+ assert_se(tmpfds[i] >= 0);
+ assert_se(unlink(tmpfile[i]) == 0);
+ }
+
+ assert_se(send_many_fds_iov(pair[1], tmpfds, 3, &iov, 1, MSG_DONTWAIT) > 0);
+ close_many(tmpfds, 3);
+ _exit(EXIT_SUCCESS);
+ }
+
+ /* Parent */
+ char buf[64];
+ struct iovec iov = IOVEC_MAKE(buf, sizeof(buf)-1);
+ _cleanup_free_ int *fds = NULL;
+ size_t n_fds = 0;
+ ssize_t k;
+
+ pair[1] = safe_close(pair[1]);
+
+ k = receive_many_fds_iov(pair[0], &iov, 1, &fds, &n_fds, MSG_DONTWAIT);
+ assert_se(k > 0);
+ buf[k] = 0;
+ assert_se(streq(buf, wire_contents));
+
+ assert_se(n_fds == 3);
+
+ for (size_t i = 0; i < 3; ++i) {
+ assert_se(fds[i] >= 0);
+ r = read(fds[i], buf, sizeof(buf)-1);
+ assert_se(r >= 0);
+ buf[r] = 0;
+ assert_se(streq(buf, file_contents[i]));
+ safe_close(fds[i]);
+ }
+}
+
+TEST(receive_nopassfd) {
+ _cleanup_close_pair_ int pair[2];
+ static const char wire_contents[] = "no fd passed here";
+ int r;
+
+ assert_se(socketpair(AF_UNIX, SOCK_DGRAM, 0, pair) >= 0);
+
+ r = safe_fork("(receive_nopassfd)", FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT, NULL);
+ assert_se(r >= 0);
+
+ if (r == 0) {
+ /* Child */
+ struct iovec iov = IOVEC_MAKE_STRING(wire_contents);
+
+ pair[0] = safe_close(pair[0]);
+
+ assert_se(send_one_fd_iov(pair[1], -1, &iov, 1, MSG_DONTWAIT) > 0);
+ _exit(EXIT_SUCCESS);
+ }
+
+ /* Parent */
+ char buf[64];
+ struct iovec iov = IOVEC_MAKE(buf, sizeof(buf)-1);
+ int fd = -999;
+ ssize_t k;
+
+ pair[1] = safe_close(pair[1]);
+
+ k = receive_one_fd_iov(pair[0], &iov, 1, MSG_DONTWAIT, &fd);
+ assert_se(k > 0);
+ buf[k] = 0;
+ assert_se(streq(buf, wire_contents));
+
+ /* no fd passed here, confirm it was reset */
+ assert_se(fd == -EBADF);
+}
+
+TEST(send_nodata_nofd) {
+ _cleanup_close_pair_ int pair[2];
+ int r;
+
+ assert_se(socketpair(AF_UNIX, SOCK_DGRAM, 0, pair) >= 0);
+
+ r = safe_fork("(send_nodata_nofd)", FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT, NULL);
+ assert_se(r >= 0);
+
+ if (r == 0) {
+ /* Child */
+ pair[0] = safe_close(pair[0]);
+
+ assert_se(send_one_fd_iov(pair[1], -1, NULL, 0, MSG_DONTWAIT) == -EINVAL);
+ _exit(EXIT_SUCCESS);
+ }
+
+ /* Parent */
+ char buf[64];
+ struct iovec iov = IOVEC_MAKE(buf, sizeof(buf)-1);
+ int fd = -999;
+ ssize_t k;
+
+ pair[1] = safe_close(pair[1]);
+
+ k = receive_one_fd_iov(pair[0], &iov, 1, MSG_DONTWAIT, &fd);
+ /* recvmsg() will return errno EAGAIN if nothing was sent */
+ assert_se(k == -EAGAIN);
+
+ /* receive_one_fd_iov returned error, so confirm &fd wasn't touched */
+ assert_se(fd == -999);
+}
+
+TEST(send_emptydata) {
+ _cleanup_close_pair_ int pair[2];
+ int r;
+
+ assert_se(socketpair(AF_UNIX, SOCK_DGRAM, 0, pair) >= 0);
+
+ r = safe_fork("(send_emptydata)", FORK_DEATHSIG_SIGTERM|FORK_LOG|FORK_WAIT, NULL);
+ assert_se(r >= 0);
+
+ if (r == 0) {
+ /* Child */
+ struct iovec iov = IOVEC_MAKE_STRING(""); /* zero-length iov */
+ assert_se(iov.iov_len == 0);
+
+ pair[0] = safe_close(pair[0]);
+
+ /* This will succeed, since iov is set. */
+ assert_se(send_one_fd_iov(pair[1], -1, &iov, 1, MSG_DONTWAIT) == 0);
+ _exit(EXIT_SUCCESS);
+ }
+
+ /* Parent */
+ char buf[64];
+ struct iovec iov = IOVEC_MAKE(buf, sizeof(buf)-1);
+ int fd = -999;
+ ssize_t k;
+
+ pair[1] = safe_close(pair[1]);
+
+ k = receive_one_fd_iov(pair[0], &iov, 1, MSG_DONTWAIT, &fd);
+ /* receive_one_fd_iov() returns -EIO if an fd is not found and no data was returned. */
+ assert_se(k == -EIO);
+
+ /* receive_one_fd_iov returned error, so confirm &fd wasn't touched */
+ assert_se(fd == -999);
+}
+
+TEST(flush_accept) {
+ _cleanup_close_ int listen_stream, listen_dgram, listen_seqpacket, connect_stream, connect_dgram, connect_seqpacket;
+ static const union sockaddr_union sa = { .un.sun_family = AF_UNIX };
+ union sockaddr_union lsa;
+ socklen_t l;
+
+ listen_stream = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+ assert_se(listen_stream >= 0);
+
+ listen_dgram = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+ assert_se(listen_dgram >= 0);
+
+ listen_seqpacket = socket(AF_UNIX, SOCK_SEQPACKET|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+ assert_se(listen_seqpacket >= 0);
+
+ assert_se(flush_accept(listen_stream) < 0);
+ assert_se(flush_accept(listen_dgram) < 0);
+ assert_se(flush_accept(listen_seqpacket) < 0);
+
+ assert_se(bind(listen_stream, &sa.sa, sizeof(sa_family_t)) >= 0);
+ assert_se(bind(listen_dgram, &sa.sa, sizeof(sa_family_t)) >= 0);
+ assert_se(bind(listen_seqpacket, &sa.sa, sizeof(sa_family_t)) >= 0);
+
+ assert_se(flush_accept(listen_stream) < 0);
+ assert_se(flush_accept(listen_dgram) < 0);
+ assert_se(flush_accept(listen_seqpacket) < 0);
+
+ assert_se(listen(listen_stream, SOMAXCONN_DELUXE) >= 0);
+ assert_se(listen(listen_dgram, SOMAXCONN_DELUXE) < 0);
+ assert_se(listen(listen_seqpacket, SOMAXCONN_DELUXE) >= 0);
+
+ assert_se(flush_accept(listen_stream) >= 0);
+ assert_se(flush_accept(listen_dgram) < 0);
+ assert_se(flush_accept(listen_seqpacket) >= 0);
+
+ connect_stream = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+ assert_se(connect_stream >= 0);
+
+ connect_dgram = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+ assert_se(connect_dgram >= 0);
+
+ connect_seqpacket = socket(AF_UNIX, SOCK_SEQPACKET|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+ assert_se(connect_seqpacket >= 0);
+
+ l = sizeof(lsa);
+ assert_se(getsockname(listen_stream, &lsa.sa, &l) >= 0);
+ assert_se(connect(connect_stream, &lsa.sa, l) >= 0);
+
+ l = sizeof(lsa);
+ assert_se(getsockname(listen_dgram, &lsa.sa, &l) >= 0);
+ assert_se(connect(connect_dgram, &lsa.sa, l) >= 0);
+
+ l = sizeof(lsa);
+ assert_se(getsockname(listen_seqpacket, &lsa.sa, &l) >= 0);
+ assert_se(connect(connect_seqpacket, &lsa.sa, l) >= 0);
+
+ assert_se(flush_accept(listen_stream) >= 0);
+ assert_se(flush_accept(listen_dgram) < 0);
+ assert_se(flush_accept(listen_seqpacket) >= 0);
+}
+
+TEST(ipv6_enabled) {
+ log_info("IPv6 supported: %s", yes_no(socket_ipv6_is_supported()));
+ log_info("IPv6 enabled: %s", yes_no(socket_ipv6_is_enabled()));
+}
+
+TEST(sockaddr_un_set_path) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ _cleanup_(unlink_and_freep) char *sh = NULL;
+ _cleanup_free_ char *j = NULL;
+ union sockaddr_union sa;
+ _cleanup_close_ int fd1, fd2, fd3;
+
+ assert_se(mkdtemp_malloc("/tmp/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaXXXXXX", &t) >= 0);
+ assert_se(strlen(t) > SUN_PATH_LEN);
+
+ assert_se(j = path_join(t, "sock"));
+ assert_se(sockaddr_un_set_path(&sa.un, j) == -ENAMETOOLONG); /* too long for AF_UNIX socket */
+
+ assert_se(asprintf(&sh, "/tmp/%" PRIx64, random_u64()) >= 0);
+ assert_se(symlink(t, sh) >= 0); /* create temporary symlink, to access it anyway */
+
+ free(j);
+ assert_se(j = path_join(sh, "sock"));
+ assert_se(sockaddr_un_set_path(&sa.un, j) >= 0);
+
+ fd1 = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0);
+ assert_se(fd1 >= 0);
+ assert_se(bind(fd1, &sa.sa, SOCKADDR_LEN(sa)) >= 0);
+ assert_se(listen(fd1, 1) >= 0);
+
+ sh = unlink_and_free(sh); /* remove temporary symlink */
+
+ fd2 = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0);
+ assert_se(fd2 >= 0);
+ assert_se(connect(fd2, &sa.sa, SOCKADDR_LEN(sa)) < 0);
+ assert_se(errno == ENOENT); /* we removed the symlink, must fail */
+
+ free(j);
+ assert_se(j = path_join(t, "sock"));
+
+ fd3 = open(j, O_CLOEXEC|O_PATH|O_NOFOLLOW);
+ assert_se(fd3 > 0);
+ assert_se(sockaddr_un_set_path(&sa.un, FORMAT_PROC_FD_PATH(fd3)) >= 0); /* connect via O_PATH instead, circumventing 108ch limit */
+
+ assert_se(connect(fd2, &sa.sa, SOCKADDR_LEN(sa)) >= 0);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-specifier.c b/src/test/test-specifier.c
new file mode 100644
index 0000000..d6a8b79
--- /dev/null
+++ b/src/test/test-specifier.c
@@ -0,0 +1,188 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-id128.h"
+
+#include "alloc-util.h"
+#include "log.h"
+#include "specifier.h"
+#include "stat-util.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "unit-file.h"
+
+static void test_specifier_escape_one(const char *a, const char *b) {
+ _cleanup_free_ char *x = NULL;
+
+ x = specifier_escape(a);
+ assert_se(streq_ptr(x, b));
+}
+
+TEST(specifier_escape) {
+ test_specifier_escape_one(NULL, NULL);
+ test_specifier_escape_one("", "");
+ test_specifier_escape_one("%", "%%");
+ test_specifier_escape_one("foo bar", "foo bar");
+ test_specifier_escape_one("foo%bar", "foo%%bar");
+ test_specifier_escape_one("%%%%%", "%%%%%%%%%%");
+}
+
+static void test_specifier_escape_strv_one(char **a, char **b) {
+ _cleanup_strv_free_ char **x = NULL;
+
+ assert_se(specifier_escape_strv(a, &x) >= 0);
+ assert_se(strv_equal(x, b));
+}
+
+TEST(specifier_escape_strv) {
+ test_specifier_escape_strv_one(NULL, NULL);
+ test_specifier_escape_strv_one(STRV_MAKE(NULL), STRV_MAKE(NULL));
+ test_specifier_escape_strv_one(STRV_MAKE(""), STRV_MAKE(""));
+ test_specifier_escape_strv_one(STRV_MAKE("foo"), STRV_MAKE("foo"));
+ test_specifier_escape_strv_one(STRV_MAKE("%"), STRV_MAKE("%%"));
+ test_specifier_escape_strv_one(STRV_MAKE("foo", "%", "foo%", "%foo", "foo%foo", "quux", "%%%"),
+ STRV_MAKE("foo", "%%", "foo%%", "%%foo", "foo%%foo", "quux", "%%%%%%"));
+}
+
+/* Any specifier functions which don't need an argument. */
+static const Specifier specifier_table[] = {
+ COMMON_SYSTEM_SPECIFIERS,
+
+ COMMON_CREDS_SPECIFIERS(RUNTIME_SCOPE_USER),
+ { 'h', specifier_user_home, NULL },
+
+ COMMON_TMP_SPECIFIERS,
+ {}
+};
+
+TEST(specifier_printf) {
+ static const Specifier table[] = {
+ { 'X', specifier_string, (char*) "AAAA" },
+ { 'Y', specifier_string, (char*) "BBBB" },
+ { 'e', specifier_string, NULL },
+ COMMON_SYSTEM_SPECIFIERS,
+ {}
+ };
+
+ _cleanup_free_ char *w = NULL;
+ int r;
+
+ r = specifier_printf("xxx a=%X b=%Y e=%e yyy", SIZE_MAX, table, NULL, NULL, &w);
+ assert_se(r >= 0);
+ assert_se(w);
+
+ puts(w);
+ assert_se(streq(w, "xxx a=AAAA b=BBBB e= yyy"));
+
+ free(w);
+ r = specifier_printf("boot=%b, host=%H, pretty=%q, version=%v, arch=%a, empty=%e", SIZE_MAX, table, NULL, NULL, &w);
+ assert_se(r >= 0);
+ assert_se(w);
+ puts(w);
+
+ w = mfree(w);
+ specifier_printf("os=%o, os-version=%w, build=%B, variant=%W, empty=%e%e%e", SIZE_MAX, table, NULL, NULL, &w);
+ if (w)
+ puts(w);
+}
+
+TEST(specifier_real_path) {
+ static const Specifier table[] = {
+ { 'p', specifier_string, "/dev/initctl" },
+ { 'y', specifier_real_path, "/dev/initctl" },
+ { 'Y', specifier_real_directory, "/dev/initctl" },
+ { 'w', specifier_real_path, "/dev/tty" },
+ { 'W', specifier_real_directory, "/dev/tty" },
+ {}
+ };
+
+ _cleanup_free_ char *w = NULL;
+ int r;
+
+ r = specifier_printf("p=%p y=%y Y=%Y w=%w W=%W", SIZE_MAX, table, NULL, NULL, &w);
+ assert_se(r >= 0 || r == -ENOENT);
+ assert_se(w || r == -ENOENT);
+ puts(strnull(w));
+
+ /* /dev/initctl should normally be a symlink to /run/initctl */
+ if (inode_same("/dev/initctl", "/run/initctl", 0) > 0)
+ assert_se(streq(w, "p=/dev/initctl y=/run/initctl Y=/run w=/dev/tty W=/dev"));
+}
+
+TEST(specifier_real_path_missing_file) {
+ static const Specifier table[] = {
+ { 'p', specifier_string, "/dev/-no-such-file--" },
+ { 'y', specifier_real_path, "/dev/-no-such-file--" },
+ { 'Y', specifier_real_directory, "/dev/-no-such-file--" },
+ {}
+ };
+
+ _cleanup_free_ char *w = NULL;
+ int r;
+
+ r = specifier_printf("p=%p y=%y", SIZE_MAX, table, NULL, NULL, &w);
+ assert_se(r == -ENOENT);
+
+ r = specifier_printf("p=%p Y=%Y", SIZE_MAX, table, NULL, NULL, &w);
+ assert_se(r == -ENOENT);
+}
+
+TEST(specifiers) {
+ int r;
+
+ for (const Specifier *s = specifier_table; s->specifier; s++) {
+ char spec[3];
+ _cleanup_free_ char *resolved = NULL;
+
+ xsprintf(spec, "%%%c", s->specifier);
+
+ r = specifier_printf(spec, SIZE_MAX, specifier_table, NULL, NULL, &resolved);
+ if (s->specifier == 'm' && IN_SET(r, -EUNATCH, -ENOMEDIUM, -ENOPKG)) /* machine-id might be missing in build chroots */
+ continue;
+ assert_se(r >= 0);
+
+ log_info("%%%c → %s", s->specifier, resolved);
+ }
+}
+
+/* Bunch of specifiers that are not part of the common lists */
+TEST(specifiers_assorted) {
+ const sd_id128_t id = SD_ID128_ALLF;
+ const uint64_t llu = UINT64_MAX;
+ const Specifier table[] = {
+ /* Used in src/partition/repart.c */
+ { 'a', specifier_uuid, &id },
+ { 'b', specifier_uint64, &llu },
+ {}
+ };
+
+ for (const Specifier *s = table; s->specifier; s++) {
+ char spec[3];
+ _cleanup_free_ char *resolved = NULL;
+ int r;
+
+ xsprintf(spec, "%%%c", s->specifier);
+
+ r = specifier_printf(spec, SIZE_MAX, table, NULL, NULL, &resolved);
+ assert_se(r >= 0);
+
+ log_info("%%%c → %s", s->specifier, resolved);
+ }
+}
+
+TEST(specifiers_missing_data_ok) {
+ _cleanup_free_ char *resolved = NULL;
+
+ assert_se(setenv("SYSTEMD_OS_RELEASE", "/dev/null", 1) == 0);
+ assert_se(specifier_printf("%A-%B-%M-%o-%w-%W", SIZE_MAX, specifier_table, NULL, NULL, &resolved) >= 0);
+ assert_se(streq(resolved, "-----"));
+
+ assert_se(setenv("SYSTEMD_OS_RELEASE", "/nosuchfileordirectory", 1) == 0);
+ assert_se(specifier_printf("%A-%B-%M-%o-%w-%W", SIZE_MAX, specifier_table, NULL, NULL, &resolved) == -EUNATCH);
+ assert_se(streq(resolved, "-----"));
+
+ assert_se(unsetenv("SYSTEMD_OS_RELEASE") == 0);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-stat-util.c b/src/test/test-stat-util.c
new file mode 100644
index 0000000..5aca207
--- /dev/null
+++ b/src/test/test-stat-util.c
@@ -0,0 +1,188 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <linux/magic.h>
+#include <sched.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "errno-list.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "macro.h"
+#include "mountpoint-util.h"
+#include "namespace-util.h"
+#include "path-util.h"
+#include "rm-rf.h"
+#include "stat-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+TEST(null_or_empty_path) {
+ assert_se(null_or_empty_path("/dev/null") == 1);
+ assert_se(null_or_empty_path("/dev/tty") == 1); /* We assume that any character device is "empty", bleh. */
+ assert_se(null_or_empty_path("../../../../../../../../../../../../../../../../../../../../dev/null") == 1);
+ assert_se(null_or_empty_path("/proc/self/exe") == 0);
+ assert_se(null_or_empty_path("/nosuchfileordir") == -ENOENT);
+}
+
+TEST(null_or_empty_path_with_root) {
+ assert_se(null_or_empty_path_with_root("/dev/null", NULL) == 1);
+ assert_se(null_or_empty_path_with_root("/dev/null", "/") == 1);
+ assert_se(null_or_empty_path_with_root("/dev/null", "/.././../") == 1);
+ assert_se(null_or_empty_path_with_root("/dev/null", "/.././..") == 1);
+ assert_se(null_or_empty_path_with_root("../../../../../../../../../../../../../../../../../../../../dev/null", NULL) == 1);
+ assert_se(null_or_empty_path_with_root("../../../../../../../../../../../../../../../../../../../../dev/null", "/") == 1);
+ assert_se(null_or_empty_path_with_root("/proc/self/exe", NULL) == 0);
+ assert_se(null_or_empty_path_with_root("/proc/self/exe", "/") == 0);
+ assert_se(null_or_empty_path_with_root("/nosuchfileordir", NULL) == -ENOENT);
+ assert_se(null_or_empty_path_with_root("/nosuchfileordir", "/.././../") == -ENOENT);
+ assert_se(null_or_empty_path_with_root("/nosuchfileordir", "/.././..") == -ENOENT);
+ assert_se(null_or_empty_path_with_root("/foobar/barbar/dev/null", "/foobar/barbar") == 1);
+ assert_se(null_or_empty_path_with_root("/foobar/barbar/dev/null", "/foobar/barbar/") == 1);
+}
+
+TEST(inode_same) {
+ _cleanup_close_ int fd = -EBADF;
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-files_same.XXXXXX";
+ _cleanup_(unlink_tempfilep) char name_alias[] = "/tmp/test-files_same.alias";
+
+ fd = mkostemp_safe(name);
+ assert_se(fd >= 0);
+ assert_se(symlink(name, name_alias) >= 0);
+
+ assert_se(inode_same(name, name, 0));
+ assert_se(inode_same(name, name, AT_SYMLINK_NOFOLLOW));
+ assert_se(inode_same(name, name_alias, 0));
+ assert_se(!inode_same(name, name_alias, AT_SYMLINK_NOFOLLOW));
+}
+
+TEST(is_symlink) {
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-is_symlink.XXXXXX";
+ _cleanup_(unlink_tempfilep) char name_link[] = "/tmp/test-is_symlink.link";
+ _cleanup_close_ int fd = -EBADF;
+
+ fd = mkostemp_safe(name);
+ assert_se(fd >= 0);
+ assert_se(symlink(name, name_link) >= 0);
+
+ assert_se(is_symlink(name) == 0);
+ assert_se(is_symlink(name_link) == 1);
+ assert_se(is_symlink("/a/file/which/does/not/exist/i/guess") < 0);
+}
+
+TEST(path_is_fs_type) {
+ /* run might not be a mount point in build chroots */
+ if (path_is_mount_point("/run", NULL, AT_SYMLINK_FOLLOW) > 0) {
+ assert_se(path_is_fs_type("/run", TMPFS_MAGIC) > 0);
+ assert_se(path_is_fs_type("/run", BTRFS_SUPER_MAGIC) == 0);
+ }
+ if (path_is_mount_point("/proc", NULL, AT_SYMLINK_FOLLOW) > 0) {
+ assert_se(path_is_fs_type("/proc", PROC_SUPER_MAGIC) > 0);
+ assert_se(path_is_fs_type("/proc", BTRFS_SUPER_MAGIC) == 0);
+ }
+ assert_se(path_is_fs_type("/i-dont-exist", BTRFS_SUPER_MAGIC) == -ENOENT);
+}
+
+TEST(path_is_temporary_fs) {
+ int r;
+
+ FOREACH_STRING(s, "/", "/run", "/sys", "/sys/", "/proc", "/i-dont-exist", "/var", "/var/lib") {
+ r = path_is_temporary_fs(s);
+
+ log_info_errno(r, "path_is_temporary_fs(\"%s\"): %d, %s",
+ s, r, r < 0 ? errno_to_name(r) : yes_no(r));
+ }
+
+ /* run might not be a mount point in build chroots */
+ if (path_is_mount_point("/run", NULL, AT_SYMLINK_FOLLOW) > 0)
+ assert_se(path_is_temporary_fs("/run") > 0);
+ assert_se(path_is_temporary_fs("/proc") == 0);
+ assert_se(path_is_temporary_fs("/i-dont-exist") == -ENOENT);
+}
+
+TEST(path_is_read_only_fs) {
+ int r;
+
+ FOREACH_STRING(s, "/", "/run", "/sys", "/sys/", "/proc", "/i-dont-exist", "/var", "/var/lib") {
+ r = path_is_read_only_fs(s);
+
+ log_info_errno(r, "path_is_read_only_fs(\"%s\"): %d, %s",
+ s, r, r < 0 ? errno_to_name(r) : yes_no(r));
+ }
+
+ if (path_is_mount_point("/sys", NULL, AT_SYMLINK_FOLLOW) > 0)
+ assert_se(IN_SET(path_is_read_only_fs("/sys"), 0, 1));
+
+ assert_se(path_is_read_only_fs("/proc") == 0);
+ assert_se(path_is_read_only_fs("/i-dont-exist") == -ENOENT);
+}
+
+TEST(fd_is_ns) {
+ _cleanup_close_ int fd = -EBADF;
+
+ assert_se(fd_is_ns(STDIN_FILENO, CLONE_NEWNET) == 0);
+ assert_se(fd_is_ns(STDERR_FILENO, CLONE_NEWNET) == 0);
+ assert_se(fd_is_ns(STDOUT_FILENO, CLONE_NEWNET) == 0);
+
+ fd = open("/proc/self/ns/mnt", O_CLOEXEC|O_RDONLY);
+ if (fd < 0) {
+ assert_se(errno == ENOENT);
+ log_notice("Path %s not found, skipping test", "/proc/self/ns/mnt");
+ return;
+ }
+ assert_se(fd >= 0);
+ assert_se(IN_SET(fd_is_ns(fd, CLONE_NEWNET), 0, -EUCLEAN));
+ fd = safe_close(fd);
+
+ assert_se((fd = open("/proc/self/ns/ipc", O_CLOEXEC|O_RDONLY)) >= 0);
+ assert_se(IN_SET(fd_is_ns(fd, CLONE_NEWIPC), 1, -EUCLEAN));
+ fd = safe_close(fd);
+
+ assert_se((fd = open("/proc/self/ns/net", O_CLOEXEC|O_RDONLY)) >= 0);
+ assert_se(IN_SET(fd_is_ns(fd, CLONE_NEWNET), 1, -EUCLEAN));
+}
+
+TEST(dir_is_empty) {
+ _cleanup_(rm_rf_physical_and_freep) char *empty_dir = NULL;
+ _cleanup_free_ char *j = NULL, *jj = NULL, *jjj = NULL;
+
+ assert_se(dir_is_empty_at(AT_FDCWD, "/proc", /* ignore_hidden_or_backup= */ true) == 0);
+ assert_se(dir_is_empty_at(AT_FDCWD, "/icertainlydontexistdoi", /* ignore_hidden_or_backup= */ true) == -ENOENT);
+
+ assert_se(mkdtemp_malloc("/tmp/emptyXXXXXX", &empty_dir) >= 0);
+ assert_se(dir_is_empty_at(AT_FDCWD, empty_dir, /* ignore_hidden_or_backup= */ true) > 0);
+
+ j = path_join(empty_dir, "zzz");
+ assert_se(j);
+ assert_se(touch(j) >= 0);
+
+ assert_se(dir_is_empty_at(AT_FDCWD, empty_dir, /* ignore_hidden_or_backup= */ true) == 0);
+
+ jj = path_join(empty_dir, "ppp");
+ assert_se(jj);
+ assert_se(touch(jj) >= 0);
+
+ jjj = path_join(empty_dir, ".qqq");
+ assert_se(jjj);
+ assert_se(touch(jjj) >= 0);
+
+ assert_se(dir_is_empty_at(AT_FDCWD, empty_dir, /* ignore_hidden_or_backup= */ true) == 0);
+ assert_se(dir_is_empty_at(AT_FDCWD, empty_dir, /* ignore_hidden_or_backup= */ false) == 0);
+ assert_se(unlink(j) >= 0);
+ assert_se(dir_is_empty_at(AT_FDCWD, empty_dir, /* ignore_hidden_or_backup= */ true) == 0);
+ assert_se(dir_is_empty_at(AT_FDCWD, empty_dir, /* ignore_hidden_or_backup= */ false) == 0);
+ assert_se(unlink(jj) >= 0);
+ assert_se(dir_is_empty_at(AT_FDCWD, empty_dir, /* ignore_hidden_or_backup= */ true) > 0);
+ assert_se(dir_is_empty_at(AT_FDCWD, empty_dir, /* ignore_hidden_or_backup= */ false) == 0);
+ assert_se(unlink(jjj) >= 0);
+ assert_se(dir_is_empty_at(AT_FDCWD, empty_dir, /* ignore_hidden_or_backup= */ true) > 0);
+ assert_se(dir_is_empty_at(AT_FDCWD, empty_dir, /* ignore_hidden_or_backup= */ false) > 0);
+}
+
+static int intro(void) {
+ log_show_color(true);
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_INFO, intro);
diff --git a/src/test/test-static-destruct.c b/src/test/test-static-destruct.c
new file mode 100644
index 0000000..ef8648f
--- /dev/null
+++ b/src/test/test-static-destruct.c
@@ -0,0 +1,67 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "static-destruct.h"
+#include "strv.h"
+#include "tests.h"
+
+static int foo = 0;
+static int bar = 0;
+static int baz = 0;
+static char *memory = NULL;
+static char **strings = NULL;
+static size_t n_strings = 0;
+static int *integers = NULL;
+static size_t n_integers = 0;
+
+static void test_destroy(int *b) {
+ (*b)++;
+}
+
+static void test_strings_destroy(char **array, size_t n) {
+ assert_se(n == 3);
+ assert_se(strv_equal(array, STRV_MAKE("a", "bbb", "ccc")));
+
+ strv_free(array);
+}
+
+static void test_integers_destroy(int *array, size_t n) {
+ assert_se(n == 10);
+
+ for (size_t i = 0; i < n; i++)
+ assert_se(array[i] == (int)(i * i));
+
+ free(array);
+}
+
+STATIC_DESTRUCTOR_REGISTER(foo, test_destroy);
+STATIC_DESTRUCTOR_REGISTER(bar, test_destroy);
+STATIC_DESTRUCTOR_REGISTER(bar, test_destroy);
+STATIC_DESTRUCTOR_REGISTER(baz, test_destroy);
+STATIC_DESTRUCTOR_REGISTER(baz, test_destroy);
+STATIC_DESTRUCTOR_REGISTER(baz, test_destroy);
+STATIC_DESTRUCTOR_REGISTER(memory, freep);
+STATIC_ARRAY_DESTRUCTOR_REGISTER(strings, n_strings, test_strings_destroy);
+STATIC_ARRAY_DESTRUCTOR_REGISTER(integers, n_integers, test_integers_destroy);
+
+TEST(static_destruct) {
+ assert_se(foo == 0 && bar == 0 && baz == 0);
+ assert_se(memory = strdup("hallo"));
+ assert_se(strings = strv_new("a", "bbb", "ccc"));
+ n_strings = strv_length(strings);
+ n_integers = 10;
+ assert_se(integers = new(int, n_integers));
+ for (size_t i = 0; i < n_integers; i++)
+ integers[i] = i * i;
+
+ static_destruct();
+
+ assert_se(foo == 1 && bar == 2 && baz == 3);
+ assert_se(!memory);
+ assert_se(!strings);
+ assert_se(n_strings == 0);
+ assert_se(!integers);
+ assert_se(n_integers == 0);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-strbuf.c b/src/test/test-strbuf.c
new file mode 100644
index 0000000..39a7142
--- /dev/null
+++ b/src/test/test-strbuf.c
@@ -0,0 +1,73 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdlib.h>
+
+#include "nulstr-util.h"
+#include "strbuf.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+
+static ssize_t add_string(struct strbuf *sb, const char *s) {
+ return strbuf_add_string(sb, s, strlen(s));
+}
+
+TEST(strbuf) {
+ _cleanup_(strbuf_freep) struct strbuf *sb = NULL;
+ _cleanup_strv_free_ char **l = NULL;
+ ssize_t a, b, c, d, e, f, g, h;
+
+ sb = strbuf_new();
+
+ a = add_string(sb, "waldo");
+ b = add_string(sb, "foo");
+ c = add_string(sb, "bar");
+ d = add_string(sb, "waldo"); /* duplicate */
+ e = add_string(sb, "aldo"); /* duplicate */
+ f = add_string(sb, "do"); /* duplicate */
+ g = add_string(sb, "waldorf"); /* not a duplicate: matches from tail */
+ h = add_string(sb, "");
+
+ /* check the content of the buffer directly */
+ l = strv_parse_nulstr(sb->buf, sb->len);
+ assert_se(l);
+
+ assert_se(streq(l[0], "")); /* root */
+ assert_se(streq(l[1], "waldo"));
+ assert_se(streq(l[2], "foo"));
+ assert_se(streq(l[3], "bar"));
+ assert_se(streq(l[4], "waldorf"));
+ assert_se(l[5] == NULL);
+
+ assert_se(sb->nodes_count == 5); /* root + 4 non-duplicates */
+ assert_se(sb->dedup_count == 4);
+ assert_se(sb->in_count == 8);
+
+ assert_se(sb->in_len == 29); /* length of all strings added */
+ assert_se(sb->dedup_len == 11); /* length of all strings duplicated */
+ assert_se(sb->len == 23); /* buffer length: in - dedup + \0 for each node */
+
+ /* check the returned offsets and the respective content in the buffer */
+ assert_se(a == 1);
+ assert_se(b == 7);
+ assert_se(c == 11);
+ assert_se(d == 1);
+ assert_se(e == 2);
+ assert_se(f == 4);
+ assert_se(g == 15);
+ assert_se(h == 0);
+
+ assert_se(streq(sb->buf + a, "waldo"));
+ assert_se(streq(sb->buf + b, "foo"));
+ assert_se(streq(sb->buf + c, "bar"));
+ assert_se(streq(sb->buf + d, "waldo"));
+ assert_se(streq(sb->buf + e, "aldo"));
+ assert_se(streq(sb->buf + f, "do"));
+ assert_se(streq(sb->buf + g, "waldorf"));
+ assert_se(streq(sb->buf + h, ""));
+
+ strbuf_complete(sb);
+ assert_se(sb->root == NULL);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-string-util.c b/src/test/test-string-util.c
new file mode 100644
index 0000000..a8fd45d
--- /dev/null
+++ b/src/test/test-string-util.c
@@ -0,0 +1,1327 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <ctype.h>
+
+#include "alloc-util.h"
+#include "locale-util.h"
+#include "macro.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "utf8.h"
+
+TEST(string_erase) {
+ char *x;
+ x = strdupa_safe("");
+ assert_se(streq(string_erase(x), ""));
+
+ x = strdupa_safe("1");
+ assert_se(streq(string_erase(x), ""));
+
+ x = strdupa_safe("123456789");
+ assert_se(streq(string_erase(x), ""));
+
+ assert_se(x[1] == '\0');
+ assert_se(x[2] == '\0');
+ assert_se(x[3] == '\0');
+ assert_se(x[4] == '\0');
+ assert_se(x[5] == '\0');
+ assert_se(x[6] == '\0');
+ assert_se(x[7] == '\0');
+ assert_se(x[8] == '\0');
+ assert_se(x[9] == '\0');
+}
+
+static void test_free_and_strndup_one(char **t, const char *src, size_t l, const char *expected, bool change) {
+ log_debug("%s: \"%s\", \"%s\", %zu (expect \"%s\", %s)",
+ __func__, strnull(*t), strnull(src), l, strnull(expected), yes_no(change));
+
+ int r = free_and_strndup(t, src, l);
+ assert_se(streq_ptr(*t, expected));
+ assert_se(r == change); /* check that change occurs only when necessary */
+}
+
+TEST(free_and_strndup) {
+ static const struct test_case {
+ const char *src;
+ size_t len;
+ const char *expected;
+ } cases[] = {
+ {"abc", 0, ""},
+ {"abc", 0, ""},
+ {"abc", 1, "a"},
+ {"abc", 2, "ab"},
+ {"abc", 3, "abc"},
+ {"abc", 4, "abc"},
+ {"abc", 5, "abc"},
+ {"abc", 5, "abc"},
+ {"abc", 4, "abc"},
+ {"abc", 3, "abc"},
+ {"abc", 2, "ab"},
+ {"abc", 1, "a"},
+ {"abc", 0, ""},
+
+ {"", 0, ""},
+ {"", 1, ""},
+ {"", 2, ""},
+ {"", 0, ""},
+ {"", 1, ""},
+ {"", 2, ""},
+ {"", 2, ""},
+ {"", 1, ""},
+ {"", 0, ""},
+
+ {NULL, 0, NULL},
+
+ {"foo", 3, "foo"},
+ {"foobar", 6, "foobar"},
+ };
+
+ _cleanup_free_ char *t = NULL;
+ const char *prev_expected = t;
+
+ for (unsigned i = 0; i < ELEMENTSOF(cases); i++) {
+ test_free_and_strndup_one(&t,
+ cases[i].src, cases[i].len, cases[i].expected,
+ !streq_ptr(cases[i].expected, prev_expected));
+ prev_expected = t;
+ }
+}
+
+TEST(ascii_strcasecmp_n) {
+ assert_se(ascii_strcasecmp_n("", "", 0) == 0);
+ assert_se(ascii_strcasecmp_n("", "", 1) == 0);
+ assert_se(ascii_strcasecmp_n("", "a", 1) < 0);
+ assert_se(ascii_strcasecmp_n("", "a", 2) < 0);
+ assert_se(ascii_strcasecmp_n("a", "", 1) > 0);
+ assert_se(ascii_strcasecmp_n("a", "", 2) > 0);
+ assert_se(ascii_strcasecmp_n("a", "a", 1) == 0);
+ assert_se(ascii_strcasecmp_n("a", "a", 2) == 0);
+ assert_se(ascii_strcasecmp_n("a", "b", 1) < 0);
+ assert_se(ascii_strcasecmp_n("a", "b", 2) < 0);
+ assert_se(ascii_strcasecmp_n("b", "a", 1) > 0);
+ assert_se(ascii_strcasecmp_n("b", "a", 2) > 0);
+ assert_se(ascii_strcasecmp_n("xxxxyxxxx", "xxxxYxxxx", 9) == 0);
+ assert_se(ascii_strcasecmp_n("xxxxxxxxx", "xxxxyxxxx", 9) < 0);
+ assert_se(ascii_strcasecmp_n("xxxxXxxxx", "xxxxyxxxx", 9) < 0);
+ assert_se(ascii_strcasecmp_n("xxxxxxxxx", "xxxxYxxxx", 9) < 0);
+ assert_se(ascii_strcasecmp_n("xxxxXxxxx", "xxxxYxxxx", 9) < 0);
+
+ assert_se(ascii_strcasecmp_n("xxxxYxxxx", "xxxxYxxxx", 9) == 0);
+ assert_se(ascii_strcasecmp_n("xxxxyxxxx", "xxxxxxxxx", 9) > 0);
+ assert_se(ascii_strcasecmp_n("xxxxyxxxx", "xxxxXxxxx", 9) > 0);
+ assert_se(ascii_strcasecmp_n("xxxxYxxxx", "xxxxxxxxx", 9) > 0);
+ assert_se(ascii_strcasecmp_n("xxxxYxxxx", "xxxxXxxxx", 9) > 0);
+}
+
+TEST(ascii_strcasecmp_nn) {
+ assert_se(ascii_strcasecmp_nn("", 0, "", 0) == 0);
+ assert_se(ascii_strcasecmp_nn("", 0, "", 1) < 0);
+ assert_se(ascii_strcasecmp_nn("", 1, "", 0) > 0);
+ assert_se(ascii_strcasecmp_nn("", 1, "", 1) == 0);
+
+ assert_se(ascii_strcasecmp_nn("aaaa", 4, "aaAa", 4) == 0);
+ assert_se(ascii_strcasecmp_nn("aaa", 3, "aaAa", 4) < 0);
+ assert_se(ascii_strcasecmp_nn("aaa", 4, "aaAa", 4) < 0);
+ assert_se(ascii_strcasecmp_nn("aaaa", 4, "aaA", 3) > 0);
+ assert_se(ascii_strcasecmp_nn("aaaa", 4, "AAA", 4) > 0);
+
+ assert_se(ascii_strcasecmp_nn("aaaa", 4, "bbbb", 4) < 0);
+ assert_se(ascii_strcasecmp_nn("aaAA", 4, "BBbb", 4) < 0);
+ assert_se(ascii_strcasecmp_nn("BBbb", 4, "aaaa", 4) > 0);
+}
+
+TEST(cellescape) {
+ char buf[40];
+
+ assert_se(streq(cellescape(buf, 1, ""), ""));
+ assert_se(streq(cellescape(buf, 1, "1"), ""));
+ assert_se(streq(cellescape(buf, 1, "12"), ""));
+
+ assert_se(streq(cellescape(buf, 2, ""), ""));
+ assert_se(streq(cellescape(buf, 2, "1"), "1"));
+ assert_se(streq(cellescape(buf, 2, "12"), "."));
+ assert_se(streq(cellescape(buf, 2, "123"), "."));
+
+ assert_se(streq(cellescape(buf, 3, ""), ""));
+ assert_se(streq(cellescape(buf, 3, "1"), "1"));
+ assert_se(streq(cellescape(buf, 3, "12"), "12"));
+ assert_se(streq(cellescape(buf, 3, "123"), ".."));
+ assert_se(streq(cellescape(buf, 3, "1234"), ".."));
+
+ assert_se(streq(cellescape(buf, 4, ""), ""));
+ assert_se(streq(cellescape(buf, 4, "1"), "1"));
+ assert_se(streq(cellescape(buf, 4, "12"), "12"));
+ assert_se(streq(cellescape(buf, 4, "123"), "123"));
+ assert_se(streq(cellescape(buf, 4, "1234"), is_locale_utf8() ? "…" : "..."));
+ assert_se(streq(cellescape(buf, 4, "12345"), is_locale_utf8() ? "…" : "..."));
+
+ assert_se(streq(cellescape(buf, 5, ""), ""));
+ assert_se(streq(cellescape(buf, 5, "1"), "1"));
+ assert_se(streq(cellescape(buf, 5, "12"), "12"));
+ assert_se(streq(cellescape(buf, 5, "123"), "123"));
+ assert_se(streq(cellescape(buf, 5, "1234"), "1234"));
+ assert_se(streq(cellescape(buf, 5, "12345"), is_locale_utf8() ? "1…" : "1..."));
+ assert_se(streq(cellescape(buf, 5, "123456"), is_locale_utf8() ? "1…" : "1..."));
+
+ assert_se(streq(cellescape(buf, 1, "\020"), ""));
+ assert_se(streq(cellescape(buf, 2, "\020"), "."));
+ assert_se(streq(cellescape(buf, 3, "\020"), ".."));
+ assert_se(streq(cellescape(buf, 4, "\020"), is_locale_utf8() ? "…" : "..."));
+ assert_se(streq(cellescape(buf, 5, "\020"), "\\020"));
+
+ assert_se(streq(cellescape(buf, 5, "1234\020"), is_locale_utf8() ? "1…" : "1..."));
+ assert_se(streq(cellescape(buf, 6, "1234\020"), is_locale_utf8() ? "12…" : "12..."));
+ assert_se(streq(cellescape(buf, 7, "1234\020"), is_locale_utf8() ? "123…" : "123..."));
+ assert_se(streq(cellescape(buf, 8, "1234\020"), is_locale_utf8() ? "1234…" : "1234..."));
+ assert_se(streq(cellescape(buf, 9, "1234\020"), "1234\\020"));
+
+ assert_se(streq(cellescape(buf, 1, "\t\n"), ""));
+ assert_se(streq(cellescape(buf, 2, "\t\n"), "."));
+ assert_se(streq(cellescape(buf, 3, "\t\n"), ".."));
+ assert_se(streq(cellescape(buf, 4, "\t\n"), is_locale_utf8() ? "…" : "..."));
+ assert_se(streq(cellescape(buf, 5, "\t\n"), "\\t\\n"));
+
+ assert_se(streq(cellescape(buf, 5, "1234\t\n"), is_locale_utf8() ? "1…" : "1..."));
+ assert_se(streq(cellescape(buf, 6, "1234\t\n"), is_locale_utf8() ? "12…" : "12..."));
+ assert_se(streq(cellescape(buf, 7, "1234\t\n"), is_locale_utf8() ? "123…" : "123..."));
+ assert_se(streq(cellescape(buf, 8, "1234\t\n"), is_locale_utf8() ? "1234…" : "1234..."));
+ assert_se(streq(cellescape(buf, 9, "1234\t\n"), "1234\\t\\n"));
+
+ assert_se(streq(cellescape(buf, 4, "x\t\020\n"), is_locale_utf8() ? "…" : "..."));
+ assert_se(streq(cellescape(buf, 5, "x\t\020\n"), is_locale_utf8() ? "x…" : "x..."));
+ assert_se(streq(cellescape(buf, 6, "x\t\020\n"), is_locale_utf8() ? "x…" : "x..."));
+ assert_se(streq(cellescape(buf, 7, "x\t\020\n"), is_locale_utf8() ? "x\\t…" : "x\\t..."));
+ assert_se(streq(cellescape(buf, 8, "x\t\020\n"), is_locale_utf8() ? "x\\t…" : "x\\t..."));
+ assert_se(streq(cellescape(buf, 9, "x\t\020\n"), is_locale_utf8() ? "x\\t…" : "x\\t..."));
+ assert_se(streq(cellescape(buf, 10, "x\t\020\n"), "x\\t\\020\\n"));
+
+ assert_se(streq(cellescape(buf, 6, "1\011"), "1\\t"));
+ assert_se(streq(cellescape(buf, 6, "1\020"), "1\\020"));
+ assert_se(streq(cellescape(buf, 6, "1\020x"), is_locale_utf8() ? "1…" : "1..."));
+
+ assert_se(streq(cellescape(buf, 40, "1\020"), "1\\020"));
+ assert_se(streq(cellescape(buf, 40, "1\020x"), "1\\020x"));
+
+ assert_se(streq(cellescape(buf, 40, "\a\b\f\n\r\t\v\\\"'"), "\\a\\b\\f\\n\\r\\t\\v\\\\\\\"\\'"));
+ assert_se(streq(cellescape(buf, 6, "\a\b\f\n\r\t\v\\\"'"), is_locale_utf8() ? "\\a…" : "\\a..."));
+ assert_se(streq(cellescape(buf, 7, "\a\b\f\n\r\t\v\\\"'"), is_locale_utf8() ? "\\a…" : "\\a..."));
+ assert_se(streq(cellescape(buf, 8, "\a\b\f\n\r\t\v\\\"'"), is_locale_utf8() ? "\\a\\b…" : "\\a\\b..."));
+
+ assert_se(streq(cellescape(buf, sizeof buf, "1\020"), "1\\020"));
+ assert_se(streq(cellescape(buf, sizeof buf, "1\020x"), "1\\020x"));
+}
+
+TEST(streq_ptr) {
+ assert_se(streq_ptr(NULL, NULL));
+ assert_se(!streq_ptr("abc", "cdef"));
+}
+
+TEST(strstrip) {
+ char *ret, input[] = " hello, waldo. ";
+
+ ret = strstrip(input);
+ assert_se(streq(ret, "hello, waldo."));
+}
+
+TEST(strextend) {
+ _cleanup_free_ char *str = NULL;
+
+ assert_se(strextend(&str, NULL));
+ assert_se(streq_ptr(str, ""));
+ assert_se(strextend(&str, "", "0", "", "", "123"));
+ assert_se(streq_ptr(str, "0123"));
+ assert_se(strextend(&str, "456", "78", "9"));
+ assert_se(streq_ptr(str, "0123456789"));
+}
+
+TEST(strextend_with_separator) {
+ _cleanup_free_ char *str = NULL;
+
+ assert_se(strextend_with_separator(&str, NULL, NULL));
+ assert_se(streq_ptr(str, ""));
+ str = mfree(str);
+
+ assert_se(strextend_with_separator(&str, "...", NULL));
+ assert_se(streq_ptr(str, ""));
+ assert_se(strextend_with_separator(&str, "...", NULL));
+ assert_se(streq_ptr(str, ""));
+ str = mfree(str);
+
+ assert_se(strextend_with_separator(&str, "xyz", "a", "bb", "ccc"));
+ assert_se(streq_ptr(str, "axyzbbxyzccc"));
+ str = mfree(str);
+
+ assert_se(strextend_with_separator(&str, ",", "start", "", "1", "234"));
+ assert_se(streq_ptr(str, "start,,1,234"));
+ assert_se(strextend_with_separator(&str, ";", "more", "5", "678"));
+ assert_se(streq_ptr(str, "start,,1,234;more;5;678"));
+}
+
+TEST(strrep) {
+ _cleanup_free_ char *one = NULL, *three = NULL, *zero = NULL;
+ char *onea, *threea;
+
+ one = strrep("waldo", 1);
+ three = strrep("waldo", 3);
+ zero = strrep("waldo", 0);
+
+ assert_se(streq(one, "waldo"));
+ assert_se(streq(three, "waldowaldowaldo"));
+ assert_se(streq(zero, ""));
+
+ onea = strrepa("waldo", 1);
+ threea = strrepa("waldo", 3);
+
+ assert_se(streq(onea, "waldo"));
+ assert_se(streq(threea, "waldowaldowaldo"));
+}
+
+TEST(string_has_cc) {
+ assert_se(string_has_cc("abc\1", NULL));
+ assert_se(string_has_cc("abc\x7f", NULL));
+ assert_se(string_has_cc("abc\x7f", NULL));
+ assert_se(string_has_cc("abc\t\x7f", "\t"));
+ assert_se(string_has_cc("abc\t\x7f", "\t"));
+ assert_se(string_has_cc("\x7f", "\t"));
+ assert_se(string_has_cc("\x7f", "\t\a"));
+
+ assert_se(!string_has_cc("abc\t\t", "\t"));
+ assert_se(!string_has_cc("abc\t\t\a", "\t\a"));
+ assert_se(!string_has_cc("a\ab\tc", "\t\a"));
+}
+
+TEST(ascii_strlower) {
+ char a[] = "AabBcC Jk Ii Od LKJJJ kkd LK";
+ assert_se(streq(ascii_strlower(a), "aabbcc jk ii od lkjjj kkd lk"));
+}
+
+TEST(strshorten) {
+ char s[] = "foobar";
+
+ assert_se(strlen(strshorten(s, 6)) == 6);
+ assert_se(strlen(strshorten(s, 12)) == 6);
+ assert_se(strlen(strshorten(s, 2)) == 2);
+ assert_se(strlen(strshorten(s, 0)) == 0);
+}
+
+TEST(strjoina) {
+ char *actual;
+
+ actual = strjoina("", "foo", "bar");
+ assert_se(streq(actual, "foobar"));
+
+ actual = strjoina("foo", "bar", "baz");
+ assert_se(streq(actual, "foobarbaz"));
+
+ actual = strjoina("foo", "", "bar", "baz");
+ assert_se(streq(actual, "foobarbaz"));
+
+ actual = strjoina("foo");
+ assert_se(streq(actual, "foo"));
+
+ actual = strjoina(NULL);
+ assert_se(streq(actual, ""));
+
+ actual = strjoina(NULL, "foo");
+ assert_se(streq(actual, ""));
+
+ actual = strjoina("foo", NULL, "bar");
+ assert_se(streq(actual, "foo"));
+
+ actual = strjoina("/sys/fs/cgroup/", "dn", "/a/b/c", "/cgroup.procs");
+ assert_se(streq(actual, "/sys/fs/cgroup/dn/a/b/c/cgroup.procs"));
+
+ actual = strjoina("/sys/fs/cgroup/", "dn", NULL, NULL);
+ assert_se(streq(actual, "/sys/fs/cgroup/dn"));
+}
+
+TEST(strjoin) {
+ char *actual;
+
+ actual = strjoin("", "foo", "bar");
+ assert_se(streq(actual, "foobar"));
+ mfree(actual);
+
+ actual = strjoin("foo", "bar", "baz");
+ assert_se(streq(actual, "foobarbaz"));
+ mfree(actual);
+
+ actual = strjoin("foo", "", "bar", "baz");
+ assert_se(streq(actual, "foobarbaz"));
+ mfree(actual);
+
+ actual = strjoin("foo", NULL);
+ assert_se(streq(actual, "foo"));
+ mfree(actual);
+
+ actual = strjoin(NULL, NULL);
+ assert_se(streq(actual, ""));
+ mfree(actual);
+
+ actual = strjoin(NULL, "foo");
+ assert_se(streq(actual, ""));
+ mfree(actual);
+
+ actual = strjoin("foo", NULL, "bar");
+ assert_se(streq(actual, "foo"));
+ mfree(actual);
+}
+
+TEST(strcmp_ptr) {
+ assert_se(strcmp_ptr(NULL, NULL) == 0);
+ assert_se(strcmp_ptr("", NULL) > 0);
+ assert_se(strcmp_ptr("foo", NULL) > 0);
+ assert_se(strcmp_ptr(NULL, "") < 0);
+ assert_se(strcmp_ptr(NULL, "bar") < 0);
+ assert_se(strcmp_ptr("foo", "bar") > 0);
+ assert_se(strcmp_ptr("bar", "baz") < 0);
+ assert_se(strcmp_ptr("foo", "foo") == 0);
+ assert_se(strcmp_ptr("", "") == 0);
+}
+
+TEST(foreach_word) {
+ const char *test = "test abc d\te f ";
+ const char * const expected[] = {
+ "test",
+ "abc",
+ "d",
+ "e",
+ "f",
+ };
+
+ size_t i = 0;
+ int r;
+ for (const char *p = test;;) {
+ _cleanup_free_ char *word = NULL;
+
+ r = extract_first_word(&p, &word, NULL, 0);
+ if (r == 0) {
+ assert_se(i == ELEMENTSOF(expected));
+ break;
+ }
+ assert_se(r > 0);
+
+ assert_se(streq(expected[i++], word));
+ }
+}
+
+static void check(const char *test, char** expected, bool trailing) {
+ size_t i = 0;
+ int r;
+
+ printf("<<<%s>>>\n", test);
+ for (;;) {
+ _cleanup_free_ char *word = NULL;
+
+ r = extract_first_word(&test, &word, NULL, EXTRACT_UNQUOTE);
+ if (r == 0) {
+ assert_se(!trailing);
+ break;
+ } else if (r < 0) {
+ assert_se(trailing);
+ break;
+ }
+
+ assert_se(streq(word, expected[i++]));
+ printf("<%s>\n", word);
+ }
+ assert_se(expected[i] == NULL);
+}
+
+TEST(foreach_word_quoted) {
+ check("test a b c 'd' e '' '' hhh '' '' \"a b c\"",
+ STRV_MAKE("test",
+ "a",
+ "b",
+ "c",
+ "d",
+ "e",
+ "",
+ "",
+ "hhh",
+ "",
+ "",
+ "a b c"),
+ false);
+
+ check("test \"xxx",
+ STRV_MAKE("test"),
+ true);
+
+ check("test\\",
+ STRV_MAKE_EMPTY,
+ true);
+}
+
+TEST(endswith) {
+ assert_se(endswith("foobar", "bar"));
+ assert_se(endswith("foobar", ""));
+ assert_se(endswith("foobar", "foobar"));
+ assert_se(endswith("", ""));
+
+ assert_se(!endswith("foobar", "foo"));
+ assert_se(!endswith("foobar", "foobarfoofoo"));
+}
+
+TEST(endswith_no_case) {
+ assert_se(endswith_no_case("fooBAR", "bar"));
+ assert_se(endswith_no_case("foobar", ""));
+ assert_se(endswith_no_case("foobar", "FOOBAR"));
+ assert_se(endswith_no_case("", ""));
+
+ assert_se(!endswith_no_case("foobar", "FOO"));
+ assert_se(!endswith_no_case("foobar", "FOOBARFOOFOO"));
+}
+
+TEST(delete_chars) {
+ char *s, input[] = " hello, waldo. abc";
+
+ s = delete_chars(input, WHITESPACE);
+ assert_se(streq(s, "hello,waldo.abc"));
+ assert_se(s == input);
+}
+
+TEST(delete_trailing_chars) {
+ char *s,
+ input1[] = " \n \r k \n \r ",
+ input2[] = "kkkkthiskkkiskkkaktestkkk",
+ input3[] = "abcdef";
+
+ s = delete_trailing_chars(input1, WHITESPACE);
+ assert_se(streq(s, " \n \r k"));
+ assert_se(s == input1);
+
+ s = delete_trailing_chars(input2, "kt");
+ assert_se(streq(s, "kkkkthiskkkiskkkaktes"));
+ assert_se(s == input2);
+
+ s = delete_trailing_chars(input3, WHITESPACE);
+ assert_se(streq(s, "abcdef"));
+ assert_se(s == input3);
+
+ s = delete_trailing_chars(input3, "fe");
+ assert_se(streq(s, "abcd"));
+ assert_se(s == input3);
+}
+
+TEST(delete_trailing_slashes) {
+ char s1[] = "foobar//",
+ s2[] = "foobar/",
+ s3[] = "foobar",
+ s4[] = "";
+
+ assert_se(streq(delete_trailing_chars(s1, "_"), "foobar//"));
+ assert_se(streq(delete_trailing_chars(s1, "/"), "foobar"));
+ assert_se(streq(delete_trailing_chars(s2, "/"), "foobar"));
+ assert_se(streq(delete_trailing_chars(s3, "/"), "foobar"));
+ assert_se(streq(delete_trailing_chars(s4, "/"), ""));
+}
+
+TEST(skip_leading_chars) {
+ char input1[] = " \n \r k \n \r ",
+ input2[] = "kkkkthiskkkiskkkaktestkkk",
+ input3[] = "abcdef";
+
+ assert_se(streq(skip_leading_chars(input1, WHITESPACE), "k \n \r "));
+ assert_se(streq(skip_leading_chars(input2, "k"), "thiskkkiskkkaktestkkk"));
+ assert_se(streq(skip_leading_chars(input2, "tk"), "hiskkkiskkkaktestkkk"));
+ assert_se(streq(skip_leading_chars(input3, WHITESPACE), "abcdef"));
+ assert_se(streq(skip_leading_chars(input3, "bcaef"), "def"));
+}
+
+TEST(in_charset) {
+ assert_se(in_charset("dddaaabbbcccc", "abcd"));
+ assert_se(!in_charset("dddaaabbbcccc", "abc f"));
+}
+
+TEST(split_pair) {
+ _cleanup_free_ char *a = NULL, *b = NULL;
+
+ assert_se(split_pair("", "", &a, &b) == -EINVAL);
+ assert_se(split_pair("foo=bar", "", &a, &b) == -EINVAL);
+ assert_se(split_pair("", "=", &a, &b) == -EINVAL);
+ assert_se(split_pair("foo=bar", "=", &a, &b) >= 0);
+ assert_se(streq(a, "foo"));
+ assert_se(streq(b, "bar"));
+ free(a);
+ free(b);
+ assert_se(split_pair("==", "==", &a, &b) >= 0);
+ assert_se(streq(a, ""));
+ assert_se(streq(b, ""));
+ free(a);
+ free(b);
+
+ assert_se(split_pair("===", "==", &a, &b) >= 0);
+ assert_se(streq(a, ""));
+ assert_se(streq(b, "="));
+}
+
+TEST(first_word) {
+ assert_se(first_word("Hello", ""));
+ assert_se(first_word("Hello", "Hello"));
+ assert_se(first_word("Hello world", "Hello"));
+ assert_se(first_word("Hello\tworld", "Hello"));
+ assert_se(first_word("Hello\nworld", "Hello"));
+ assert_se(first_word("Hello\rworld", "Hello"));
+ assert_se(first_word("Hello ", "Hello"));
+
+ assert_se(!first_word("Hello", "Hellooo"));
+ assert_se(!first_word("Hello", "xxxxx"));
+ assert_se(!first_word("Hellooo", "Hello"));
+}
+
+TEST(strlen_ptr) {
+ assert_se(strlen_ptr("foo") == 3);
+ assert_se(strlen_ptr("") == 0);
+ assert_se(strlen_ptr(NULL) == 0);
+}
+
+TEST(memory_startswith) {
+ assert_se(streq(memory_startswith("", 0, ""), ""));
+ assert_se(streq(memory_startswith("", 1, ""), ""));
+ assert_se(streq(memory_startswith("x", 2, ""), "x"));
+ assert_se(!memory_startswith("", 1, "x"));
+ assert_se(!memory_startswith("", 1, "xxxxxxxx"));
+ assert_se(streq(memory_startswith("xxx", 4, "x"), "xx"));
+ assert_se(streq(memory_startswith("xxx", 4, "xx"), "x"));
+ assert_se(streq(memory_startswith("xxx", 4, "xxx"), ""));
+ assert_se(!memory_startswith("xxx", 4, "xxxx"));
+}
+
+TEST(memory_startswith_no_case) {
+ assert_se(streq(memory_startswith_no_case("", 0, ""), ""));
+ assert_se(streq(memory_startswith_no_case("", 1, ""), ""));
+ assert_se(streq(memory_startswith_no_case("x", 2, ""), "x"));
+ assert_se(streq(memory_startswith_no_case("X", 2, ""), "X"));
+ assert_se(!memory_startswith_no_case("", 1, "X"));
+ assert_se(!memory_startswith_no_case("", 1, "xxxxXXXX"));
+ assert_se(streq(memory_startswith_no_case("xxx", 4, "X"), "xx"));
+ assert_se(streq(memory_startswith_no_case("XXX", 4, "x"), "XX"));
+ assert_se(streq(memory_startswith_no_case("XXX", 4, "X"), "XX"));
+ assert_se(streq(memory_startswith_no_case("xxx", 4, "XX"), "x"));
+ assert_se(streq(memory_startswith_no_case("XXX", 4, "xx"), "X"));
+ assert_se(streq(memory_startswith_no_case("XXX", 4, "XX"), "X"));
+ assert_se(streq(memory_startswith_no_case("xxx", 4, "XXX"), ""));
+ assert_se(streq(memory_startswith_no_case("XXX", 4, "xxx"), ""));
+ assert_se(streq(memory_startswith_no_case("XXX", 4, "XXX"), ""));
+
+ assert_se(memory_startswith_no_case((char[2]){'x', 'x'}, 2, "xx"));
+ assert_se(memory_startswith_no_case((char[2]){'x', 'X'}, 2, "xX"));
+ assert_se(memory_startswith_no_case((char[2]){'X', 'x'}, 2, "Xx"));
+ assert_se(memory_startswith_no_case((char[2]){'X', 'X'}, 2, "XX"));
+}
+
+static void test_string_truncate_lines_one(const char *input, size_t n_lines, const char *output, bool truncation) {
+ _cleanup_free_ char *b = NULL;
+ int k;
+
+ assert_se((k = string_truncate_lines(input, n_lines, &b)) >= 0);
+ assert_se(streq(b, output));
+ assert_se(!!k == truncation);
+}
+
+TEST(string_truncate_lines) {
+ test_string_truncate_lines_one("", 0, "", false);
+ test_string_truncate_lines_one("", 1, "", false);
+ test_string_truncate_lines_one("", 2, "", false);
+ test_string_truncate_lines_one("", 3, "", false);
+
+ test_string_truncate_lines_one("x", 0, "", true);
+ test_string_truncate_lines_one("x", 1, "x", false);
+ test_string_truncate_lines_one("x", 2, "x", false);
+ test_string_truncate_lines_one("x", 3, "x", false);
+
+ test_string_truncate_lines_one("x\n", 0, "", true);
+ test_string_truncate_lines_one("x\n", 1, "x", false);
+ test_string_truncate_lines_one("x\n", 2, "x", false);
+ test_string_truncate_lines_one("x\n", 3, "x", false);
+
+ test_string_truncate_lines_one("x\ny", 0, "", true);
+ test_string_truncate_lines_one("x\ny", 1, "x", true);
+ test_string_truncate_lines_one("x\ny", 2, "x\ny", false);
+ test_string_truncate_lines_one("x\ny", 3, "x\ny", false);
+
+ test_string_truncate_lines_one("x\ny\n", 0, "", true);
+ test_string_truncate_lines_one("x\ny\n", 1, "x", true);
+ test_string_truncate_lines_one("x\ny\n", 2, "x\ny", false);
+ test_string_truncate_lines_one("x\ny\n", 3, "x\ny", false);
+
+ test_string_truncate_lines_one("x\ny\nz", 0, "", true);
+ test_string_truncate_lines_one("x\ny\nz", 1, "x", true);
+ test_string_truncate_lines_one("x\ny\nz", 2, "x\ny", true);
+ test_string_truncate_lines_one("x\ny\nz", 3, "x\ny\nz", false);
+
+ test_string_truncate_lines_one("x\ny\nz\n", 0, "", true);
+ test_string_truncate_lines_one("x\ny\nz\n", 1, "x", true);
+ test_string_truncate_lines_one("x\ny\nz\n", 2, "x\ny", true);
+ test_string_truncate_lines_one("x\ny\nz\n", 3, "x\ny\nz", false);
+
+ test_string_truncate_lines_one("\n", 0, "", false);
+ test_string_truncate_lines_one("\n", 1, "", false);
+ test_string_truncate_lines_one("\n", 2, "", false);
+ test_string_truncate_lines_one("\n", 3, "", false);
+
+ test_string_truncate_lines_one("\n\n", 0, "", false);
+ test_string_truncate_lines_one("\n\n", 1, "", false);
+ test_string_truncate_lines_one("\n\n", 2, "", false);
+ test_string_truncate_lines_one("\n\n", 3, "", false);
+
+ test_string_truncate_lines_one("\n\n\n", 0, "", false);
+ test_string_truncate_lines_one("\n\n\n", 1, "", false);
+ test_string_truncate_lines_one("\n\n\n", 2, "", false);
+ test_string_truncate_lines_one("\n\n\n", 3, "", false);
+
+ test_string_truncate_lines_one("\nx\n\n", 0, "", true);
+ test_string_truncate_lines_one("\nx\n\n", 1, "", true);
+ test_string_truncate_lines_one("\nx\n\n", 2, "\nx", false);
+ test_string_truncate_lines_one("\nx\n\n", 3, "\nx", false);
+
+ test_string_truncate_lines_one("\n\nx\n", 0, "", true);
+ test_string_truncate_lines_one("\n\nx\n", 1, "", true);
+ test_string_truncate_lines_one("\n\nx\n", 2, "", true);
+ test_string_truncate_lines_one("\n\nx\n", 3, "\n\nx", false);
+}
+
+static void test_string_extract_lines_one(const char *input, size_t i, const char *output, bool more) {
+ _cleanup_free_ char *b = NULL;
+ int k;
+
+ assert_se((k = string_extract_line(input, i, &b)) >= 0);
+ assert_se(streq(b ?: input, output));
+ assert_se(!!k == more);
+}
+
+TEST(string_extract_line) {
+ test_string_extract_lines_one("", 0, "", false);
+ test_string_extract_lines_one("", 1, "", false);
+ test_string_extract_lines_one("", 2, "", false);
+ test_string_extract_lines_one("", 3, "", false);
+
+ test_string_extract_lines_one("x", 0, "x", false);
+ test_string_extract_lines_one("x", 1, "", false);
+ test_string_extract_lines_one("x", 2, "", false);
+ test_string_extract_lines_one("x", 3, "", false);
+
+ test_string_extract_lines_one("x\n", 0, "x", false);
+ test_string_extract_lines_one("x\n", 1, "", false);
+ test_string_extract_lines_one("x\n", 2, "", false);
+ test_string_extract_lines_one("x\n", 3, "", false);
+
+ test_string_extract_lines_one("x\ny", 0, "x", true);
+ test_string_extract_lines_one("x\ny", 1, "y", false);
+ test_string_extract_lines_one("x\ny", 2, "", false);
+ test_string_extract_lines_one("x\ny", 3, "", false);
+
+ test_string_extract_lines_one("x\ny\n", 0, "x", true);
+ test_string_extract_lines_one("x\ny\n", 1, "y", false);
+ test_string_extract_lines_one("x\ny\n", 2, "", false);
+ test_string_extract_lines_one("x\ny\n", 3, "", false);
+
+ test_string_extract_lines_one("x\ny\nz", 0, "x", true);
+ test_string_extract_lines_one("x\ny\nz", 1, "y", true);
+ test_string_extract_lines_one("x\ny\nz", 2, "z", false);
+ test_string_extract_lines_one("x\ny\nz", 3, "", false);
+
+ test_string_extract_lines_one("\n", 0, "", false);
+ test_string_extract_lines_one("\n", 1, "", false);
+ test_string_extract_lines_one("\n", 2, "", false);
+ test_string_extract_lines_one("\n", 3, "", false);
+
+ test_string_extract_lines_one("\n\n", 0, "", true);
+ test_string_extract_lines_one("\n\n", 1, "", false);
+ test_string_extract_lines_one("\n\n", 2, "", false);
+ test_string_extract_lines_one("\n\n", 3, "", false);
+
+ test_string_extract_lines_one("\n\n\n", 0, "", true);
+ test_string_extract_lines_one("\n\n\n", 1, "", true);
+ test_string_extract_lines_one("\n\n\n", 2, "", false);
+ test_string_extract_lines_one("\n\n\n", 3, "", false);
+
+ test_string_extract_lines_one("\n\n\n\n", 0, "", true);
+ test_string_extract_lines_one("\n\n\n\n", 1, "", true);
+ test_string_extract_lines_one("\n\n\n\n", 2, "", true);
+ test_string_extract_lines_one("\n\n\n\n", 3, "", false);
+
+ test_string_extract_lines_one("\nx\n\n\n", 0, "", true);
+ test_string_extract_lines_one("\nx\n\n\n", 1, "x", true);
+ test_string_extract_lines_one("\nx\n\n\n", 2, "", true);
+ test_string_extract_lines_one("\nx\n\n\n", 3, "", false);
+
+ test_string_extract_lines_one("\n\nx\n\n", 0, "", true);
+ test_string_extract_lines_one("\n\nx\n\n", 1, "", true);
+ test_string_extract_lines_one("\n\nx\n\n", 2, "x", true);
+ test_string_extract_lines_one("\n\nx\n\n", 3, "", false);
+
+ test_string_extract_lines_one("\n\n\nx\n", 0, "", true);
+ test_string_extract_lines_one("\n\n\nx\n", 1, "", true);
+ test_string_extract_lines_one("\n\n\nx\n", 2, "", true);
+ test_string_extract_lines_one("\n\n\nx\n", 3, "x", false);
+}
+
+TEST(string_contains_word_strv) {
+ const char *w;
+
+ assert_se(string_contains_word_strv("a b cc", NULL, STRV_MAKE("a", "b"), NULL));
+
+ assert_se(string_contains_word_strv("a b cc", NULL, STRV_MAKE("a", "b"), &w));
+ assert_se(streq(w, "a"));
+
+ assert_se(!string_contains_word_strv("a b cc", NULL, STRV_MAKE("d"), &w));
+ assert_se(w == NULL);
+
+ assert_se(string_contains_word_strv("a b cc", NULL, STRV_MAKE("b", "a"), &w));
+ assert_se(streq(w, "a"));
+
+ assert_se(string_contains_word_strv("b a b cc", NULL, STRV_MAKE("b", "a", "b"), &w));
+ assert_se(streq(w, "b"));
+
+ assert_se(string_contains_word_strv("a b cc", NULL, STRV_MAKE("b", ""), &w));
+ assert_se(streq(w, "b"));
+
+ assert_se(!string_contains_word_strv("a b cc", NULL, STRV_MAKE(""), &w));
+ assert_se(w == NULL);
+
+ assert_se(string_contains_word_strv("a b cc", " ", STRV_MAKE(""), &w));
+ assert_se(streq(w, ""));
+}
+
+TEST(string_contains_word) {
+ assert_se( string_contains_word("a b cc", NULL, "a"));
+ assert_se( string_contains_word("a b cc", NULL, "b"));
+ assert_se(!string_contains_word("a b cc", NULL, "c"));
+ assert_se( string_contains_word("a b cc", NULL, "cc"));
+ assert_se(!string_contains_word("a b cc", NULL, "d"));
+ assert_se(!string_contains_word("a b cc", NULL, "a b"));
+ assert_se(!string_contains_word("a b cc", NULL, "a b c"));
+ assert_se(!string_contains_word("a b cc", NULL, "b c"));
+ assert_se(!string_contains_word("a b cc", NULL, "b cc"));
+ assert_se(!string_contains_word("a b cc", NULL, "a "));
+ assert_se(!string_contains_word("a b cc", NULL, " b "));
+ assert_se(!string_contains_word("a b cc", NULL, " cc"));
+
+ assert_se( string_contains_word(" a b\t\tcc", NULL, "a"));
+ assert_se( string_contains_word(" a b\t\tcc", NULL, "b"));
+ assert_se(!string_contains_word(" a b\t\tcc", NULL, "c"));
+ assert_se( string_contains_word(" a b\t\tcc", NULL, "cc"));
+ assert_se(!string_contains_word(" a b\t\tcc", NULL, "d"));
+ assert_se(!string_contains_word(" a b\t\tcc", NULL, "a b"));
+ assert_se(!string_contains_word(" a b\t\tcc", NULL, "a b\t\tc"));
+ assert_se(!string_contains_word(" a b\t\tcc", NULL, "b\t\tc"));
+ assert_se(!string_contains_word(" a b\t\tcc", NULL, "b\t\tcc"));
+ assert_se(!string_contains_word(" a b\t\tcc", NULL, "a "));
+ assert_se(!string_contains_word(" a b\t\tcc", NULL, " b "));
+ assert_se(!string_contains_word(" a b\t\tcc", NULL, " cc"));
+
+ assert_se(!string_contains_word(" a b\t\tcc", NULL, ""));
+ assert_se(!string_contains_word(" a b\t\tcc", NULL, " "));
+ assert_se(!string_contains_word(" a b\t\tcc", NULL, " "));
+ assert_se( string_contains_word(" a b\t\tcc", " ", ""));
+ assert_se( string_contains_word(" a b\t\tcc", "\t", ""));
+ assert_se( string_contains_word(" a b\t\tcc", WHITESPACE, ""));
+
+ assert_se( string_contains_word("a:b:cc", ":#", "a"));
+ assert_se( string_contains_word("a:b:cc", ":#", "b"));
+ assert_se(!string_contains_word("a:b:cc", ":#", "c"));
+ assert_se( string_contains_word("a:b:cc", ":#", "cc"));
+ assert_se(!string_contains_word("a:b:cc", ":#", "d"));
+ assert_se(!string_contains_word("a:b:cc", ":#", "a:b"));
+ assert_se(!string_contains_word("a:b:cc", ":#", "a:b:c"));
+ assert_se(!string_contains_word("a:b:cc", ":#", "b:c"));
+ assert_se(!string_contains_word("a#b#cc", ":#", "b:cc"));
+ assert_se( string_contains_word("a#b#cc", ":#", "b"));
+ assert_se( string_contains_word("a#b#cc", ":#", "cc"));
+ assert_se(!string_contains_word("a:b:cc", ":#", "a:"));
+ assert_se(!string_contains_word("a:b cc", ":#", "b"));
+ assert_se( string_contains_word("a:b cc", ":#", "b cc"));
+ assert_se(!string_contains_word("a:b:cc", ":#", ":cc"));
+}
+
+static void test_strverscmp_improved_one(const char* a, const char *b, int expected) {
+ int r = strverscmp_improved(a, b);
+
+ log_info("'%s' %s '%s'%s",
+ strnull(a),
+ comparison_operator(r),
+ strnull(b),
+ r == expected ? "" : " !!!!!!!!!!!!!");
+ assert_se(r == expected);
+}
+
+static void test_strverscmp_improved_newer(const char *older, const char *newer) {
+ test_strverscmp_improved_one(older, newer, -1);
+
+ assert_se(strverscmp_improved(older, older) == 0);
+ assert_se(strverscmp_improved(older, newer) < 0);
+ assert_se(strverscmp_improved(newer, older) > 0);
+ assert_se(strverscmp_improved(newer, newer) == 0);
+}
+
+TEST(strverscmp_improved) {
+ static const char * const versions[] = {
+ "~1",
+ "",
+ "ab",
+ "abb",
+ "abc",
+ "0001",
+ "002",
+ "12",
+ "122",
+ "122.9",
+ "123~rc1",
+ "123",
+ "123-a",
+ "123-a.1",
+ "123-a1",
+ "123-a1.1",
+ "123-3",
+ "123-3.1",
+ "123^patch1",
+ "123^1",
+ "123.a-1",
+ "123.1-1",
+ "123a-1",
+ "124",
+ NULL,
+ };
+
+ STRV_FOREACH(p, versions)
+ STRV_FOREACH(q, p + 1)
+ test_strverscmp_improved_newer(*p, *q);
+
+ test_strverscmp_improved_newer("123.45-67.88", "123.45-67.89");
+ test_strverscmp_improved_newer("123.45-67.89", "123.45-67.89a");
+ test_strverscmp_improved_newer("123.45-67.ab", "123.45-67.89");
+ test_strverscmp_improved_newer("123.45-67.9", "123.45-67.89");
+ test_strverscmp_improved_newer("123.45-67", "123.45-67.89");
+ test_strverscmp_improved_newer("123.45-66.89", "123.45-67.89");
+ test_strverscmp_improved_newer("123.45-9.99", "123.45-67.89");
+ test_strverscmp_improved_newer("123.42-99.99", "123.45-67.89");
+ test_strverscmp_improved_newer("123-99.99", "123.45-67.89");
+
+ /* '~' : pre-releases */
+ test_strverscmp_improved_newer("123~rc1-99.99", "123.45-67.89");
+ test_strverscmp_improved_newer("123~rc1-99.99", "123-45.67.89");
+ test_strverscmp_improved_newer("123~rc1-99.99", "123~rc2-67.89");
+ test_strverscmp_improved_newer("123~rc1-99.99", "123^aa2-67.89");
+ test_strverscmp_improved_newer("123~rc1-99.99", "123aa2-67.89");
+
+ /* '-' : separator between version and release. */
+ test_strverscmp_improved_newer("123-99.99", "123.45-67.89");
+ test_strverscmp_improved_newer("123-99.99", "123^aa2-67.89");
+ test_strverscmp_improved_newer("123-99.99", "123aa2-67.89");
+
+ /* '^' : patch releases */
+ test_strverscmp_improved_newer("123^45-67.89", "123.45-67.89");
+ test_strverscmp_improved_newer("123^aa1-99.99", "123^aa2-67.89");
+ test_strverscmp_improved_newer("123^aa2-67.89", "123aa2-67.89");
+
+ /* '.' : point release */
+ test_strverscmp_improved_newer("123.aa2-67.89", "123aa2-67.89");
+ test_strverscmp_improved_newer("123.aa2-67.89", "123.ab2-67.89");
+
+ /* invalid characters */
+ assert_se(strverscmp_improved("123_aa2-67.89", "123aa+2-67.89") == 0);
+
+ /* some corner cases */
+ assert_se(strverscmp_improved("123.", "123") > 0); /* One more version segment */
+ assert_se(strverscmp_improved("12_3", "123") < 0); /* 12 < 123 */
+ assert_se(strverscmp_improved("12_3", "12") > 0); /* 3 > '' */
+ assert_se(strverscmp_improved("12_3", "12.3") > 0); /* 3 > '' */
+ assert_se(strverscmp_improved("123.0", "123") > 0); /* 0 > '' */
+ assert_se(strverscmp_improved("123_0", "123") > 0); /* 0 > '' */
+ assert_se(strverscmp_improved("123..0", "123.0") < 0); /* '' < 0 */
+
+ /* empty strings or strings with ignored characters only */
+ assert_se(strverscmp_improved("", NULL) == 0);
+ assert_se(strverscmp_improved(NULL, "") == 0);
+ assert_se(strverscmp_improved("0_", "0") == 0);
+ assert_se(strverscmp_improved("_0_", "0") == 0);
+ assert_se(strverscmp_improved("_0", "0") == 0);
+ assert_se(strverscmp_improved("0", "0___") == 0);
+ assert_se(strverscmp_improved("", "_") == 0);
+ assert_se(strverscmp_improved("_", "") == 0);
+ assert_se(strverscmp_improved("_", "_") == 0);
+ assert_se(strverscmp_improved("", "~") > 0);
+ assert_se(strverscmp_improved("~", "") < 0);
+ assert_se(strverscmp_improved("~", "~") == 0);
+
+ /* non-ASCII digits */
+ (void) setlocale(LC_NUMERIC, "ar_YE.utf8");
+ assert_se(strverscmp_improved("1٠١٢٣٤٥٦٧٨٩", "1") == 0);
+
+ (void) setlocale(LC_NUMERIC, "th_TH.utf8");
+ assert_se(strverscmp_improved("1๐๑๒๓๔๕๖๗๘๙", "1") == 0);
+}
+
+#define RPMVERCMP(a, b, c) \
+ test_strverscmp_improved_one(STRINGIFY(a), STRINGIFY(b), (c))
+
+TEST(strverscmp_improved_rpm) {
+ /* Tests copied from rmp's rpmio test suite, under the LGPL license:
+ * https://github.com/rpm-software-management/rpm/blob/master/tests/rpmvercmp.at.
+ * The original form is retained for easy comparisons and updates.
+ */
+
+ RPMVERCMP(1.0, 1.0, 0);
+ RPMVERCMP(1.0, 2.0, -1);
+ RPMVERCMP(2.0, 1.0, 1);
+
+ RPMVERCMP(2.0.1, 2.0.1, 0);
+ RPMVERCMP(2.0, 2.0.1, -1);
+ RPMVERCMP(2.0.1, 2.0, 1);
+
+ RPMVERCMP(2.0.1a, 2.0.1a, 0);
+ RPMVERCMP(2.0.1a, 2.0.1, 1);
+ RPMVERCMP(2.0.1, 2.0.1a, -1);
+
+ RPMVERCMP(5.5p1, 5.5p1, 0);
+ RPMVERCMP(5.5p1, 5.5p2, -1);
+ RPMVERCMP(5.5p2, 5.5p1, 1);
+
+ RPMVERCMP(5.5p10, 5.5p10, 0);
+ RPMVERCMP(5.5p1, 5.5p10, -1);
+ RPMVERCMP(5.5p10, 5.5p1, 1);
+
+ RPMVERCMP(10xyz, 10.1xyz, 1); /* Note: this is reversed from rpm's vercmp */
+ RPMVERCMP(10.1xyz, 10xyz, -1); /* Note: this is reversed from rpm's vercmp */
+
+ RPMVERCMP(xyz10, xyz10, 0);
+ RPMVERCMP(xyz10, xyz10.1, -1);
+ RPMVERCMP(xyz10.1, xyz10, 1);
+
+ RPMVERCMP(xyz.4, xyz.4, 0);
+ RPMVERCMP(xyz.4, 8, -1);
+ RPMVERCMP(8, xyz.4, 1);
+ RPMVERCMP(xyz.4, 2, -1);
+ RPMVERCMP(2, xyz.4, 1);
+
+ RPMVERCMP(5.5p2, 5.6p1, -1);
+ RPMVERCMP(5.6p1, 5.5p2, 1);
+
+ RPMVERCMP(5.6p1, 6.5p1, -1);
+ RPMVERCMP(6.5p1, 5.6p1, 1);
+
+ RPMVERCMP(6.0.rc1, 6.0, 1);
+ RPMVERCMP(6.0, 6.0.rc1, -1);
+
+ RPMVERCMP(10b2, 10a1, 1);
+ RPMVERCMP(10a2, 10b2, -1);
+
+ RPMVERCMP(1.0aa, 1.0aa, 0);
+ RPMVERCMP(1.0a, 1.0aa, -1);
+ RPMVERCMP(1.0aa, 1.0a, 1);
+
+ RPMVERCMP(10.0001, 10.0001, 0);
+ RPMVERCMP(10.0001, 10.1, 0);
+ RPMVERCMP(10.1, 10.0001, 0);
+ RPMVERCMP(10.0001, 10.0039, -1);
+ RPMVERCMP(10.0039, 10.0001, 1);
+
+ RPMVERCMP(4.999.9, 5.0, -1);
+ RPMVERCMP(5.0, 4.999.9, 1);
+
+ RPMVERCMP(20101121, 20101121, 0);
+ RPMVERCMP(20101121, 20101122, -1);
+ RPMVERCMP(20101122, 20101121, 1);
+
+ RPMVERCMP(2_0, 2_0, 0);
+ RPMVERCMP(2.0, 2_0, -1); /* Note: in rpm those compare equal */
+ RPMVERCMP(2_0, 2.0, 1); /* Note: in rpm those compare equal */
+
+ /* RhBug:178798 case */
+ RPMVERCMP(a, a, 0);
+ RPMVERCMP(a+, a+, 0);
+ RPMVERCMP(a+, a_, 0);
+ RPMVERCMP(a_, a+, 0);
+ RPMVERCMP(+a, +a, 0);
+ RPMVERCMP(+a, _a, 0);
+ RPMVERCMP(_a, +a, 0);
+ RPMVERCMP(+_, +_, 0);
+ RPMVERCMP(_+, +_, 0);
+ RPMVERCMP(_+, _+, 0);
+ RPMVERCMP(+, _, 0);
+ RPMVERCMP(_, +, 0);
+
+ /* Basic testcases for tilde sorting */
+ RPMVERCMP(1.0~rc1, 1.0~rc1, 0);
+ RPMVERCMP(1.0~rc1, 1.0, -1);
+ RPMVERCMP(1.0, 1.0~rc1, 1);
+ RPMVERCMP(1.0~rc1, 1.0~rc2, -1);
+ RPMVERCMP(1.0~rc2, 1.0~rc1, 1);
+ RPMVERCMP(1.0~rc1~git123, 1.0~rc1~git123, 0);
+ RPMVERCMP(1.0~rc1~git123, 1.0~rc1, -1);
+ RPMVERCMP(1.0~rc1, 1.0~rc1~git123, 1);
+
+ /* Basic testcases for caret sorting */
+ RPMVERCMP(1.0^, 1.0^, 0);
+ RPMVERCMP(1.0^, 1.0, 1);
+ RPMVERCMP(1.0, 1.0^, -1);
+ RPMVERCMP(1.0^git1, 1.0^git1, 0);
+ RPMVERCMP(1.0^git1, 1.0, 1);
+ RPMVERCMP(1.0, 1.0^git1, -1);
+ RPMVERCMP(1.0^git1, 1.0^git2, -1);
+ RPMVERCMP(1.0^git2, 1.0^git1, 1);
+ RPMVERCMP(1.0^git1, 1.01, -1);
+ RPMVERCMP(1.01, 1.0^git1, 1);
+ RPMVERCMP(1.0^20160101, 1.0^20160101, 0);
+ RPMVERCMP(1.0^20160101, 1.0.1, -1);
+ RPMVERCMP(1.0.1, 1.0^20160101, 1);
+ RPMVERCMP(1.0^20160101^git1, 1.0^20160101^git1, 0);
+ RPMVERCMP(1.0^20160102, 1.0^20160101^git1, 1);
+ RPMVERCMP(1.0^20160101^git1, 1.0^20160102, -1);
+
+ /* Basic testcases for tilde and caret sorting */
+ RPMVERCMP(1.0~rc1^git1, 1.0~rc1^git1, 0);
+ RPMVERCMP(1.0~rc1^git1, 1.0~rc1, 1);
+ RPMVERCMP(1.0~rc1, 1.0~rc1^git1, -1);
+ RPMVERCMP(1.0^git1~pre, 1.0^git1~pre, 0);
+ RPMVERCMP(1.0^git1, 1.0^git1~pre, 1);
+ RPMVERCMP(1.0^git1~pre, 1.0^git1, -1);
+
+ /* These are included here to document current, arguably buggy behaviors
+ * for reference purposes and for easy checking against unintended
+ * behavior changes. */
+ log_info("/* RPM version comparison oddities */");
+ /* RhBug:811992 case */
+ RPMVERCMP(1b.fc17, 1b.fc17, 0);
+ RPMVERCMP(1b.fc17, 1.fc17, 1); /* Note: this is reversed from rpm's vercmp, WAT! */
+ RPMVERCMP(1.fc17, 1b.fc17, -1);
+ RPMVERCMP(1g.fc17, 1g.fc17, 0);
+ RPMVERCMP(1g.fc17, 1.fc17, 1);
+ RPMVERCMP(1.fc17, 1g.fc17, -1);
+
+ /* Non-ascii characters are considered equal so these are all the same, eh… */
+ RPMVERCMP(1.1.α, 1.1.α, 0);
+ RPMVERCMP(1.1.α, 1.1.β, 0);
+ RPMVERCMP(1.1.β, 1.1.α, 0);
+ RPMVERCMP(1.1.αα, 1.1.α, 0);
+ RPMVERCMP(1.1.α, 1.1.ββ, 0);
+ RPMVERCMP(1.1.ββ, 1.1.αα, 0);
+}
+
+TEST(strextendf) {
+ _cleanup_free_ char *p = NULL;
+
+ assert_se(strextendf(&p, "<%i>", 77) >= 0);
+ assert_se(streq(p, "<77>"));
+
+ assert_se(strextendf(&p, "<%i>", 99) >= 0);
+ assert_se(streq(p, "<77><99>"));
+
+ assert_se(strextendf(&p, "<%80i>", 88) >= 0);
+ assert_se(streq(p, "<77><99>< 88>"));
+
+ assert_se(strextendf(&p, "<%08x>", 0x1234u) >= 0);
+ assert_se(streq(p, "<77><99>< 88><00001234>"));
+
+ p = mfree(p);
+
+ assert_se(strextendf_with_separator(&p, ",", "<%i>", 77) >= 0);
+ assert_se(streq(p, "<77>"));
+
+ assert_se(strextendf_with_separator(&p, ",", "<%i>", 99) >= 0);
+ assert_se(streq(p, "<77>,<99>"));
+
+ assert_se(strextendf_with_separator(&p, ",", "<%80i>", 88) >= 0);
+ assert_se(streq(p, "<77>,<99>,< 88>"));
+
+ assert_se(strextendf_with_separator(&p, ",", "<%08x>", 0x1234u) >= 0);
+ assert_se(streq(p, "<77>,<99>,< 88>,<00001234>"));
+}
+
+TEST(string_replace_char) {
+ assert_se(streq(string_replace_char(strdupa_safe(""), 'a', 'b'), ""));
+ assert_se(streq(string_replace_char(strdupa_safe("abc"), 'a', 'b'), "bbc"));
+ assert_se(streq(string_replace_char(strdupa_safe("hoge"), 'a', 'b'), "hoge"));
+ assert_se(streq(string_replace_char(strdupa_safe("aaaa"), 'a', 'b'), "bbbb"));
+ assert_se(streq(string_replace_char(strdupa_safe("aaaa"), 'a', '\t'), "\t\t\t\t"));
+}
+
+TEST(strspn_from_end) {
+ assert_se(strspn_from_end(NULL, NULL) == 0);
+ assert_se(strspn_from_end("hoge", NULL) == 0);
+ assert_se(strspn_from_end(NULL, DIGITS) == 0);
+ assert_se(strspn_from_end("", DIGITS) == 0);
+ assert_se(strspn_from_end("hoge", DIGITS) == 0);
+ assert_se(strspn_from_end("1234", DIGITS) == 4);
+ assert_se(strspn_from_end("aaa1234", DIGITS) == 4);
+ assert_se(strspn_from_end("aaa1234aaa", DIGITS) == 0);
+ assert_se(strspn_from_end("aaa12aa34", DIGITS) == 2);
+}
+
+TEST(streq_skip_trailing_chars) {
+ /* NULL is WHITESPACE by default*/
+ assert_se(streq_skip_trailing_chars("foo bar", "foo bar", NULL));
+ assert_se(streq_skip_trailing_chars("foo", "foo", NULL));
+ assert_se(streq_skip_trailing_chars("foo bar ", "foo bar", NULL));
+ assert_se(streq_skip_trailing_chars("foo bar", "foo bar\t\t", NULL));
+ assert_se(streq_skip_trailing_chars("foo bar ", "foo bar\t\t", NULL));
+ assert_se(streq_skip_trailing_chars("foo\nbar", "foo\nbar", NULL));
+ assert_se(streq_skip_trailing_chars("\t\tfoo bar", "\t\tfoo bar", NULL));
+ assert_se(streq_skip_trailing_chars(" foo bar\t", " foo bar\n", NULL));
+
+ assert_se(!streq_skip_trailing_chars("foobar", "foo bar", NULL));
+ assert_se(!streq_skip_trailing_chars("foo\nbar", "foo\tbar", NULL));
+ assert_se(!streq_skip_trailing_chars("\t\nfoo bar", "\t foo bar", NULL));
+
+ assert_se(streq_skip_trailing_chars("foo bar ", "foo bar", WHITESPACE));
+ assert_se(!streq_skip_trailing_chars("foo bar ", "foo bar", NEWLINE));
+
+ assert_se(streq_skip_trailing_chars(NULL, NULL, NULL));
+ assert_se(streq_skip_trailing_chars("", "", NULL));
+ assert_se(!streq_skip_trailing_chars(NULL, "foo bar", NULL));
+ assert_se(!streq_skip_trailing_chars("foo", NULL, NULL));
+ assert_se(!streq_skip_trailing_chars("", "f", NULL));
+}
+
+#define TEST_MAKE_CSTRING_ONE(x, ret, mode, expect) \
+ do { \
+ _cleanup_free_ char *b = NULL; \
+ assert_se(make_cstring((x), ELEMENTSOF(x), (mode), &b) == (ret)); \
+ assert_se(streq_ptr(b, (expect))); \
+ } while(false)
+
+TEST(make_cstring) {
+ static const char test1[] = "this is a test",
+ test2[] = "",
+ test3[] = "a",
+ test4[] = "aa\0aa",
+ test5[] = { 'b', 'b', 0, 'b' , 'b' },
+ test6[] = {},
+ test7[] = { 'x' },
+ test8[] = { 'x', 'y', 'z' };
+
+ TEST_MAKE_CSTRING_ONE(test1, -EINVAL, MAKE_CSTRING_REFUSE_TRAILING_NUL, NULL);
+ TEST_MAKE_CSTRING_ONE(test1, 0, MAKE_CSTRING_ALLOW_TRAILING_NUL, "this is a test");
+ TEST_MAKE_CSTRING_ONE(test1, 0, MAKE_CSTRING_REQUIRE_TRAILING_NUL, "this is a test");
+
+ TEST_MAKE_CSTRING_ONE(test2, -EINVAL, MAKE_CSTRING_REFUSE_TRAILING_NUL, NULL);
+ TEST_MAKE_CSTRING_ONE(test2, 0, MAKE_CSTRING_ALLOW_TRAILING_NUL, "");
+ TEST_MAKE_CSTRING_ONE(test2, 0, MAKE_CSTRING_REQUIRE_TRAILING_NUL, "");
+
+ TEST_MAKE_CSTRING_ONE(test3, -EINVAL, MAKE_CSTRING_REFUSE_TRAILING_NUL, NULL);
+ TEST_MAKE_CSTRING_ONE(test3, 0, MAKE_CSTRING_ALLOW_TRAILING_NUL, "a");
+ TEST_MAKE_CSTRING_ONE(test3, 0, MAKE_CSTRING_REQUIRE_TRAILING_NUL, "a");
+
+ TEST_MAKE_CSTRING_ONE(test4, -EINVAL, MAKE_CSTRING_REFUSE_TRAILING_NUL, NULL);
+ TEST_MAKE_CSTRING_ONE(test4, -EINVAL, MAKE_CSTRING_ALLOW_TRAILING_NUL, NULL);
+ TEST_MAKE_CSTRING_ONE(test4, -EINVAL, MAKE_CSTRING_REQUIRE_TRAILING_NUL, NULL);
+
+ TEST_MAKE_CSTRING_ONE(test5, -EINVAL, MAKE_CSTRING_REFUSE_TRAILING_NUL, NULL);
+ TEST_MAKE_CSTRING_ONE(test5, -EINVAL, MAKE_CSTRING_ALLOW_TRAILING_NUL, NULL);
+ TEST_MAKE_CSTRING_ONE(test5, -EINVAL, MAKE_CSTRING_REQUIRE_TRAILING_NUL, NULL);
+
+ TEST_MAKE_CSTRING_ONE(test6, 0, MAKE_CSTRING_REFUSE_TRAILING_NUL, "");
+ TEST_MAKE_CSTRING_ONE(test6, 0, MAKE_CSTRING_ALLOW_TRAILING_NUL, "");
+ TEST_MAKE_CSTRING_ONE(test6, -EINVAL, MAKE_CSTRING_REQUIRE_TRAILING_NUL, NULL);
+
+ TEST_MAKE_CSTRING_ONE(test7, 0, MAKE_CSTRING_REFUSE_TRAILING_NUL, "x");
+ TEST_MAKE_CSTRING_ONE(test7, 0, MAKE_CSTRING_ALLOW_TRAILING_NUL, "x");
+ TEST_MAKE_CSTRING_ONE(test7, -EINVAL, MAKE_CSTRING_REQUIRE_TRAILING_NUL, NULL);
+
+ TEST_MAKE_CSTRING_ONE(test8, 0, MAKE_CSTRING_REFUSE_TRAILING_NUL, "xyz");
+ TEST_MAKE_CSTRING_ONE(test8, 0, MAKE_CSTRING_ALLOW_TRAILING_NUL, "xyz");
+ TEST_MAKE_CSTRING_ONE(test8, -EINVAL, MAKE_CSTRING_REQUIRE_TRAILING_NUL, NULL);
+}
+
+TEST(find_line_startswith) {
+ static const char text[] =
+ "foobar\n"
+ "this is a test\n"
+ "foobar: waldo\n"
+ "more\n"
+ "\n"
+ "piff\n"
+ "foobarfoobar\n"
+ "iff\n";
+ static const char emptystring[] = "";
+
+ assert_se(find_line_startswith(text, "") == text);
+ assert_se(find_line_startswith(text, "f") == text+1);
+ assert_se(find_line_startswith(text, "foobar") == text+6);
+ assert_se(!find_line_startswith(text, "foobarx"));
+ assert_se(!find_line_startswith(text, "oobar"));
+ assert_se(find_line_startswith(text, "t") == text + 8);
+ assert_se(find_line_startswith(text, "th") == text + 9);
+ assert_se(find_line_startswith(text, "this") == text + 11);
+ assert_se(find_line_startswith(text, "foobarf") == text + 54);
+ assert_se(find_line_startswith(text, "more\n") == text + 41);
+ assert_se(find_line_startswith(text, "\n") == text + 42);
+ assert_se(find_line_startswith(text, "iff") == text + 63);
+
+ assert_se(find_line_startswith(emptystring, "") == emptystring);
+ assert_se(!find_line_startswith(emptystring, "x"));
+}
+
+TEST(strstrafter) {
+ static const char buffer[] = "abcdefghijklmnopqrstuvwxyz";
+
+ assert_se(!strstrafter(NULL, NULL));
+ assert_se(!strstrafter("", NULL));
+ assert_se(!strstrafter(NULL, ""));
+ assert_se(streq_ptr(strstrafter("", ""), ""));
+
+ assert_se(strstrafter(buffer, "a") == buffer + 1);
+ assert_se(strstrafter(buffer, "") == buffer);
+ assert_se(strstrafter(buffer, "ab") == buffer + 2);
+ assert_se(strstrafter(buffer, "cde") == buffer + 5);
+ assert_se(strstrafter(buffer, "xyz") == strchr(buffer, 0));
+ assert_se(strstrafter(buffer, buffer) == strchr(buffer, 0));
+ assert_se(!strstrafter(buffer, "-"));
+}
+
+TEST(version_is_valid) {
+ assert_se(!version_is_valid(NULL));
+ assert_se(!version_is_valid(""));
+ assert_se(version_is_valid("0"));
+ assert_se(version_is_valid("5"));
+ assert_se(version_is_valid("999999"));
+ assert_se(version_is_valid("999999.5"));
+ assert_se(version_is_valid("6.2.12-300.fc38.x86_64"));
+}
+
+TEST(strextendn) {
+ _cleanup_free_ char *x = NULL;
+
+ assert_se(streq_ptr(strextendn(&x, NULL, 0), ""));
+ x = mfree(x);
+
+ assert_se(streq_ptr(strextendn(&x, "", 0), ""));
+ x = mfree(x);
+
+ assert_se(streq_ptr(strextendn(&x, "xxx", 3), "xxx"));
+ assert_se(streq_ptr(strextendn(&x, "xxx", 3), "xxxxxx"));
+ assert_se(streq_ptr(strextendn(&x, "...", 1), "xxxxxx."));
+ assert_se(streq_ptr(strextendn(&x, "...", 2), "xxxxxx..."));
+ assert_se(streq_ptr(strextendn(&x, "...", 3), "xxxxxx......"));
+ assert_se(streq_ptr(strextendn(&x, "...", 4), "xxxxxx........."));
+ x = mfree(x);
+}
+
+TEST(strlevenshtein) {
+ assert_se(strlevenshtein(NULL, NULL) == 0);
+ assert_se(strlevenshtein("", "") == 0);
+ assert_se(strlevenshtein("", NULL) == 0);
+ assert_se(strlevenshtein(NULL, "") == 0);
+
+ assert_se(strlevenshtein("a", "a") == 0);
+ assert_se(strlevenshtein("a", "b") == 1);
+ assert_se(strlevenshtein("b", "a") == 1);
+ assert_se(strlevenshtein("a", "") == 1);
+ assert_se(strlevenshtein("", "a") == 1);
+
+ assert_se(strlevenshtein("xxx", "xxx") == 0);
+ assert_se(strlevenshtein("xxx", "yyy") == 3);
+ assert_se(strlevenshtein("yyy", "xxx") == 3);
+ assert_se(strlevenshtein("xx", "xxx") == 1);
+ assert_se(strlevenshtein("xxx", "xx") == 1);
+ assert_se(strlevenshtein("x", "xxx") == 2);
+ assert_se(strlevenshtein("xxx", "x") == 2);
+
+ assert_se(strlevenshtein("sitting", "kitten") == 3);
+ assert_se(strlevenshtein("sunday", "saturday") == 3);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-strip-tab-ansi.c b/src/test/test-strip-tab-ansi.c
new file mode 100644
index 0000000..6f73d26
--- /dev/null
+++ b/src/test/test-strip-tab-ansi.c
@@ -0,0 +1,72 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+
+#include "alloc-util.h"
+#include "pretty-print.h"
+#include "string-util.h"
+#include "terminal-util.h"
+#include "tests.h"
+
+TEST(strip_tab_ansi) {
+ _cleanup_free_ char *urlified = NULL, *q = NULL, *qq = NULL;
+ char *p, *z;
+
+ assert_se(p = strdup("\tFoobar\tbar\twaldo\t"));
+ assert_se(strip_tab_ansi(&p, NULL, NULL));
+ fprintf(stdout, "<%s>\n", p);
+ assert_se(streq(p, " Foobar bar waldo "));
+ free(p);
+
+ assert_se(p = strdup(ANSI_HIGHLIGHT "Hello" ANSI_NORMAL ANSI_HIGHLIGHT_RED " world!" ANSI_NORMAL));
+ assert_se(strip_tab_ansi(&p, NULL, NULL));
+ fprintf(stdout, "<%s>\n", p);
+ assert_se(streq(p, "Hello world!"));
+ free(p);
+
+ assert_se(p = strdup("\x1B[\x1B[\t\x1B[" ANSI_HIGHLIGHT "\x1B[" "Hello" ANSI_NORMAL ANSI_HIGHLIGHT_RED " world!" ANSI_NORMAL));
+ assert_se(strip_tab_ansi(&p, NULL, NULL));
+ assert_se(streq(p, "\x1B[\x1B[ \x1B[\x1B[Hello world!"));
+ free(p);
+
+ assert_se(p = strdup("\x1B[waldo"));
+ assert_se(strip_tab_ansi(&p, NULL, NULL));
+ assert_se(streq(p, "\x1B[waldo"));
+ free(p);
+
+ assert_se(p = strdup("\r\rwaldo"));
+ assert_se(strip_tab_ansi(&p, NULL, NULL));
+ assert_se(streq(p, "\r\rwaldo"));
+ free(p);
+
+ assert_se(p = strdup("waldo\r\r"));
+ assert_se(strip_tab_ansi(&p, NULL, NULL));
+ assert_se(streq(p, "waldo"));
+ free(p);
+
+ assert_se(p = strdup("waldo\r\r\n\r\n"));
+ assert_se(strip_tab_ansi(&p, NULL, NULL));
+ assert_se(streq(p, "waldo\n\n"));
+ free(p);
+
+ assert_se(terminal_urlify_path("/etc/fstab", "i am a fabulous link", &urlified) >= 0);
+ assert_se(p = strjoin("something ", urlified, " something-else"));
+ assert_se(q = strdup(p));
+ printf("<%s>\n", p);
+ assert_se(strip_tab_ansi(&p, NULL, NULL));
+ printf("<%s>\n", p);
+ assert_se(streq(p, "something i am a fabulous link something-else"));
+ p = mfree(p);
+
+ /* Truncate the formatted string in the middle of an ANSI sequence (in which case we shouldn't touch the
+ * incomplete sequence) */
+ z = strstr(q, "fstab");
+ if (z) {
+ *z = 0;
+ assert_se(qq = strdup(q));
+ assert_se(strip_tab_ansi(&q, NULL, NULL));
+ assert_se(streq(q, qq));
+ }
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-strv.c b/src/test/test-strv.c
new file mode 100644
index 0000000..cfd662b
--- /dev/null
+++ b/src/test/test-strv.c
@@ -0,0 +1,1009 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "escape.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+
+TEST(str_in_set) {
+ assert_se(STR_IN_SET("x", "x", "y", "z"));
+ assert_se(!STR_IN_SET("X", "x", "y", "z"));
+ assert_se(!STR_IN_SET("", "x", "y", "z"));
+ assert_se(STR_IN_SET("x", "w", "x"));
+}
+
+TEST(strptr_in_set) {
+ assert_se(STRPTR_IN_SET("x", "x", "y", "z"));
+ assert_se(!STRPTR_IN_SET("X", "x", "y", "z"));
+ assert_se(!STRPTR_IN_SET("", "x", "y", "z"));
+ assert_se(STRPTR_IN_SET("x", "w", "x"));
+
+ assert_se(!STRPTR_IN_SET(NULL, "x", "y", "z"));
+ assert_se(!STRPTR_IN_SET(NULL, ""));
+ /* strv cannot contain a null, hence the result below */
+ assert_se(!STRPTR_IN_SET(NULL, NULL));
+}
+
+TEST(startswith_set) {
+ assert_se(!STARTSWITH_SET("foo", "bar", "baz", "waldo"));
+ assert_se(!STARTSWITH_SET("foo", "bar"));
+
+ assert_se(STARTSWITH_SET("abc", "a", "ab", "abc"));
+ assert_se(STARTSWITH_SET("abc", "ax", "ab", "abc"));
+ assert_se(STARTSWITH_SET("abc", "ax", "abx", "abc"));
+ assert_se(!STARTSWITH_SET("abc", "ax", "abx", "abcx"));
+
+ assert_se(streq_ptr(STARTSWITH_SET("foobar", "hhh", "kkk", "foo", "zzz"), "bar"));
+ assert_se(streq_ptr(STARTSWITH_SET("foobar", "hhh", "kkk", "", "zzz"), "foobar"));
+ assert_se(streq_ptr(STARTSWITH_SET("", "hhh", "kkk", "zzz", ""), ""));
+}
+
+static const char* const input_table_multiple[] = {
+ "one",
+ "two",
+ "three",
+ NULL,
+};
+
+static const char* const input_table_quoted[] = {
+ "one",
+ " two\t three ",
+ " four five",
+ NULL,
+};
+
+static const char* const input_table_quoted_joined[] = {
+ "one",
+ " two\t three " " four five",
+ NULL,
+};
+
+static const char* const input_table_one[] = {
+ "one",
+ NULL,
+};
+
+static const char* const input_table_none[] = {
+ NULL,
+};
+
+static const char* const input_table_two_empties[] = {
+ "",
+ "",
+ NULL,
+};
+
+static const char* const input_table_one_empty[] = {
+ "",
+ NULL,
+};
+
+static const char* const input_table_unescape[] = {
+ "ID_VENDOR=QEMU",
+ "ID_VENDOR_ENC=QEMUx20x20x20x20",
+ "ID_MODEL_ENC=QEMUx20HARDDISKx20x20x20",
+ NULL,
+};
+
+static const char* const input_table_retain_escape[] = {
+ "ID_VENDOR=QEMU",
+ "ID_VENDOR_ENC=QEMU\\x20\\x20\\x20\\x20",
+ "ID_MODEL_ENC=QEMU\\x20HARDDISK\\x20\\x20\\x20",
+ NULL,
+};
+
+TEST(strv_find) {
+ assert_se(strv_find((char **)input_table_multiple, "three"));
+ assert_se(!strv_find((char **)input_table_multiple, "four"));
+}
+
+TEST(strv_find_prefix) {
+ assert_se(strv_find_prefix((char **)input_table_multiple, "o"));
+ assert_se(strv_find_prefix((char **)input_table_multiple, "one"));
+ assert_se(strv_find_prefix((char **)input_table_multiple, ""));
+ assert_se(!strv_find_prefix((char **)input_table_multiple, "xxx"));
+ assert_se(!strv_find_prefix((char **)input_table_multiple, "onee"));
+}
+
+TEST(strv_find_startswith) {
+ char *r;
+
+ r = strv_find_startswith((char **)input_table_multiple, "o");
+ assert_se(r && streq(r, "ne"));
+
+ r = strv_find_startswith((char **)input_table_multiple, "one");
+ assert_se(r && streq(r, ""));
+
+ r = strv_find_startswith((char **)input_table_multiple, "");
+ assert_se(r && streq(r, "one"));
+
+ assert_se(!strv_find_startswith((char **)input_table_multiple, "xxx"));
+ assert_se(!strv_find_startswith((char **)input_table_multiple, "onee"));
+}
+
+TEST(strv_join) {
+ _cleanup_free_ char *p = strv_join((char **)input_table_multiple, ", ");
+ assert_se(p);
+ assert_se(streq(p, "one, two, three"));
+
+ _cleanup_free_ char *q = strv_join((char **)input_table_multiple, ";");
+ assert_se(q);
+ assert_se(streq(q, "one;two;three"));
+
+ _cleanup_free_ char *r = strv_join((char **)input_table_multiple, NULL);
+ assert_se(r);
+ assert_se(streq(r, "one two three"));
+
+ _cleanup_free_ char *s = strv_join(STRV_MAKE("1", "2", "3,3"), ",");
+ assert_se(s);
+ assert_se(streq(s, "1,2,3,3"));
+
+ _cleanup_free_ char *t = strv_join((char **)input_table_one, ", ");
+ assert_se(t);
+ assert_se(streq(t, "one"));
+
+ _cleanup_free_ char *u = strv_join((char **)input_table_none, ", ");
+ assert_se(u);
+ assert_se(streq(u, ""));
+
+ _cleanup_free_ char *v = strv_join((char **)input_table_two_empties, ", ");
+ assert_se(v);
+ assert_se(streq(v, ", "));
+
+ _cleanup_free_ char *w = strv_join((char **)input_table_one_empty, ", ");
+ assert_se(w);
+ assert_se(streq(w, ""));
+}
+
+TEST(strv_join_full) {
+ _cleanup_free_ char *p = strv_join_full((char **)input_table_multiple, ", ", "foo", false);
+ assert_se(p);
+ assert_se(streq(p, "fooone, footwo, foothree"));
+
+ _cleanup_free_ char *q = strv_join_full((char **)input_table_multiple, ";", "foo", false);
+ assert_se(q);
+ assert_se(streq(q, "fooone;footwo;foothree"));
+
+ _cleanup_free_ char *r = strv_join_full(STRV_MAKE("a", "a;b", "a:c"), ";", NULL, true);
+ assert_se(r);
+ assert_se(streq(r, "a;a\\;b;a:c"));
+
+ _cleanup_free_ char *s = strv_join_full(STRV_MAKE("a", "a;b", "a;;c", ";", ";x"), ";", NULL, true);
+ assert_se(s);
+ assert_se(streq(s, "a;a\\;b;a\\;\\;c;\\;;\\;x"));
+
+ _cleanup_free_ char *t = strv_join_full(STRV_MAKE("a", "a;b", "a:c", ";"), ";", "=", true);
+ assert_se(t);
+ assert_se(streq(t, "=a;=a\\;b;=a:c;=\\;"));
+ t = mfree(t);
+
+ _cleanup_free_ char *u = strv_join_full((char **)input_table_multiple, NULL, "foo", false);
+ assert_se(u);
+ assert_se(streq(u, "fooone footwo foothree"));
+
+ _cleanup_free_ char *v = strv_join_full((char **)input_table_one, ", ", "foo", false);
+ assert_se(v);
+ assert_se(streq(v, "fooone"));
+
+ _cleanup_free_ char *w = strv_join_full((char **)input_table_none, ", ", "foo", false);
+ assert_se(w);
+ assert_se(streq(w, ""));
+
+ _cleanup_free_ char *x = strv_join_full((char **)input_table_two_empties, ", ", "foo", false);
+ assert_se(x);
+ assert_se(streq(x, "foo, foo"));
+
+ _cleanup_free_ char *y = strv_join_full((char **)input_table_one_empty, ", ", "foo", false);
+ assert_se(y);
+ assert_se(streq(y, "foo"));
+}
+
+static void test_strv_unquote_one(const char *quoted, char **list) {
+ _cleanup_strv_free_ char **s = NULL;
+ _cleanup_free_ char *j = NULL;
+ unsigned i = 0;
+ int r;
+
+ log_info("/* %s */", __func__);
+
+ r = strv_split_full(&s, quoted, WHITESPACE, EXTRACT_UNQUOTE);
+ assert_se(r == (int) strv_length(list));
+ assert_se(s);
+ j = strv_join(s, " | ");
+ assert_se(j);
+ puts(j);
+
+ STRV_FOREACH(t, s)
+ assert_se(streq(list[i++], *t));
+
+ assert_se(list[i] == NULL);
+}
+
+TEST(strv_unquote) {
+ test_strv_unquote_one(" foo=bar \"waldo\" zzz ", STRV_MAKE("foo=bar", "waldo", "zzz"));
+ test_strv_unquote_one("", STRV_MAKE_EMPTY);
+ test_strv_unquote_one(" ", STRV_MAKE_EMPTY);
+ test_strv_unquote_one(" ", STRV_MAKE_EMPTY);
+ test_strv_unquote_one(" x", STRV_MAKE("x"));
+ test_strv_unquote_one("x ", STRV_MAKE("x"));
+ test_strv_unquote_one(" x ", STRV_MAKE("x"));
+ test_strv_unquote_one(" \"x\" ", STRV_MAKE("x"));
+ test_strv_unquote_one(" 'x' ", STRV_MAKE("x"));
+ test_strv_unquote_one(" 'x\"' ", STRV_MAKE("x\""));
+ test_strv_unquote_one(" \"x'\" ", STRV_MAKE("x'"));
+ test_strv_unquote_one("a '--b=c \"d e\"'", STRV_MAKE("a", "--b=c \"d e\""));
+
+ /* trailing backslashes */
+ test_strv_unquote_one(" x\\\\", STRV_MAKE("x\\"));
+}
+
+static void test_invalid_unquote_one(const char *quoted) {
+ char **s = NULL;
+ int r;
+
+ log_info("/* %s */", __func__);
+
+ r = strv_split_full(&s, quoted, WHITESPACE, EXTRACT_UNQUOTE);
+ assert_se(s == NULL);
+ assert_se(r == -EINVAL);
+}
+
+TEST(invalid_unquote) {
+ test_invalid_unquote_one(" x\\");
+ test_invalid_unquote_one("a --b='c \"d e\"''");
+ test_invalid_unquote_one("a --b='c \"d e\" '\"");
+ test_invalid_unquote_one("a --b='c \"d e\"garbage");
+ test_invalid_unquote_one("'");
+ test_invalid_unquote_one("\"");
+ test_invalid_unquote_one("'x'y'g");
+}
+
+TEST(strv_split) {
+ _cleanup_strv_free_erase_ char **l = NULL;
+ const char str[] = "one,two,three";
+
+ l = strv_split(str, ",");
+ assert_se(l);
+ assert_se(strv_equal(l, (char**) input_table_multiple));
+
+ strv_free_erase(l);
+
+ l = strv_split(" one two\t three", WHITESPACE);
+ assert_se(l);
+ assert_se(strv_equal(l, (char**) input_table_multiple));
+
+ strv_free_erase(l);
+
+ /* Setting NULL for separator is equivalent to WHITESPACE */
+ l = strv_split(" one two\t three", NULL);
+ assert_se(l);
+ assert_se(strv_equal(l, (char**) input_table_multiple));
+
+ strv_free_erase(l);
+
+ assert_se(strv_split_full(&l, " one two\t three", NULL, 0) == 3);
+ assert_se(strv_equal(l, (char**) input_table_multiple));
+
+ strv_free_erase(l);
+
+ assert_se(strv_split_full(&l, " 'one' \" two\t three \" ' four five'", NULL, EXTRACT_UNQUOTE) == 3);
+ assert_se(strv_equal(l, (char**) input_table_quoted));
+
+ l = strv_free_erase(l);
+
+ /* missing last quote causes extraction to fail. */
+ assert_se(strv_split_full(&l, " 'one' \" two\t three \" ' four five", NULL, EXTRACT_UNQUOTE) == -EINVAL);
+ assert_se(!l);
+
+ /* missing last quote, but the last element is _not_ ignored with EXTRACT_RELAX. */
+ assert_se(strv_split_full(&l, " 'one' \" two\t three \" ' four five", NULL, EXTRACT_UNQUOTE | EXTRACT_RELAX) == 3);
+ assert_se(strv_equal(l, (char**) input_table_quoted));
+
+ l = strv_free_erase(l);
+
+ /* missing separator between items */
+ assert_se(strv_split_full(&l, " 'one' \" two\t three \"' four five'", NULL, EXTRACT_UNQUOTE | EXTRACT_RELAX) == 2);
+ assert_se(strv_equal(l, (char**) input_table_quoted_joined));
+
+ l = strv_free_erase(l);
+
+ assert_se(strv_split_full(&l, " 'one' \" two\t three \"' four five", NULL,
+ EXTRACT_UNQUOTE | EXTRACT_RELAX | EXTRACT_UNESCAPE_RELAX) == 2);
+ assert_se(strv_equal(l, (char**) input_table_quoted_joined));
+
+ l = strv_free_erase(l);
+
+ assert_se(strv_split_full(&l, "\\", NULL, EXTRACT_UNQUOTE | EXTRACT_RELAX | EXTRACT_UNESCAPE_RELAX) == 1);
+ assert_se(strv_equal(l, STRV_MAKE("\\")));
+
+ l = strv_free_erase(l);
+
+ assert_se(l = strv_split("\\", NULL));
+ assert_se(strv_equal(l, STRV_MAKE("\\")));
+
+ l = strv_free_erase(l);
+
+ assert_se(l = strv_split("aa\\ bb\\", NULL));
+ assert_se(strv_equal(l, STRV_MAKE("aa\\", "bb\\")));
+
+ l = strv_free_erase(l);
+
+ assert_se(l = strv_split("aa\" bb'", NULL));
+ assert_se(strv_equal(l, STRV_MAKE("aa\"", "bb'")));
+}
+
+TEST(strv_split_empty) {
+ _cleanup_strv_free_ char **l = NULL;
+
+ l = strv_split("", WHITESPACE);
+ assert_se(l);
+ assert_se(strv_isempty(l));
+ l = strv_free(l);
+
+ assert_se(l = strv_split("", NULL));
+ assert_se(strv_isempty(l));
+ l = strv_free(l);
+
+ assert_se(strv_split_full(&l, "", NULL, 0) == 0);
+ assert_se(l);
+ assert_se(strv_isempty(l));
+ l = strv_free(l);
+
+ assert_se(strv_split_full(&l, "", NULL, EXTRACT_UNQUOTE) == 0);
+ assert_se(l);
+ assert_se(strv_isempty(l));
+ l = strv_free(l);
+
+ assert_se(strv_split_full(&l, "", WHITESPACE, EXTRACT_UNQUOTE) == 0);
+ assert_se(l);
+ assert_se(strv_isempty(l));
+ l = strv_free(l);
+
+ assert_se(strv_split_full(&l, "", WHITESPACE, EXTRACT_UNQUOTE | EXTRACT_RELAX) == 0);
+ assert_se(l);
+ assert_se(strv_isempty(l));
+ strv_free(l);
+
+ l = strv_split(" ", WHITESPACE);
+ assert_se(l);
+ assert_se(strv_isempty(l));
+ strv_free(l);
+
+ l = strv_split(" ", NULL);
+ assert_se(l);
+ assert_se(strv_isempty(l));
+ l = strv_free(l);
+
+ assert_se(strv_split_full(&l, " ", NULL, 0) == 0);
+ assert_se(l);
+ assert_se(strv_isempty(l));
+ l = strv_free(l);
+
+ assert_se(strv_split_full(&l, " ", WHITESPACE, EXTRACT_UNQUOTE) == 0);
+ assert_se(l);
+ assert_se(strv_isempty(l));
+ l = strv_free(l);
+
+ assert_se(strv_split_full(&l, " ", NULL, EXTRACT_UNQUOTE) == 0);
+ assert_se(l);
+ assert_se(strv_isempty(l));
+ l = strv_free(l);
+
+ assert_se(strv_split_full(&l, " ", NULL, EXTRACT_UNQUOTE | EXTRACT_RELAX) == 0);
+ assert_se(l);
+ assert_se(strv_isempty(l));
+}
+
+TEST(strv_split_full) {
+ _cleanup_strv_free_ char **l = NULL;
+ const char *str = ":foo\\:bar::waldo:";
+ int r;
+
+ r = strv_split_full(&l, str, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
+ assert_se(r == (int) strv_length(l));
+ assert_se(streq_ptr(l[0], ""));
+ assert_se(streq_ptr(l[1], "foo:bar"));
+ assert_se(streq_ptr(l[2], ""));
+ assert_se(streq_ptr(l[3], "waldo"));
+ assert_se(streq_ptr(l[4], ""));
+ assert_se(streq_ptr(l[5], NULL));
+}
+
+TEST(strv_split_and_extend_full) {
+ _cleanup_strv_free_ char **l = NULL;
+ const char *str1 = ":foo\\:bar:";
+ const char *str2 = "waldo::::::baz";
+ int r;
+
+ r = strv_split_and_extend(&l, "", ":", false);
+ assert_se(r == (int) strv_length(l));
+ r = strv_split_and_extend_full(&l, str1, ":", false, EXTRACT_DONT_COALESCE_SEPARATORS);
+ assert_se(r == (int) strv_length(l));
+ assert_se(streq_ptr(l[0], ""));
+ assert_se(streq_ptr(l[1], "foo:bar"));
+ assert_se(streq_ptr(l[2], ""));
+ r = strv_split_and_extend_full(&l, str2, ":", false, 0);
+ assert_se(r == (int) strv_length(l));
+ assert_se(streq_ptr(l[3], "waldo"));
+ assert_se(streq_ptr(l[4], "baz"));
+ assert_se(streq_ptr(l[5], NULL));
+}
+
+TEST(strv_split_colon_pairs) {
+ _cleanup_strv_free_ char **l = NULL;
+ const char *str = "one:two three four:five six seven:eight\\:nine ten\\:eleven\\\\",
+ *str_inval="one:two three:four:five";
+ int r;
+
+ r = strv_split_colon_pairs(&l, str);
+ assert_se(r == (int) strv_length(l));
+ assert_se(r == 12);
+ assert_se(streq_ptr(l[0], "one"));
+ assert_se(streq_ptr(l[1], "two"));
+ assert_se(streq_ptr(l[2], "three"));
+ assert_se(streq_ptr(l[3], ""));
+ assert_se(streq_ptr(l[4], "four"));
+ assert_se(streq_ptr(l[5], "five"));
+ assert_se(streq_ptr(l[6], "six"));
+ assert_se(streq_ptr(l[7], ""));
+ assert_se(streq_ptr(l[8], "seven"));
+ assert_se(streq_ptr(l[9], "eight:nine"));
+ assert_se(streq_ptr(l[10], "ten:eleven\\"));
+ assert_se(streq_ptr(l[11], ""));
+ assert_se(streq_ptr(l[12], NULL));
+
+ r = strv_split_colon_pairs(&l, str_inval);
+ assert_se(r == -EINVAL);
+}
+
+TEST(strv_split_newlines) {
+ unsigned i = 0;
+ _cleanup_strv_free_ char **l = NULL;
+ const char str[] = "one\ntwo\nthree";
+
+ l = strv_split_newlines(str);
+ assert_se(l);
+
+ STRV_FOREACH(s, l)
+ assert_se(streq(*s, input_table_multiple[i++]));
+}
+
+TEST(strv_split_newlines_full) {
+ const char str[] =
+ "ID_VENDOR=QEMU\n"
+ "ID_VENDOR_ENC=QEMU\\x20\\x20\\x20\\x20\n"
+ "ID_MODEL_ENC=QEMU\\x20HARDDISK\\x20\\x20\\x20\n"
+ "\n\n\n";
+ _cleanup_strv_free_ char **l = NULL;
+
+ assert_se(strv_split_newlines_full(&l, str, 0) == 3);
+ assert_se(strv_equal(l, (char**) input_table_unescape));
+
+ l = strv_free(l);
+
+ assert_se(strv_split_newlines_full(&l, str, EXTRACT_RETAIN_ESCAPE) == 3);
+ assert_se(strv_equal(l, (char**) input_table_retain_escape));
+}
+
+TEST(strv_overlap) {
+ const char * const input_table[] = {
+ "one",
+ "two",
+ "three",
+ NULL
+ };
+ const char * const input_table_overlap[] = {
+ "two",
+ NULL
+ };
+ const char * const input_table_unique[] = {
+ "four",
+ "five",
+ "six",
+ NULL
+ };
+
+ assert_se(strv_overlap((char **)input_table, (char**)input_table_overlap));
+ assert_se(!strv_overlap((char **)input_table, (char**)input_table_unique));
+}
+
+TEST(strv_sort) {
+ const char* input_table[] = {
+ "durian",
+ "apple",
+ "citrus",
+ "CAPITAL LETTERS FIRST",
+ "banana",
+ NULL
+ };
+
+ strv_sort((char **)input_table);
+
+ assert_se(streq(input_table[0], "CAPITAL LETTERS FIRST"));
+ assert_se(streq(input_table[1], "apple"));
+ assert_se(streq(input_table[2], "banana"));
+ assert_se(streq(input_table[3], "citrus"));
+ assert_se(streq(input_table[4], "durian"));
+}
+
+TEST(strv_extend_strv_concat) {
+ _cleanup_strv_free_ char **a = NULL, **b = NULL;
+
+ a = strv_new("without", "suffix");
+ b = strv_new("with", "suffix");
+ assert_se(a);
+ assert_se(b);
+
+ assert_se(strv_extend_strv_concat(&a, b, "_suffix") >= 0);
+
+ assert_se(streq(a[0], "without"));
+ assert_se(streq(a[1], "suffix"));
+ assert_se(streq(a[2], "with_suffix"));
+ assert_se(streq(a[3], "suffix_suffix"));
+}
+
+TEST(strv_extend_strv) {
+ _cleanup_strv_free_ char **a = NULL, **b = NULL, **n = NULL;
+
+ a = strv_new("abc", "def", "ghi");
+ b = strv_new("jkl", "mno", "abc", "pqr");
+ assert_se(a);
+ assert_se(b);
+
+ assert_se(strv_extend_strv(&a, b, true) == 3);
+
+ assert_se(streq(a[0], "abc"));
+ assert_se(streq(a[1], "def"));
+ assert_se(streq(a[2], "ghi"));
+ assert_se(streq(a[3], "jkl"));
+ assert_se(streq(a[4], "mno"));
+ assert_se(streq(a[5], "pqr"));
+ assert_se(strv_length(a) == 6);
+
+ assert_se(strv_extend_strv(&n, b, false) >= 0);
+ assert_se(streq(n[0], "jkl"));
+ assert_se(streq(n[1], "mno"));
+ assert_se(streq(n[2], "abc"));
+ assert_se(streq(n[3], "pqr"));
+ assert_se(strv_length(n) == 4);
+}
+
+TEST(strv_extend_with_size) {
+ _cleanup_strv_free_ char **a = NULL;
+ size_t n = SIZE_MAX;
+
+ a = strv_new("test", "test1");
+ assert_se(a);
+
+ assert_se(strv_extend_with_size(&a, &n, "test2") >= 0);
+ assert_se(n == 3);
+ assert_se(strv_extend_with_size(&a, &n, "test3") >= 0);
+ assert_se(n == 4);
+
+ assert_se(streq(a[0], "test"));
+ assert_se(streq(a[1], "test1"));
+ assert_se(streq(a[2], "test2"));
+ assert_se(streq(a[3], "test3"));
+ assert_se(a[4] == NULL);
+}
+
+TEST(strv_extend) {
+ _cleanup_strv_free_ char **a = NULL, **b = NULL;
+
+ a = strv_new("test", "test1");
+ assert_se(a);
+ assert_se(strv_extend(&a, "test2") >= 0);
+ assert_se(strv_extend(&b, "test3") >= 0);
+
+ assert_se(streq(a[0], "test"));
+ assert_se(streq(a[1], "test1"));
+ assert_se(streq(a[2], "test2"));
+ assert_se(streq(b[0], "test3"));
+}
+
+TEST(strv_extendf) {
+ _cleanup_strv_free_ char **a = NULL, **b = NULL;
+
+ a = strv_new("test", "test1");
+ assert_se(a);
+ assert_se(strv_extendf(&a, "test2 %s %d %s", "foo", 128, "bar") >= 0);
+ assert_se(strv_extendf(&b, "test3 %s %s %d", "bar", "foo", 128) >= 0);
+
+ assert_se(streq(a[0], "test"));
+ assert_se(streq(a[1], "test1"));
+ assert_se(streq(a[2], "test2 foo 128 bar"));
+ assert_se(streq(b[0], "test3 bar foo 128"));
+}
+
+TEST(strv_foreach) {
+ _cleanup_strv_free_ char **a;
+ unsigned i = 0;
+
+ a = strv_new("one", "two", "three");
+ assert_se(a);
+
+ STRV_FOREACH(check, a)
+ assert_se(streq(*check, input_table_multiple[i++]));
+}
+
+TEST(strv_foreach_backwards) {
+ _cleanup_strv_free_ char **a;
+ unsigned i = 2;
+
+ a = strv_new("one", "two", "three");
+
+ assert_se(a);
+
+ STRV_FOREACH_BACKWARDS(check, a)
+ assert_se(streq_ptr(*check, input_table_multiple[i--]));
+
+ STRV_FOREACH_BACKWARDS(check, (char**) NULL)
+ assert_not_reached();
+
+ STRV_FOREACH_BACKWARDS(check, STRV_MAKE_EMPTY)
+ assert_not_reached();
+
+ unsigned count = 0;
+ STRV_FOREACH_BACKWARDS(check, STRV_MAKE("ONE"))
+ count++;
+ assert_se(count == 1);
+}
+
+TEST(strv_foreach_pair) {
+ _cleanup_strv_free_ char **a = NULL;
+
+ a = strv_new("pair_one", "pair_one",
+ "pair_two", "pair_two",
+ "pair_three", "pair_three");
+ STRV_FOREACH_PAIR(x, y, a)
+ assert_se(streq(*x, *y));
+}
+
+static void test_strv_from_stdarg_alloca_one(char **l, const char *first, ...) {
+ char **j;
+ unsigned i;
+
+ log_info("/* %s */", __func__);
+
+ j = strv_from_stdarg_alloca(first);
+
+ for (i = 0;; i++) {
+ assert_se(streq_ptr(l[i], j[i]));
+
+ if (!l[i])
+ break;
+ }
+}
+
+TEST(strv_from_stdarg_alloca) {
+ test_strv_from_stdarg_alloca_one(STRV_MAKE("foo", "bar"), "foo", "bar", NULL);
+ test_strv_from_stdarg_alloca_one(STRV_MAKE("foo"), "foo", NULL);
+ test_strv_from_stdarg_alloca_one(STRV_MAKE_EMPTY, NULL);
+}
+
+TEST(strv_insert) {
+ _cleanup_strv_free_ char **a = NULL;
+
+ assert_se(strv_insert(&a, 0, strdup("first")) == 0);
+ assert_se(streq(a[0], "first"));
+ assert_se(!a[1]);
+
+ assert_se(strv_insert(&a, 0, NULL) == 0);
+ assert_se(streq(a[0], "first"));
+ assert_se(!a[1]);
+
+ assert_se(strv_insert(&a, 1, strdup("two")) == 0);
+ assert_se(streq(a[0], "first"));
+ assert_se(streq(a[1], "two"));
+ assert_se(!a[2]);
+
+ assert_se(strv_insert(&a, 4, strdup("tri")) == 0);
+ assert_se(streq(a[0], "first"));
+ assert_se(streq(a[1], "two"));
+ assert_se(streq(a[2], "tri"));
+ assert_se(!a[3]);
+
+ assert_se(strv_insert(&a, 1, strdup("duo")) == 0);
+ assert_se(streq(a[0], "first"));
+ assert_se(streq(a[1], "duo"));
+ assert_se(streq(a[2], "two"));
+ assert_se(streq(a[3], "tri"));
+ assert_se(!a[4]);
+}
+
+TEST(strv_push_prepend) {
+ _cleanup_strv_free_ char **a = NULL;
+
+ assert_se(a = strv_new("foo", "bar", "three"));
+
+ assert_se(strv_push_prepend(&a, strdup("first")) >= 0);
+ assert_se(streq(a[0], "first"));
+ assert_se(streq(a[1], "foo"));
+ assert_se(streq(a[2], "bar"));
+ assert_se(streq(a[3], "three"));
+ assert_se(!a[4]);
+
+ assert_se(strv_consume_prepend(&a, strdup("first2")) >= 0);
+ assert_se(streq(a[0], "first2"));
+ assert_se(streq(a[1], "first"));
+ assert_se(streq(a[2], "foo"));
+ assert_se(streq(a[3], "bar"));
+ assert_se(streq(a[4], "three"));
+ assert_se(!a[5]);
+}
+
+TEST(strv_push_with_size) {
+ _cleanup_strv_free_ char **a = NULL;
+ size_t n = 0;
+ char *i, *j;
+
+ assert_se(i = strdup("foo"));
+ assert_se(strv_push_with_size(&a, &n, i) >= 0);
+ assert_se(n == 1);
+
+ assert_se(i = strdup("a"));
+ assert_se(j = strdup("b"));
+ assert_se(strv_push_with_size(&a, &n, i) >= 0);
+ assert_se(n == 2);
+ assert_se(strv_push_with_size(&a, &n, j) >= 0);
+ assert_se(n == 3);
+
+ assert_se(streq_ptr(a[0], "foo"));
+ assert_se(streq_ptr(a[1], "a"));
+ assert_se(streq_ptr(a[2], "b"));
+ assert_se(streq_ptr(a[3], NULL));
+
+ assert_se(n = strv_length(a));
+}
+
+TEST(strv_push) {
+ _cleanup_strv_free_ char **a = NULL;
+ char *i, *j;
+
+ assert_se(i = strdup("foo"));
+ assert_se(strv_push(&a, i) >= 0);
+
+ assert_se(i = strdup("a"));
+ assert_se(j = strdup("b"));
+ assert_se(strv_push_pair(&a, i, j) >= 0);
+
+ assert_se(streq_ptr(a[0], "foo"));
+ assert_se(streq_ptr(a[1], "a"));
+ assert_se(streq_ptr(a[2], "b"));
+ assert_se(streq_ptr(a[3], NULL));
+}
+
+TEST(strv_compare) {
+ _cleanup_strv_free_ char **a = NULL;
+ _cleanup_strv_free_ char **b = NULL;
+ _cleanup_strv_free_ char **c = NULL;
+ _cleanup_strv_free_ char **d = NULL;
+
+ a = strv_new("one", "two", "three");
+ assert_se(a);
+ b = strv_new("one", "two", "three");
+ assert_se(b);
+ c = strv_new("one", "two", "three", "four");
+ assert_se(c);
+ d = strv_new(NULL);
+ assert_se(d);
+
+ assert_se(strv_compare(a, a) == 0);
+ assert_se(strv_compare(a, b) == 0);
+ assert_se(strv_compare(d, d) == 0);
+ assert_se(strv_compare(d, NULL) == 0);
+ assert_se(strv_compare(NULL, NULL) == 0);
+
+ assert_se(strv_compare(a, c) < 0);
+ assert_se(strv_compare(b, c) < 0);
+ assert_se(strv_compare(b, d) == 1);
+ assert_se(strv_compare(b, NULL) == 1);
+}
+
+TEST(strv_is_uniq) {
+ _cleanup_strv_free_ char **a = NULL, **b = NULL, **c = NULL, **d = NULL;
+
+ a = strv_new(NULL);
+ assert_se(a);
+ assert_se(strv_is_uniq(a));
+
+ b = strv_new("foo");
+ assert_se(b);
+ assert_se(strv_is_uniq(b));
+
+ c = strv_new("foo", "bar");
+ assert_se(c);
+ assert_se(strv_is_uniq(c));
+
+ d = strv_new("foo", "bar", "waldo", "bar", "piep");
+ assert_se(d);
+ assert_se(!strv_is_uniq(d));
+}
+
+TEST(strv_reverse) {
+ _cleanup_strv_free_ char **a = NULL, **b = NULL, **c = NULL, **d = NULL;
+
+ a = strv_new(NULL);
+ assert_se(a);
+
+ strv_reverse(a);
+ assert_se(strv_isempty(a));
+
+ b = strv_new("foo");
+ assert_se(b);
+ strv_reverse(b);
+ assert_se(streq_ptr(b[0], "foo"));
+ assert_se(streq_ptr(b[1], NULL));
+
+ c = strv_new("foo", "bar");
+ assert_se(c);
+ strv_reverse(c);
+ assert_se(streq_ptr(c[0], "bar"));
+ assert_se(streq_ptr(c[1], "foo"));
+ assert_se(streq_ptr(c[2], NULL));
+
+ d = strv_new("foo", "bar", "waldo");
+ assert_se(d);
+ strv_reverse(d);
+ assert_se(streq_ptr(d[0], "waldo"));
+ assert_se(streq_ptr(d[1], "bar"));
+ assert_se(streq_ptr(d[2], "foo"));
+ assert_se(streq_ptr(d[3], NULL));
+}
+
+TEST(strv_shell_escape) {
+ _cleanup_strv_free_ char **v = NULL;
+
+ v = strv_new("foo:bar", "bar,baz", "wal\\do");
+ assert_se(v);
+ assert_se(strv_shell_escape(v, ",:"));
+ assert_se(streq_ptr(v[0], "foo\\:bar"));
+ assert_se(streq_ptr(v[1], "bar\\,baz"));
+ assert_se(streq_ptr(v[2], "wal\\\\do"));
+ assert_se(streq_ptr(v[3], NULL));
+}
+
+static void test_strv_skip_one(char **a, size_t n, char **b) {
+ a = strv_skip(a, n);
+ assert_se(strv_equal(a, b));
+}
+
+TEST(strv_skip) {
+ test_strv_skip_one(STRV_MAKE("foo", "bar", "baz"), 0, STRV_MAKE("foo", "bar", "baz"));
+ test_strv_skip_one(STRV_MAKE("foo", "bar", "baz"), 1, STRV_MAKE("bar", "baz"));
+ test_strv_skip_one(STRV_MAKE("foo", "bar", "baz"), 2, STRV_MAKE("baz"));
+ test_strv_skip_one(STRV_MAKE("foo", "bar", "baz"), 3, STRV_MAKE(NULL));
+ test_strv_skip_one(STRV_MAKE("foo", "bar", "baz"), 4, STRV_MAKE(NULL));
+ test_strv_skip_one(STRV_MAKE("foo", "bar", "baz"), 55, STRV_MAKE(NULL));
+
+ test_strv_skip_one(STRV_MAKE("quux"), 0, STRV_MAKE("quux"));
+ test_strv_skip_one(STRV_MAKE("quux"), 1, STRV_MAKE(NULL));
+ test_strv_skip_one(STRV_MAKE("quux"), 55, STRV_MAKE(NULL));
+
+ test_strv_skip_one(STRV_MAKE(NULL), 0, STRV_MAKE(NULL));
+ test_strv_skip_one(STRV_MAKE(NULL), 1, STRV_MAKE(NULL));
+ test_strv_skip_one(STRV_MAKE(NULL), 55, STRV_MAKE(NULL));
+}
+
+TEST(strv_extend_n) {
+ _cleanup_strv_free_ char **v = NULL;
+
+ v = strv_new("foo", "bar");
+ assert_se(v);
+
+ assert_se(strv_extend_n(&v, "waldo", 3) >= 0);
+ assert_se(strv_extend_n(&v, "piep", 2) >= 0);
+
+ assert_se(streq(v[0], "foo"));
+ assert_se(streq(v[1], "bar"));
+ assert_se(streq(v[2], "waldo"));
+ assert_se(streq(v[3], "waldo"));
+ assert_se(streq(v[4], "waldo"));
+ assert_se(streq(v[5], "piep"));
+ assert_se(streq(v[6], "piep"));
+ assert_se(v[7] == NULL);
+
+ v = strv_free(v);
+
+ assert_se(strv_extend_n(&v, "foo", 1) >= 0);
+ assert_se(strv_extend_n(&v, "bar", 0) >= 0);
+
+ assert_se(streq(v[0], "foo"));
+ assert_se(v[1] == NULL);
+}
+
+TEST(foreach_string) {
+ const char * const t[] = {
+ "foo",
+ "bar",
+ "waldo",
+ NULL
+ };
+
+ unsigned i = 0;
+ FOREACH_STRING(x, "foo", "bar", "waldo")
+ assert_se(streq_ptr(t[i++], x));
+ assert_se(i == 3);
+
+ FOREACH_STRING(x, "zzz")
+ assert_se(streq(x, "zzz"));
+}
+
+TEST(strv_fnmatch) {
+ _cleanup_strv_free_ char **v = NULL;
+ size_t pos;
+
+ assert_se(!strv_fnmatch(STRV_MAKE_EMPTY, "a"));
+
+ v = strv_new("xxx", "*\\*", "yyy");
+ assert_se(!strv_fnmatch_full(v, "\\", 0, NULL));
+ assert_se(strv_fnmatch_full(v, "\\", FNM_NOESCAPE, &pos));
+ assert_se(pos == 1);
+}
+
+TEST(strv_extend_join) {
+ _cleanup_strv_free_ char **v = NULL;
+
+ assert_se(strv_extend_assignment(&v, "MESSAGE", "ABC") >= 0);
+ assert_se(strv_extend_assignment(&v, "ABC", "QER") >= 0);
+ assert_se(strv_extend_assignment(&v, "MISSING", NULL) >= 0);
+
+ assert_se(strv_length(v) == 2);
+ assert_se(streq(v[0], "MESSAGE=ABC"));
+ assert_se(streq(v[1], "ABC=QER"));
+}
+
+TEST(strv_copy_n) {
+ char **x = STRV_MAKE("a", "b", "c", "d", "e");
+ _cleanup_strv_free_ char **l = NULL;
+
+ l = strv_copy_n(x, 0);
+ assert_se(strv_equal(l, NULL));
+ strv_free(l);
+
+ l = strv_copy_n(x, 0);
+ assert_se(strv_equal(l, (char**) { NULL }));
+ strv_free(l);
+
+ l = strv_copy_n(x, 1);
+ assert_se(strv_equal(l, STRV_MAKE("a")));
+ strv_free(l);
+
+ l = strv_copy_n(x, 2);
+ assert_se(strv_equal(l, STRV_MAKE("a", "b")));
+ strv_free(l);
+
+ l = strv_copy_n(x, 3);
+ assert_se(strv_equal(l, STRV_MAKE("a", "b", "c")));
+ strv_free(l);
+
+ l = strv_copy_n(x, 4);
+ assert_se(strv_equal(l, STRV_MAKE("a", "b", "c", "d")));
+ strv_free(l);
+
+ l = strv_copy_n(x, 5);
+ assert_se(strv_equal(l, STRV_MAKE("a", "b", "c", "d", "e")));
+ strv_free(l);
+
+ l = strv_copy_n(x, 6);
+ assert_se(strv_equal(l, STRV_MAKE("a", "b", "c", "d", "e")));
+ strv_free(l);
+
+ l = strv_copy_n(x, SIZE_MAX);
+ assert_se(strv_equal(l, STRV_MAKE("a", "b", "c", "d", "e")));
+}
+
+TEST(strv_find_first_field) {
+ char **haystack = STRV_MAKE("a", "b", "c", "d", "e", "f", "g", "h", "i", "j");
+
+ assert_se(strv_find_first_field(NULL, NULL) == NULL);
+ assert_se(strv_find_first_field(NULL, haystack) == NULL);
+ assert_se(strv_find_first_field(STRV_MAKE("k", "l", "m", "d", "b"), NULL) == NULL);
+ assert_se(strv_find_first_field(STRV_MAKE("k", "l", "m", "d", "b"), haystack) == NULL);
+ assert_se(streq_ptr(strv_find_first_field(STRV_MAKE("k", "l", "m", "d", "a", "c"), haystack), "b"));
+ assert_se(streq_ptr(strv_find_first_field(STRV_MAKE("k", "l", "m", "d", "c", "a"), haystack), "d"));
+ assert_se(streq_ptr(strv_find_first_field(STRV_MAKE("i", "k", "l", "m", "d", "c", "a", "b"), haystack), "j"));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-strxcpyx.c b/src/test/test-strxcpyx.c
new file mode 100644
index 0000000..b679522
--- /dev/null
+++ b/src/test/test-strxcpyx.c
@@ -0,0 +1,175 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+
+#include "string-util.h"
+#include "strxcpyx.h"
+#include "tests.h"
+
+TEST(strpcpy) {
+ char target[25];
+ char *s = target;
+ size_t space_left;
+ bool truncated;
+
+ space_left = sizeof(target);
+ space_left = strpcpy_full(&s, space_left, "12345", &truncated);
+ assert_se(!truncated);
+ space_left = strpcpy_full(&s, space_left, "hey hey hey", &truncated);
+ assert_se(!truncated);
+ space_left = strpcpy_full(&s, space_left, "waldo", &truncated);
+ assert_se(!truncated);
+ space_left = strpcpy_full(&s, space_left, "ba", &truncated);
+ assert_se(!truncated);
+ space_left = strpcpy_full(&s, space_left, "r", &truncated);
+ assert_se(!truncated);
+ assert_se(space_left == 1);
+ assert_se(streq(target, "12345hey hey heywaldobar"));
+
+ space_left = strpcpy_full(&s, space_left, "", &truncated);
+ assert_se(!truncated);
+ assert_se(space_left == 1);
+ assert_se(streq(target, "12345hey hey heywaldobar"));
+
+ space_left = strpcpy_full(&s, space_left, "f", &truncated);
+ assert_se(truncated);
+ assert_se(space_left == 0);
+ assert_se(streq(target, "12345hey hey heywaldobar"));
+
+ space_left = strpcpy_full(&s, space_left, "", &truncated);
+ assert_se(!truncated);
+ assert_se(space_left == 0);
+ assert_se(streq(target, "12345hey hey heywaldobar"));
+
+ space_left = strpcpy_full(&s, space_left, "foo", &truncated);
+ assert_se(truncated);
+ assert_se(space_left == 0);
+ assert_se(streq(target, "12345hey hey heywaldobar"));
+}
+
+TEST(strpcpyf) {
+ char target[25];
+ char *s = target;
+ size_t space_left;
+ bool truncated;
+
+ space_left = sizeof(target);
+ space_left = strpcpyf_full(&s, space_left, &truncated, "space left: %zu. ", space_left);
+ assert_se(!truncated);
+ space_left = strpcpyf_full(&s, space_left, &truncated, "foo%s", "bar");
+ assert_se(!truncated);
+ assert_se(space_left == 3);
+ assert_se(streq(target, "space left: 25. foobar"));
+
+ space_left = strpcpyf_full(&s, space_left, &truncated, "%i", 42);
+ assert_se(!truncated);
+ assert_se(space_left == 1);
+ assert_se(streq(target, "space left: 25. foobar42"));
+
+ space_left = strpcpyf_full(&s, space_left, &truncated, "%s", "");
+ assert_se(!truncated);
+ assert_se(space_left == 1);
+ assert_se(streq(target, "space left: 25. foobar42"));
+
+ space_left = strpcpyf_full(&s, space_left, &truncated, "%c", 'x');
+ assert_se(truncated);
+ assert_se(space_left == 0);
+ assert_se(streq(target, "space left: 25. foobar42"));
+
+ space_left = strpcpyf_full(&s, space_left, &truncated, "%s", "");
+ assert_se(!truncated);
+ assert_se(space_left == 0);
+ assert_se(streq(target, "space left: 25. foobar42"));
+
+ space_left = strpcpyf_full(&s, space_left, &truncated, "abc%s", "hoge");
+ assert_se(truncated);
+ assert_se(space_left == 0);
+ assert_se(streq(target, "space left: 25. foobar42"));
+
+ /* test overflow */
+ s = target;
+ space_left = strpcpyf_full(&s, 12, &truncated, "00 left: %i. ", 999);
+ assert_se(truncated);
+ assert_se(streq(target, "00 left: 99"));
+ assert_se(space_left == 0);
+ assert_se(target[12] == '2');
+}
+
+TEST(strpcpyl) {
+ char target[25];
+ char *s = target;
+ size_t space_left;
+ bool truncated;
+
+ space_left = sizeof(target);
+ space_left = strpcpyl_full(&s, space_left, &truncated, "waldo", " test", " waldo. ", NULL);
+ assert_se(!truncated);
+ space_left = strpcpyl_full(&s, space_left, &truncated, "Banana", NULL);
+ assert_se(!truncated);
+ assert_se(space_left == 1);
+ assert_se(streq(target, "waldo test waldo. Banana"));
+
+ space_left = strpcpyl_full(&s, space_left, &truncated, "", "", "", NULL);
+ assert_se(!truncated);
+ assert_se(space_left == 1);
+ assert_se(streq(target, "waldo test waldo. Banana"));
+
+ space_left = strpcpyl_full(&s, space_left, &truncated, "", "x", "", NULL);
+ assert_se(truncated);
+ assert_se(space_left == 0);
+ assert_se(streq(target, "waldo test waldo. Banana"));
+
+ space_left = strpcpyl_full(&s, space_left, &truncated, "hoge", NULL);
+ assert_se(truncated);
+ assert_se(space_left == 0);
+ assert_se(streq(target, "waldo test waldo. Banana"));
+}
+
+TEST(strscpy) {
+ char target[25];
+ size_t space_left;
+ bool truncated;
+
+ space_left = sizeof(target);
+ space_left = strscpy_full(target, space_left, "12345", &truncated);
+ assert_se(!truncated);
+
+ assert_se(streq(target, "12345"));
+ assert_se(space_left == 20);
+}
+
+TEST(strscpyl) {
+ char target[25];
+ size_t space_left;
+ bool truncated;
+
+ space_left = sizeof(target);
+ space_left = strscpyl_full(target, space_left, &truncated, "12345", "waldo", "waldo", NULL);
+ assert_se(!truncated);
+
+ assert_se(streq(target, "12345waldowaldo"));
+ assert_se(space_left == 10);
+}
+
+TEST(sd_event_code_migration) {
+ char b[100 * DECIMAL_STR_MAX(unsigned) + 1];
+ char c[100 * DECIMAL_STR_MAX(unsigned) + 1], *p;
+ unsigned i;
+ size_t l;
+ int o, r;
+
+ for (i = o = 0; i < 100; i++) {
+ r = snprintf(&b[o], sizeof(b) - o, "%u ", i);
+ assert_se(r >= 0 && r < (int) sizeof(b) - o);
+ o += r;
+ }
+
+ p = c;
+ l = sizeof(c);
+ for (i = 0; i < 100; i++)
+ l = strpcpyf(&p, l, "%u ", i);
+
+ assert_se(streq(b, c));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-sysctl-util.c b/src/test/test-sysctl-util.c
new file mode 100644
index 0000000..81207f5
--- /dev/null
+++ b/src/test/test-sysctl-util.c
@@ -0,0 +1,75 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/utsname.h>
+
+#include "sd-id128.h"
+
+#include "errno-util.h"
+#include "hostname-util.h"
+#include "strv.h"
+#include "sysctl-util.h"
+#include "tests.h"
+
+static const char* const cases[] = {
+ "a.b.c", "a/b/c",
+ "a/b/c", "a/b/c",
+ "a/b.c/d", "a/b.c/d",
+ "a.b/c.d", "a/b.c/d",
+
+ "net.ipv4.conf.enp3s0/200.forwarding", "net/ipv4/conf/enp3s0.200/forwarding",
+ "net/ipv4/conf/enp3s0.200/forwarding", "net/ipv4/conf/enp3s0.200/forwarding",
+
+ "a...b...c", "a/b/c",
+ "a///b///c", "a/b/c",
+ ".a...b...c", "a/b/c",
+ "/a///b///c", "a/b/c",
+ NULL,
+};
+
+TEST(sysctl_normalize) {
+ STRV_FOREACH_PAIR(s, expected, cases) {
+ _cleanup_free_ char *t;
+
+ assert_se(t = strdup(*s));
+ assert_se(sysctl_normalize(t) == t);
+
+ log_info("\"%s\" → \"%s\", expected \"%s\"", *s, t, *expected);
+ assert_se(streq(t, *expected));
+ }
+}
+
+TEST(sysctl_read) {
+ _cleanup_free_ char *s = NULL;
+ struct utsname u;
+ sd_id128_t a, b;
+ int r;
+
+ assert_se(sysctl_read("kernel/random/boot_id", &s) >= 0);
+ assert_se(sd_id128_from_string(s, &a) >= 0);
+ assert_se(sd_id128_get_boot(&b) >= 0);
+ assert_se(sd_id128_equal(a, b));
+ s = mfree(s);
+
+ assert_se(sysctl_read_ip_property(AF_INET, "lo", "forwarding", &s));
+ assert_se(STR_IN_SET(s, "0", "1"));
+
+ r = sysctl_write_ip_property(AF_INET, "lo", "forwarding", s);
+ assert_se(r >= 0 || ERRNO_IS_PRIVILEGE(r) || r == -EROFS);
+ s = mfree(s);
+
+ assert_se(sysctl_read_ip_property(AF_INET, NULL, "ip_forward", &s));
+ assert_se(STR_IN_SET(s, "0", "1"));
+
+ r = sysctl_write_ip_property(AF_INET, NULL, "ip_forward", s);
+ assert_se(r >= 0 || ERRNO_IS_PRIVILEGE(r) || r == -EROFS);
+ s = mfree(s);
+
+ assert_se(sysctl_read("kernel/hostname", &s) >= 0);
+ assert_se(uname(&u) >= 0);
+ assert_se(streq_ptr(s, u.nodename));
+
+ r = sysctl_write("kernel/hostname", s);
+ assert_se(r >= 0 || ERRNO_IS_PRIVILEGE(r) || r == -EROFS);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-tables.c b/src/test/test-tables.c
new file mode 100644
index 0000000..8abfba5
--- /dev/null
+++ b/src/test/test-tables.c
@@ -0,0 +1,131 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "architecture.h"
+#include "automount.h"
+#include "cgroup.h"
+#include "cgroup-util.h"
+#include "compress.h"
+#include "condition.h"
+#include "confidential-virt.h"
+#include "device-private.h"
+#include "device.h"
+#include "discover-image.h"
+#include "execute.h"
+#include "import-util.h"
+#include "install.h"
+#include "job.h"
+#include "kill.h"
+#include "locale-util.h"
+#include "log.h"
+#include "logs-show.h"
+#include "mount.h"
+#include "netif-naming-scheme.h"
+#include "path.h"
+#include "process-util.h"
+#include "resolve-util.h"
+#include "rlimit-util.h"
+#include "scope.h"
+#include "service.h"
+#include "show-status.h"
+#include "slice.h"
+#include "socket-util.h"
+#include "socket.h"
+#include "swap.h"
+#include "target.h"
+#include "test-tables.h"
+#include "tests.h"
+#include "timer.h"
+#include "unit-name.h"
+#include "unit.h"
+#include "virt.h"
+
+int main(int argc, char **argv) {
+ test_setup_logging(LOG_DEBUG);
+
+ test_table(architecture, ARCHITECTURE);
+ test_table(assert_type, CONDITION_TYPE);
+ test_table(automount_result, AUTOMOUNT_RESULT);
+ test_table(automount_state, AUTOMOUNT_STATE);
+ test_table(cgroup_controller, CGROUP_CONTROLLER);
+ test_table(cgroup_device_policy, CGROUP_DEVICE_POLICY);
+ test_table(cgroup_io_limit_type, CGROUP_IO_LIMIT_TYPE);
+ test_table(collect_mode, COLLECT_MODE);
+ test_table(condition_result, CONDITION_RESULT);
+ test_table(condition_type, CONDITION_TYPE);
+ test_table(confidential_virtualization, CONFIDENTIAL_VIRTUALIZATION);
+ test_table(device_action, SD_DEVICE_ACTION);
+ test_table(device_state, DEVICE_STATE);
+ test_table(dns_over_tls_mode, DNS_OVER_TLS_MODE);
+ test_table(dnssec_mode, DNSSEC_MODE);
+ test_table(emergency_action, EMERGENCY_ACTION);
+ test_table(exec_directory_type, EXEC_DIRECTORY_TYPE);
+ test_table(exec_input, EXEC_INPUT);
+ test_table(exec_keyring_mode, EXEC_KEYRING_MODE);
+ test_table(exec_output, EXEC_OUTPUT);
+ test_table(exec_preserve_mode, EXEC_PRESERVE_MODE);
+ test_table(exec_utmp_mode, EXEC_UTMP_MODE);
+ test_table(image_type, IMAGE_TYPE);
+ test_table(import_verify, IMPORT_VERIFY);
+ test_table(job_mode, JOB_MODE);
+ test_table(job_result, JOB_RESULT);
+ test_table(job_state, JOB_STATE);
+ test_table(job_type, JOB_TYPE);
+ test_table(kill_mode, KILL_MODE);
+ test_table(kill_who, KILL_WHO);
+ test_table(locale_variable, VARIABLE_LC);
+ test_table(log_target, LOG_TARGET);
+ test_table(managed_oom_mode, MANAGED_OOM_MODE);
+ test_table(managed_oom_preference, MANAGED_OOM_PREFERENCE);
+ test_table(manager_state, MANAGER_STATE);
+ test_table(manager_timestamp, MANAGER_TIMESTAMP);
+ test_table(mount_exec_command, MOUNT_EXEC_COMMAND);
+ test_table(mount_result, MOUNT_RESULT);
+ test_table(mount_state, MOUNT_STATE);
+ test_table(name_policy, NAMEPOLICY);
+ test_table(namespace_type, NAMESPACE_TYPE);
+ test_table(notify_access, NOTIFY_ACCESS);
+ test_table(notify_state, NOTIFY_STATE);
+ test_table(output_mode, OUTPUT_MODE);
+ test_table(partition_designator, PARTITION_DESIGNATOR);
+ test_table(path_result, PATH_RESULT);
+ test_table(path_state, PATH_STATE);
+ test_table(path_type, PATH_TYPE);
+ test_table(protect_home, PROTECT_HOME);
+ test_table(protect_system, PROTECT_SYSTEM);
+ test_table(resolve_support, RESOLVE_SUPPORT);
+ test_table(rlimit, RLIMIT);
+ test_table(scope_result, SCOPE_RESULT);
+ test_table(scope_state, SCOPE_STATE);
+ test_table(service_exec_command, SERVICE_EXEC_COMMAND);
+ test_table(service_restart, SERVICE_RESTART);
+ test_table(service_restart_mode, SERVICE_RESTART_MODE);
+ test_table(service_result, SERVICE_RESULT);
+ test_table(service_state, SERVICE_STATE);
+ test_table(service_type, SERVICE_TYPE);
+ test_table(show_status, SHOW_STATUS);
+ test_table(slice_state, SLICE_STATE);
+ test_table(socket_address_bind_ipv6_only, SOCKET_ADDRESS_BIND_IPV6_ONLY);
+ test_table(socket_exec_command, SOCKET_EXEC_COMMAND);
+ test_table(socket_result, SOCKET_RESULT);
+ test_table(socket_state, SOCKET_STATE);
+ test_table(swap_exec_command, SWAP_EXEC_COMMAND);
+ test_table(swap_result, SWAP_RESULT);
+ test_table(swap_state, SWAP_STATE);
+ test_table(target_state, TARGET_STATE);
+ test_table(timer_base, TIMER_BASE);
+ test_table(timer_result, TIMER_RESULT);
+ test_table(timer_state, TIMER_STATE);
+ test_table(unit_active_state, UNIT_ACTIVE_STATE);
+ test_table(unit_dependency, UNIT_DEPENDENCY);
+ test_table(install_change_type, INSTALL_CHANGE_TYPE);
+ test_table(unit_file_preset_mode, UNIT_FILE_PRESET_MODE);
+ test_table(unit_file_state, UNIT_FILE_STATE);
+ test_table(unit_load_state, UNIT_LOAD_STATE);
+ test_table(unit_type, UNIT_TYPE);
+ test_table(virtualization, VIRTUALIZATION);
+ test_table(compression, COMPRESSION);
+
+ assert_cc(sizeof(sd_device_action_t) == sizeof(int64_t));
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/test/test-terminal-util.c b/src/test/test-terminal-util.c
new file mode 100644
index 0000000..a2b7101
--- /dev/null
+++ b/src/test/test-terminal-util.c
@@ -0,0 +1,168 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "macro.h"
+#include "path-util.h"
+#include "strv.h"
+#include "terminal-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+#define LOREM_IPSUM "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " \
+ "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation " \
+ "ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit " \
+ "in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat " \
+ "non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
+
+TEST(default_term_for_tty) {
+ puts(default_term_for_tty("/dev/tty23"));
+ puts(default_term_for_tty("/dev/ttyS23"));
+ puts(default_term_for_tty("/dev/tty0"));
+ puts(default_term_for_tty("/dev/pty0"));
+ puts(default_term_for_tty("/dev/pts/0"));
+ puts(default_term_for_tty("/dev/console"));
+ puts(default_term_for_tty("tty23"));
+ puts(default_term_for_tty("ttyS23"));
+ puts(default_term_for_tty("tty0"));
+ puts(default_term_for_tty("pty0"));
+ puts(default_term_for_tty("pts/0"));
+ puts(default_term_for_tty("console"));
+}
+
+TEST(read_one_char) {
+ _cleanup_fclose_ FILE *file = NULL;
+ char r;
+ bool need_nl;
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-read_one_char.XXXXXX";
+
+ assert_se(fmkostemp_safe(name, "r+", &file) == 0);
+
+ assert_se(fputs("c\n", file) >= 0);
+ rewind(file);
+ assert_se(read_one_char(file, &r, 1000000, &need_nl) >= 0);
+ assert_se(!need_nl);
+ assert_se(r == 'c');
+ assert_se(read_one_char(file, &r, 1000000, &need_nl) < 0);
+
+ rewind(file);
+ assert_se(fputs("foobar\n", file) >= 0);
+ rewind(file);
+ assert_se(read_one_char(file, &r, 1000000, &need_nl) < 0);
+
+ rewind(file);
+ assert_se(fputs("\n", file) >= 0);
+ rewind(file);
+ assert_se(read_one_char(file, &r, 1000000, &need_nl) < 0);
+}
+
+TEST(getttyname_malloc) {
+ _cleanup_free_ char *ttyname = NULL;
+ _cleanup_close_ int master = -EBADF;
+
+ assert_se((master = posix_openpt(O_RDWR|O_NOCTTY)) >= 0);
+ assert_se(getttyname_malloc(master, &ttyname) >= 0);
+ log_info("ttyname = %s", ttyname);
+
+ assert_se(PATH_IN_SET(ttyname, "ptmx", "pts/ptmx"));
+}
+
+typedef struct {
+ const char *name;
+ const char* (*func)(void);
+} Color;
+
+static const Color colors[] = {
+ { "normal", ansi_normal },
+ { "highlight", ansi_highlight },
+ { "black", ansi_black },
+ { "red", ansi_red },
+ { "green", ansi_green },
+ { "yellow", ansi_yellow },
+ { "blue", ansi_blue },
+ { "magenta", ansi_magenta },
+ { "cyan", ansi_cyan },
+ { "white", ansi_white },
+ { "grey", ansi_grey },
+
+ { "bright-black", ansi_bright_black },
+ { "bright-red", ansi_bright_red },
+ { "bright-green", ansi_bright_green },
+ { "bright-yellow", ansi_bright_yellow },
+ { "bright-blue", ansi_bright_blue },
+ { "bright-magenta", ansi_bright_magenta },
+ { "bright-cyan", ansi_bright_cyan },
+ { "bright-white", ansi_bright_white },
+
+ { "highlight-black", ansi_highlight_black },
+ { "highlight-red", ansi_highlight_red },
+ { "highlight-green", ansi_highlight_green },
+ { "highlight-yellow (original)", _ansi_highlight_yellow },
+ { "highlight-yellow (replacement)", ansi_highlight_yellow },
+ { "highlight-blue", ansi_highlight_blue },
+ { "highlight-magenta", ansi_highlight_magenta },
+ { "highlight-cyan", ansi_highlight_cyan },
+ { "highlight-white", ansi_highlight_white },
+ { "highlight-grey", ansi_highlight_grey },
+
+ { "underline", ansi_underline },
+ { "highlight-underline", ansi_highlight_underline },
+ { "highlight-red-underline", ansi_highlight_red_underline },
+ { "highlight-green-underline", ansi_highlight_green_underline },
+ { "highlight-yellow-underline", ansi_highlight_yellow_underline },
+ { "highlight-blue-underline", ansi_highlight_blue_underline },
+ { "highlight-magenta-underline", ansi_highlight_magenta_underline },
+ { "highlight-grey-underline", ansi_highlight_grey_underline },
+};
+
+TEST(colors) {
+ for (size_t i = 0; i < ELEMENTSOF(colors); i++)
+ printf("<%s%s%s>\n", colors[i].func(), colors[i].name, ansi_normal());
+}
+
+TEST(text) {
+ for (size_t i = 0; !streq(colors[i].name, "underline"); i++) {
+ bool blwh = strstr(colors[i].name, "black")
+ || strstr(colors[i].name, "white");
+
+ printf("\n"
+ "Testing color %s%s\n%s%s%s\n",
+ colors[i].name,
+ blwh ? "" : ", this text should be readable",
+ colors[i].func(),
+ LOREM_IPSUM,
+ ansi_normal());
+ }
+}
+
+TEST(get_ctty) {
+ _cleanup_free_ char *ctty = NULL;
+ struct stat st;
+ dev_t devnr;
+ int r;
+
+ r = get_ctty(0, &devnr, &ctty);
+ if (r < 0) {
+ log_notice_errno(r, "Apparently called without a controlling TTY, cutting get_ctty() test short: %m");
+ return;
+ }
+
+ /* In almost all cases STDIN will match our controlling TTY. Let's verify that and then compare paths */
+ assert_se(fstat(STDIN_FILENO, &st) >= 0);
+ if (S_ISCHR(st.st_mode) && st.st_rdev == devnr) {
+ _cleanup_free_ char *stdin_name = NULL;
+
+ assert_se(getttyname_malloc(STDIN_FILENO, &stdin_name) >= 0);
+ assert_se(path_equal(stdin_name, ctty));
+ } else
+ log_notice("Not invoked with stdin == ctty, cutting get_ctty() test short");
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-time-util.c b/src/test/test-time-util.c
new file mode 100644
index 0000000..53bc779
--- /dev/null
+++ b/src/test/test-time-util.c
@@ -0,0 +1,1195 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "env-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "random-util.h"
+#include "serialize.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "time-util.h"
+
+#define TRIAL 100u
+
+TEST(parse_sec) {
+ usec_t u;
+
+ assert_se(parse_sec("5s", &u) >= 0);
+ assert_se(u == 5 * USEC_PER_SEC);
+ assert_se(parse_sec("5s500ms", &u) >= 0);
+ assert_se(u == 5 * USEC_PER_SEC + 500 * USEC_PER_MSEC);
+ assert_se(parse_sec(" 5s 500ms ", &u) >= 0);
+ assert_se(u == 5 * USEC_PER_SEC + 500 * USEC_PER_MSEC);
+ assert_se(parse_sec(" 5.5s ", &u) >= 0);
+ assert_se(u == 5 * USEC_PER_SEC + 500 * USEC_PER_MSEC);
+ assert_se(parse_sec(" 5.5s 0.5ms ", &u) >= 0);
+ assert_se(u == 5 * USEC_PER_SEC + 500 * USEC_PER_MSEC + 500);
+ assert_se(parse_sec(" .22s ", &u) >= 0);
+ assert_se(u == 220 * USEC_PER_MSEC);
+ assert_se(parse_sec(" .50y ", &u) >= 0);
+ assert_se(u == USEC_PER_YEAR / 2);
+ assert_se(parse_sec("2.5", &u) >= 0);
+ assert_se(u == 2500 * USEC_PER_MSEC);
+ assert_se(parse_sec(".7", &u) >= 0);
+ assert_se(u == 700 * USEC_PER_MSEC);
+ assert_se(parse_sec("23us", &u) >= 0);
+ assert_se(u == 23);
+ assert_se(parse_sec("23μs", &u) >= 0); /* greek small letter mu */
+ assert_se(u == 23);
+ assert_se(parse_sec("23µs", &u) >= 0); /* micro symbol */
+ assert_se(u == 23);
+ assert_se(parse_sec("infinity", &u) >= 0);
+ assert_se(u == USEC_INFINITY);
+ assert_se(parse_sec(" infinity ", &u) >= 0);
+ assert_se(u == USEC_INFINITY);
+ assert_se(parse_sec("+3.1s", &u) >= 0);
+ assert_se(u == 3100 * USEC_PER_MSEC);
+ assert_se(parse_sec("3.1s.2", &u) >= 0);
+ assert_se(u == 3300 * USEC_PER_MSEC);
+ assert_se(parse_sec("3.1 .2", &u) >= 0);
+ assert_se(u == 3300 * USEC_PER_MSEC);
+ assert_se(parse_sec("3.1 sec .2 sec", &u) >= 0);
+ assert_se(u == 3300 * USEC_PER_MSEC);
+ assert_se(parse_sec("3.1 sec 1.2 sec", &u) >= 0);
+ assert_se(u == 4300 * USEC_PER_MSEC);
+
+ assert_se(parse_sec(" xyz ", &u) < 0);
+ assert_se(parse_sec("", &u) < 0);
+ assert_se(parse_sec(" . ", &u) < 0);
+ assert_se(parse_sec(" 5. ", &u) < 0);
+ assert_se(parse_sec(".s ", &u) < 0);
+ assert_se(parse_sec("-5s ", &u) < 0);
+ assert_se(parse_sec("-0.3s ", &u) < 0);
+ assert_se(parse_sec("-0.0s ", &u) < 0);
+ assert_se(parse_sec("-0.-0s ", &u) < 0);
+ assert_se(parse_sec("0.-0s ", &u) < 0);
+ assert_se(parse_sec("3.-0s ", &u) < 0);
+ assert_se(parse_sec(" infinity .7", &u) < 0);
+ assert_se(parse_sec(".3 infinity", &u) < 0);
+ assert_se(parse_sec("3.+1s", &u) < 0);
+ assert_se(parse_sec("3. 1s", &u) < 0);
+ assert_se(parse_sec("3.s", &u) < 0);
+ assert_se(parse_sec("12.34.56", &u) < 0);
+ assert_se(parse_sec("12..34", &u) < 0);
+ assert_se(parse_sec("..1234", &u) < 0);
+ assert_se(parse_sec("1234..", &u) < 0);
+}
+
+TEST(parse_sec_fix_0) {
+ usec_t u;
+
+ assert_se(parse_sec_fix_0("5s", &u) >= 0);
+ assert_se(u == 5 * USEC_PER_SEC);
+ assert_se(parse_sec_fix_0("0s", &u) >= 0);
+ assert_se(u == USEC_INFINITY);
+ assert_se(parse_sec_fix_0("0", &u) >= 0);
+ assert_se(u == USEC_INFINITY);
+ assert_se(parse_sec_fix_0(" 0", &u) >= 0);
+ assert_se(u == USEC_INFINITY);
+}
+
+TEST(parse_sec_def_infinity) {
+ usec_t u;
+
+ assert_se(parse_sec_def_infinity("5s", &u) >= 0);
+ assert_se(u == 5 * USEC_PER_SEC);
+ assert_se(parse_sec_def_infinity("", &u) >= 0);
+ assert_se(u == USEC_INFINITY);
+ assert_se(parse_sec_def_infinity(" ", &u) >= 0);
+ assert_se(u == USEC_INFINITY);
+ assert_se(parse_sec_def_infinity("0s", &u) >= 0);
+ assert_se(u == 0);
+ assert_se(parse_sec_def_infinity("0", &u) >= 0);
+ assert_se(u == 0);
+ assert_se(parse_sec_def_infinity(" 0", &u) >= 0);
+ assert_se(u == 0);
+ assert_se(parse_sec_def_infinity("-5s", &u) < 0);
+}
+
+TEST(parse_time) {
+ usec_t u;
+
+ assert_se(parse_time("5", &u, 1) >= 0);
+ assert_se(u == 5);
+
+ assert_se(parse_time("5", &u, USEC_PER_MSEC) >= 0);
+ assert_se(u == 5 * USEC_PER_MSEC);
+
+ assert_se(parse_time("5", &u, USEC_PER_SEC) >= 0);
+ assert_se(u == 5 * USEC_PER_SEC);
+
+ assert_se(parse_time("5s", &u, 1) >= 0);
+ assert_se(u == 5 * USEC_PER_SEC);
+
+ assert_se(parse_time("5s", &u, USEC_PER_SEC) >= 0);
+ assert_se(u == 5 * USEC_PER_SEC);
+
+ assert_se(parse_time("5s", &u, USEC_PER_MSEC) >= 0);
+ assert_se(u == 5 * USEC_PER_SEC);
+
+ assert_se(parse_time("11111111111111y", &u, 1) == -ERANGE);
+ assert_se(parse_time("1.1111111111111y", &u, 1) >= 0);
+}
+
+TEST(parse_nsec) {
+ nsec_t u;
+
+ assert_se(parse_nsec("5s", &u) >= 0);
+ assert_se(u == 5 * NSEC_PER_SEC);
+ assert_se(parse_nsec("5s500ms", &u) >= 0);
+ assert_se(u == 5 * NSEC_PER_SEC + 500 * NSEC_PER_MSEC);
+ assert_se(parse_nsec(" 5s 500ms ", &u) >= 0);
+ assert_se(u == 5 * NSEC_PER_SEC + 500 * NSEC_PER_MSEC);
+ assert_se(parse_nsec(" 5.5s ", &u) >= 0);
+ assert_se(u == 5 * NSEC_PER_SEC + 500 * NSEC_PER_MSEC);
+ assert_se(parse_nsec(" 5.5s 0.5ms ", &u) >= 0);
+ assert_se(u == 5 * NSEC_PER_SEC + 500 * NSEC_PER_MSEC + 500 * NSEC_PER_USEC);
+ assert_se(parse_nsec(" .22s ", &u) >= 0);
+ assert_se(u == 220 * NSEC_PER_MSEC);
+ assert_se(parse_nsec(" .50y ", &u) >= 0);
+ assert_se(u == NSEC_PER_YEAR / 2);
+ assert_se(parse_nsec("2.5", &u) >= 0);
+ assert_se(u == 2);
+ assert_se(parse_nsec(".7", &u) >= 0);
+ assert_se(u == 0);
+ assert_se(parse_nsec("infinity", &u) >= 0);
+ assert_se(u == NSEC_INFINITY);
+ assert_se(parse_nsec(" infinity ", &u) >= 0);
+ assert_se(u == NSEC_INFINITY);
+ assert_se(parse_nsec("+3.1s", &u) >= 0);
+ assert_se(u == 3100 * NSEC_PER_MSEC);
+ assert_se(parse_nsec("3.1s.2", &u) >= 0);
+ assert_se(u == 3100 * NSEC_PER_MSEC);
+ assert_se(parse_nsec("3.1 .2s", &u) >= 0);
+ assert_se(u == 200 * NSEC_PER_MSEC + 3);
+ assert_se(parse_nsec("3.1 sec .2 sec", &u) >= 0);
+ assert_se(u == 3300 * NSEC_PER_MSEC);
+ assert_se(parse_nsec("3.1 sec 1.2 sec", &u) >= 0);
+ assert_se(u == 4300 * NSEC_PER_MSEC);
+
+ assert_se(parse_nsec(" xyz ", &u) < 0);
+ assert_se(parse_nsec("", &u) < 0);
+ assert_se(parse_nsec(" . ", &u) < 0);
+ assert_se(parse_nsec(" 5. ", &u) < 0);
+ assert_se(parse_nsec(".s ", &u) < 0);
+ assert_se(parse_nsec(" infinity .7", &u) < 0);
+ assert_se(parse_nsec(".3 infinity", &u) < 0);
+ assert_se(parse_nsec("-5s ", &u) < 0);
+ assert_se(parse_nsec("-0.3s ", &u) < 0);
+ assert_se(parse_nsec("-0.0s ", &u) < 0);
+ assert_se(parse_nsec("-0.-0s ", &u) < 0);
+ assert_se(parse_nsec("0.-0s ", &u) < 0);
+ assert_se(parse_nsec("3.-0s ", &u) < 0);
+ assert_se(parse_nsec(" infinity .7", &u) < 0);
+ assert_se(parse_nsec(".3 infinity", &u) < 0);
+ assert_se(parse_nsec("3.+1s", &u) < 0);
+ assert_se(parse_nsec("3. 1s", &u) < 0);
+ assert_se(parse_nsec("3.s", &u) < 0);
+ assert_se(parse_nsec("12.34.56", &u) < 0);
+ assert_se(parse_nsec("12..34", &u) < 0);
+ assert_se(parse_nsec("..1234", &u) < 0);
+ assert_se(parse_nsec("1234..", &u) < 0);
+ assert_se(parse_nsec("1111111111111y", &u) == -ERANGE);
+ assert_se(parse_nsec("1.111111111111y", &u) >= 0);
+}
+
+static void test_format_timespan_one(usec_t x, usec_t accuracy) {
+ char l[FORMAT_TIMESPAN_MAX];
+ const char *t;
+ usec_t y;
+
+ log_debug(USEC_FMT" (at accuracy "USEC_FMT")", x, accuracy);
+
+ assert_se(t = format_timespan(l, sizeof l, x, accuracy));
+ log_debug(" = <%s>", t);
+
+ assert_se(parse_sec(t, &y) >= 0);
+ log_debug(" = "USEC_FMT, y);
+
+ if (accuracy <= 0)
+ accuracy = 1;
+
+ assert_se(x / accuracy == y / accuracy);
+}
+
+static void test_format_timespan_accuracy(usec_t accuracy) {
+ log_info("/* %s accuracy="USEC_FMT" */", __func__, accuracy);
+
+ test_format_timespan_one(0, accuracy);
+ test_format_timespan_one(1, accuracy);
+ test_format_timespan_one(1*USEC_PER_SEC, accuracy);
+ test_format_timespan_one(999*USEC_PER_MSEC, accuracy);
+ test_format_timespan_one(1234567, accuracy);
+ test_format_timespan_one(12, accuracy);
+ test_format_timespan_one(123, accuracy);
+ test_format_timespan_one(1234, accuracy);
+ test_format_timespan_one(12345, accuracy);
+ test_format_timespan_one(123456, accuracy);
+ test_format_timespan_one(1234567, accuracy);
+ test_format_timespan_one(12345678, accuracy);
+ test_format_timespan_one(1200000, accuracy);
+ test_format_timespan_one(1230000, accuracy);
+ test_format_timespan_one(1234000, accuracy);
+ test_format_timespan_one(1234500, accuracy);
+ test_format_timespan_one(1234560, accuracy);
+ test_format_timespan_one(1234567, accuracy);
+ test_format_timespan_one(986087, accuracy);
+ test_format_timespan_one(500 * USEC_PER_MSEC, accuracy);
+ test_format_timespan_one(9*USEC_PER_YEAR/5 - 23, accuracy);
+ test_format_timespan_one(USEC_INFINITY, accuracy);
+}
+
+TEST(format_timespan) {
+ test_format_timespan_accuracy(1);
+ test_format_timespan_accuracy(USEC_PER_MSEC);
+ test_format_timespan_accuracy(USEC_PER_SEC);
+
+ /* See issue #23928. */
+ _cleanup_free_ char *buf = NULL;
+ assert_se(buf = new(char, 5));
+ assert_se(buf == format_timespan(buf, 5, 100005, 1000));
+}
+
+TEST(verify_timezone) {
+ assert_se(verify_timezone("Europe/Berlin", LOG_DEBUG) == 0);
+ assert_se(verify_timezone("Australia/Sydney", LOG_DEBUG) == 0);
+ assert_se(verify_timezone("Europe/Do not exist", LOG_DEBUG) == -EINVAL);
+ assert_se(verify_timezone("Europe/DoNotExist", LOG_DEBUG) == -ENOENT);
+ assert_se(verify_timezone("/DoNotExist", LOG_DEBUG) == -EINVAL);
+ assert_se(verify_timezone("DoNotExist/", LOG_DEBUG) == -EINVAL);
+}
+
+TEST(timezone_is_valid) {
+ assert_se(timezone_is_valid("Europe/Berlin", LOG_ERR));
+ assert_se(timezone_is_valid("Australia/Sydney", LOG_ERR));
+ assert_se(!timezone_is_valid("Europe/Do not exist", LOG_ERR));
+}
+
+TEST(get_timezones) {
+ _cleanup_strv_free_ char **zones = NULL;
+ int r;
+
+ r = get_timezones(&zones);
+ assert_se(r == 0);
+
+ STRV_FOREACH(zone, zones) {
+ r = verify_timezone(*zone, LOG_ERR);
+ log_debug_errno(r, "verify_timezone(\"%s\"): %m", *zone);
+ assert_se(r >= 0 || r == -ENOENT);
+ }
+}
+
+TEST(usec_add) {
+ assert_se(usec_add(0, 0) == 0);
+ assert_se(usec_add(1, 4) == 5);
+ assert_se(usec_add(USEC_INFINITY, 5) == USEC_INFINITY);
+ assert_se(usec_add(5, USEC_INFINITY) == USEC_INFINITY);
+ assert_se(usec_add(USEC_INFINITY-5, 2) == USEC_INFINITY-3);
+ assert_se(usec_add(USEC_INFINITY-2, 2) == USEC_INFINITY);
+ assert_se(usec_add(USEC_INFINITY-1, 2) == USEC_INFINITY);
+ assert_se(usec_add(USEC_INFINITY, 2) == USEC_INFINITY);
+}
+
+TEST(usec_sub_unsigned) {
+ assert_se(usec_sub_unsigned(0, 0) == 0);
+ assert_se(usec_sub_unsigned(0, 2) == 0);
+ assert_se(usec_sub_unsigned(0, USEC_INFINITY) == 0);
+ assert_se(usec_sub_unsigned(1, 0) == 1);
+ assert_se(usec_sub_unsigned(1, 1) == 0);
+ assert_se(usec_sub_unsigned(1, 2) == 0);
+ assert_se(usec_sub_unsigned(1, 3) == 0);
+ assert_se(usec_sub_unsigned(1, USEC_INFINITY) == 0);
+ assert_se(usec_sub_unsigned(USEC_INFINITY-1, 0) == USEC_INFINITY-1);
+ assert_se(usec_sub_unsigned(USEC_INFINITY-1, 1) == USEC_INFINITY-2);
+ assert_se(usec_sub_unsigned(USEC_INFINITY-1, 2) == USEC_INFINITY-3);
+ assert_se(usec_sub_unsigned(USEC_INFINITY-1, USEC_INFINITY-2) == 1);
+ assert_se(usec_sub_unsigned(USEC_INFINITY-1, USEC_INFINITY-1) == 0);
+ assert_se(usec_sub_unsigned(USEC_INFINITY-1, USEC_INFINITY) == 0);
+ assert_se(usec_sub_unsigned(USEC_INFINITY, 0) == USEC_INFINITY);
+ assert_se(usec_sub_unsigned(USEC_INFINITY, 1) == USEC_INFINITY);
+ assert_se(usec_sub_unsigned(USEC_INFINITY, 2) == USEC_INFINITY);
+ assert_se(usec_sub_unsigned(USEC_INFINITY, USEC_INFINITY) == USEC_INFINITY);
+}
+
+TEST(usec_sub_signed) {
+ assert_se(usec_sub_signed(0, 0) == 0);
+ assert_se(usec_sub_signed(4, 1) == 3);
+ assert_se(usec_sub_signed(4, 4) == 0);
+ assert_se(usec_sub_signed(4, 5) == 0);
+
+ assert_se(usec_sub_signed(USEC_INFINITY-3, -3) == USEC_INFINITY);
+ assert_se(usec_sub_signed(USEC_INFINITY-3, -4) == USEC_INFINITY);
+ assert_se(usec_sub_signed(USEC_INFINITY-3, -5) == USEC_INFINITY);
+ assert_se(usec_sub_signed(USEC_INFINITY, 5) == USEC_INFINITY);
+
+ assert_se(usec_sub_signed(0, INT64_MAX) == 0);
+ assert_se(usec_sub_signed(0, -INT64_MAX) == INT64_MAX);
+ assert_se(usec_sub_signed(0, INT64_MIN) == (usec_t) INT64_MAX + 1);
+ assert_se(usec_sub_signed(0, -(INT64_MIN+1)) == 0);
+
+ assert_se(usec_sub_signed(USEC_INFINITY, INT64_MAX) == USEC_INFINITY);
+ assert_se(usec_sub_signed(USEC_INFINITY, -INT64_MAX) == USEC_INFINITY);
+ assert_se(usec_sub_signed(USEC_INFINITY, INT64_MIN) == USEC_INFINITY);
+ assert_se(usec_sub_signed(USEC_INFINITY, -(INT64_MIN+1)) == USEC_INFINITY);
+
+ assert_se(usec_sub_signed(USEC_INFINITY-1, INT64_MAX) == USEC_INFINITY-1-INT64_MAX);
+ assert_se(usec_sub_signed(USEC_INFINITY-1, -INT64_MAX) == USEC_INFINITY);
+ assert_se(usec_sub_signed(USEC_INFINITY-1, INT64_MIN) == USEC_INFINITY);
+ assert_se(usec_sub_signed(USEC_INFINITY-1, -(INT64_MIN+1)) == USEC_INFINITY-1-((usec_t) (-(INT64_MIN+1))));
+}
+
+TEST(format_timestamp) {
+ for (unsigned i = 0; i < TRIAL; i++) {
+ char buf[CONST_MAX(FORMAT_TIMESTAMP_MAX, FORMAT_TIMESPAN_MAX)];
+ usec_t x, y;
+
+ x = random_u64_range(USEC_TIMESTAMP_FORMATTABLE_MAX - USEC_PER_SEC) + USEC_PER_SEC;
+
+ assert_se(format_timestamp(buf, sizeof(buf), x));
+ log_debug("%s", buf);
+ assert_se(parse_timestamp(buf, &y) >= 0);
+ assert_se(x / USEC_PER_SEC == y / USEC_PER_SEC);
+
+ assert_se(format_timestamp_style(buf, sizeof(buf), x, TIMESTAMP_UNIX));
+ log_debug("%s", buf);
+ assert_se(parse_timestamp(buf, &y) >= 0);
+ assert_se(x / USEC_PER_SEC == y / USEC_PER_SEC);
+
+ assert_se(format_timestamp_style(buf, sizeof(buf), x, TIMESTAMP_UTC));
+ log_debug("%s", buf);
+ assert_se(parse_timestamp(buf, &y) >= 0);
+ assert_se(x / USEC_PER_SEC == y / USEC_PER_SEC);
+
+ assert_se(format_timestamp_style(buf, sizeof(buf), x, TIMESTAMP_US));
+ log_debug("%s", buf);
+ assert_se(parse_timestamp(buf, &y) >= 0);
+ assert_se(x == y);
+
+ assert_se(format_timestamp_style(buf, sizeof(buf), x, TIMESTAMP_US_UTC));
+ log_debug("%s", buf);
+ assert_se(parse_timestamp(buf, &y) >= 0);
+ assert_se(x == y);
+
+ if (x > 2 * USEC_PER_DAY) {
+ assert_se(format_timestamp_style(buf, sizeof(buf), x, TIMESTAMP_DATE));
+ log_debug("%s", buf);
+ assert_se(parse_timestamp(buf, &y) >= 0);
+ assert_se(y > usec_sub_unsigned(x, 2 * USEC_PER_DAY) && y < usec_add(x, 2 * USEC_PER_DAY));
+ }
+
+ assert_se(format_timestamp_relative(buf, sizeof(buf), x));
+ log_debug("%s", buf);
+ assert_se(parse_timestamp(buf, &y) >= 0);
+
+ /* The two calls above will run with a slightly different local time. Make sure we are in the same
+ * range however, but give enough leeway that this is unlikely to explode. And of course,
+ * format_timestamp_relative() scales the accuracy with the distance from the current time up to one
+ * month, cover for that too. */
+ assert_se(y > x ? y - x : x - y <= USEC_PER_MONTH + USEC_PER_DAY);
+ }
+}
+
+static void test_format_timestamp_impl(usec_t x) {
+ bool success, override;
+ const char *xx, *yy;
+ usec_t y;
+
+ xx = FORMAT_TIMESTAMP(x);
+ assert_se(xx);
+ assert_se(parse_timestamp(xx, &y) >= 0);
+ yy = FORMAT_TIMESTAMP(y);
+ assert_se(yy);
+
+ success = (x / USEC_PER_SEC == y / USEC_PER_SEC) && streq(xx, yy);
+ /* Workaround for https://github.com/systemd/systemd/issues/28472 */
+ override = !success &&
+ (STRPTR_IN_SET(tzname[0], "CAT", "EAT") ||
+ STRPTR_IN_SET(tzname[1], "CAT", "EAT")) &&
+ DIV_ROUND_UP(y - x, USEC_PER_SEC) == 3600; /* 1 hour, ignore fractional second */
+ log_full(success ? LOG_DEBUG : override ? LOG_WARNING : LOG_ERR,
+ "@" USEC_FMT " → %s → @" USEC_FMT " → %s%s",
+ x, xx, y, yy,
+ override ? ", ignoring." : "");
+ if (!override) {
+ assert_se(x / USEC_PER_SEC == y / USEC_PER_SEC);
+ assert_se(streq(xx, yy));
+ }
+}
+
+static void test_format_timestamp_loop(void) {
+ test_format_timestamp_impl(USEC_PER_SEC);
+ test_format_timestamp_impl(USEC_TIMESTAMP_FORMATTABLE_MAX_32BIT-1);
+ test_format_timestamp_impl(USEC_TIMESTAMP_FORMATTABLE_MAX_32BIT);
+ test_format_timestamp_impl(USEC_TIMESTAMP_FORMATTABLE_MAX-1);
+ test_format_timestamp_impl(USEC_TIMESTAMP_FORMATTABLE_MAX);
+
+ /* Two cases which trigger https://github.com/systemd/systemd/issues/28472 */
+ test_format_timestamp_impl(1504938962980066);
+ test_format_timestamp_impl(1509482094632752);
+
+ for (unsigned i = 0; i < TRIAL; i++) {
+ usec_t x;
+
+ x = random_u64_range(USEC_TIMESTAMP_FORMATTABLE_MAX - USEC_PER_SEC) + USEC_PER_SEC;
+ test_format_timestamp_impl(x);
+ }
+}
+
+TEST(FORMAT_TIMESTAMP) {
+ test_format_timestamp_loop();
+}
+
+static void test_format_timestamp_with_tz_one(const char *tz) {
+ const char *saved_tz, *colon_tz;
+
+ if (!timezone_is_valid(tz, LOG_DEBUG))
+ return;
+
+ log_info("/* %s(%s) */", __func__, tz);
+
+ saved_tz = getenv("TZ");
+
+ assert_se(colon_tz = strjoina(":", tz));
+ assert_se(setenv("TZ", colon_tz, 1) >= 0);
+ tzset();
+ log_debug("%s: tzname[0]=%s, tzname[1]=%s", tz, strempty(tzname[0]), strempty(tzname[1]));
+
+ test_format_timestamp_loop();
+
+ assert_se(set_unset_env("TZ", saved_tz, true) == 0);
+ tzset();
+}
+
+TEST(FORMAT_TIMESTAMP_with_tz) {
+ _cleanup_strv_free_ char **timezones = NULL;
+
+ test_format_timestamp_with_tz_one("UTC");
+
+ if (!slow_tests_enabled())
+ return (void) log_tests_skipped("slow tests are disabled");
+
+ assert_se(get_timezones(&timezones) >= 0);
+ STRV_FOREACH(tz, timezones)
+ test_format_timestamp_with_tz_one(*tz);
+}
+
+TEST(format_timestamp_relative_full) {
+ char buf[CONST_MAX(FORMAT_TIMESTAMP_MAX, FORMAT_TIMESPAN_MAX)];
+ usec_t x;
+
+ /* Years and months */
+ x = now(CLOCK_REALTIME) - (1*USEC_PER_YEAR + 1*USEC_PER_MONTH);
+ assert_se(format_timestamp_relative_full(buf, sizeof(buf), x, CLOCK_REALTIME, true));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "1 year 1 month ago"));
+
+ x = now(CLOCK_MONOTONIC) + (1*USEC_PER_YEAR + 1.5*USEC_PER_MONTH);
+ assert_se(format_timestamp_relative_full(buf, sizeof(buf), x, CLOCK_MONOTONIC, false));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "1 year 1 month left"));
+
+ x = now(CLOCK_REALTIME) - (1*USEC_PER_YEAR + 2*USEC_PER_MONTH);
+ assert_se(format_timestamp_relative_full(buf, sizeof(buf), x, CLOCK_REALTIME, true));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "1 year 2 months ago"));
+
+ x = now(CLOCK_REALTIME) - (2*USEC_PER_YEAR + 1*USEC_PER_MONTH);
+ assert_se(format_timestamp_relative_full(buf, sizeof(buf), x, CLOCK_REALTIME, true));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "2 years 1 month ago"));
+
+ x = now(CLOCK_REALTIME) - (2*USEC_PER_YEAR + 2*USEC_PER_MONTH);
+ assert_se(format_timestamp_relative_full(buf, sizeof(buf), x, CLOCK_REALTIME, true));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "2 years 2 months ago"));
+
+ /* Months and days */
+ x = now(CLOCK_REALTIME) - (1*USEC_PER_MONTH + 1*USEC_PER_DAY);
+ assert_se(format_timestamp_relative_full(buf, sizeof(buf), x, CLOCK_REALTIME, true));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "1 month 1 day ago"));
+
+ x = now(CLOCK_REALTIME) - (1*USEC_PER_MONTH + 2*USEC_PER_DAY);
+ assert_se(format_timestamp_relative_full(buf, sizeof(buf), x, CLOCK_REALTIME, true));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "1 month 2 days ago"));
+
+ x = now(CLOCK_REALTIME) - (2*USEC_PER_MONTH + 1*USEC_PER_DAY);
+ assert_se(format_timestamp_relative_full(buf, sizeof(buf), x, CLOCK_REALTIME, true));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "2 months 1 day ago"));
+
+ x = now(CLOCK_REALTIME) - (2*USEC_PER_MONTH + 2*USEC_PER_DAY);
+ assert_se(format_timestamp_relative_full(buf, sizeof(buf), x, CLOCK_REALTIME, true));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "2 months 2 days ago"));
+
+ /* Weeks and days */
+ x = now(CLOCK_REALTIME) - (1*USEC_PER_WEEK + 1*USEC_PER_DAY);
+ assert_se(format_timestamp_relative_full(buf, sizeof(buf), x, CLOCK_REALTIME, true));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "1 week 1 day ago"));
+
+ x = now(CLOCK_REALTIME) - (1*USEC_PER_WEEK + 2*USEC_PER_DAY);
+ assert_se(format_timestamp_relative_full(buf, sizeof(buf), x, CLOCK_REALTIME, true));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "1 week 2 days ago"));
+
+ x = now(CLOCK_REALTIME) - (2*USEC_PER_WEEK + 1*USEC_PER_DAY);
+ assert_se(format_timestamp_relative_full(buf, sizeof(buf), x, CLOCK_REALTIME, true));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "2 weeks 1 day ago"));
+
+ x = now(CLOCK_REALTIME) - (2*USEC_PER_WEEK + 2*USEC_PER_DAY);
+ assert_se(format_timestamp_relative_full(buf, sizeof(buf), x, CLOCK_REALTIME, true));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "2 weeks 2 days ago"));
+}
+
+TEST(format_timestamp_relative) {
+ char buf[CONST_MAX(FORMAT_TIMESTAMP_MAX, FORMAT_TIMESPAN_MAX)];
+ usec_t x;
+
+ /* Only testing timestamps in the past so we don't need to add some delta to account for time passing
+ * by while we are running the tests (unless we're running on potatoes and 24 hours somehow passes
+ * between our call to now() and format_timestamp_relative's call to now()). */
+
+ /* Years and months */
+ x = now(CLOCK_REALTIME) - (1*USEC_PER_YEAR + 1*USEC_PER_MONTH);
+ assert_se(format_timestamp_relative(buf, sizeof(buf), x));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "1 year 1 month ago"));
+
+ x = now(CLOCK_REALTIME) - (1*USEC_PER_YEAR + 2*USEC_PER_MONTH);
+ assert_se(format_timestamp_relative(buf, sizeof(buf), x));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "1 year 2 months ago"));
+
+ x = now(CLOCK_REALTIME) - (2*USEC_PER_YEAR + 1*USEC_PER_MONTH);
+ assert_se(format_timestamp_relative(buf, sizeof(buf), x));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "2 years 1 month ago"));
+
+ x = now(CLOCK_REALTIME) - (2*USEC_PER_YEAR + 2*USEC_PER_MONTH);
+ assert_se(format_timestamp_relative(buf, sizeof(buf), x));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "2 years 2 months ago"));
+
+ /* Months and days */
+ x = now(CLOCK_REALTIME) - (1*USEC_PER_MONTH + 1*USEC_PER_DAY);
+ assert_se(format_timestamp_relative(buf, sizeof(buf), x));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "1 month 1 day ago"));
+
+ x = now(CLOCK_REALTIME) - (1*USEC_PER_MONTH + 2*USEC_PER_DAY);
+ assert_se(format_timestamp_relative(buf, sizeof(buf), x));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "1 month 2 days ago"));
+
+ x = now(CLOCK_REALTIME) - (2*USEC_PER_MONTH + 1*USEC_PER_DAY);
+ assert_se(format_timestamp_relative(buf, sizeof(buf), x));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "2 months 1 day ago"));
+
+ x = now(CLOCK_REALTIME) - (2*USEC_PER_MONTH + 2*USEC_PER_DAY);
+ assert_se(format_timestamp_relative(buf, sizeof(buf), x));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "2 months 2 days ago"));
+
+ /* Weeks and days */
+ x = now(CLOCK_REALTIME) - (1*USEC_PER_WEEK + 1*USEC_PER_DAY);
+ assert_se(format_timestamp_relative(buf, sizeof(buf), x));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "1 week 1 day ago"));
+
+ x = now(CLOCK_REALTIME) - (1*USEC_PER_WEEK + 2*USEC_PER_DAY);
+ assert_se(format_timestamp_relative(buf, sizeof(buf), x));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "1 week 2 days ago"));
+
+ x = now(CLOCK_REALTIME) - (2*USEC_PER_WEEK + 1*USEC_PER_DAY);
+ assert_se(format_timestamp_relative(buf, sizeof(buf), x));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "2 weeks 1 day ago"));
+
+ x = now(CLOCK_REALTIME) - (2*USEC_PER_WEEK + 2*USEC_PER_DAY);
+ assert_se(format_timestamp_relative(buf, sizeof(buf), x));
+ log_debug("%s", buf);
+ assert_se(streq(buf, "2 weeks 2 days ago"));
+}
+
+static void test_format_timestamp_one(usec_t val, TimestampStyle style, const char *result) {
+ char buf[FORMAT_TIMESTAMP_MAX];
+ const char *t;
+
+ t = format_timestamp_style(buf, sizeof(buf), val, style);
+ assert_se(streq_ptr(t, result));
+}
+
+TEST(format_timestamp_range) {
+ test_format_timestamp_one(0, TIMESTAMP_UTC, NULL);
+ test_format_timestamp_one(0, TIMESTAMP_DATE, NULL);
+ test_format_timestamp_one(0, TIMESTAMP_US_UTC, NULL);
+
+ test_format_timestamp_one(1, TIMESTAMP_UTC, "Thu 1970-01-01 00:00:00 UTC");
+ test_format_timestamp_one(1, TIMESTAMP_DATE, "Thu 1970-01-01");
+ test_format_timestamp_one(1, TIMESTAMP_US_UTC, "Thu 1970-01-01 00:00:00.000001 UTC");
+
+ test_format_timestamp_one(USEC_PER_SEC, TIMESTAMP_UTC, "Thu 1970-01-01 00:00:01 UTC");
+ test_format_timestamp_one(USEC_PER_SEC, TIMESTAMP_DATE, "Thu 1970-01-01");
+ test_format_timestamp_one(USEC_PER_SEC, TIMESTAMP_US_UTC, "Thu 1970-01-01 00:00:01.000000 UTC");
+
+#if SIZEOF_TIME_T == 8
+ test_format_timestamp_one(USEC_TIMESTAMP_FORMATTABLE_MAX, TIMESTAMP_UTC, "Thu 9999-12-30 23:59:59 UTC");
+ test_format_timestamp_one(USEC_TIMESTAMP_FORMATTABLE_MAX, TIMESTAMP_DATE, "Thu 9999-12-30");
+ test_format_timestamp_one(USEC_TIMESTAMP_FORMATTABLE_MAX + 1, TIMESTAMP_UTC, "--- XXXX-XX-XX XX:XX:XX UTC");
+ test_format_timestamp_one(USEC_TIMESTAMP_FORMATTABLE_MAX + 1, TIMESTAMP_US_UTC, "--- XXXX-XX-XX XX:XX:XX.XXXXXX UTC");
+ test_format_timestamp_one(USEC_TIMESTAMP_FORMATTABLE_MAX + 1, TIMESTAMP_DATE, "--- XXXX-XX-XX");
+#elif SIZEOF_TIME_T == 4
+ test_format_timestamp_one(USEC_TIMESTAMP_FORMATTABLE_MAX, TIMESTAMP_UTC, "Mon 2038-01-18 03:14:07 UTC");
+ test_format_timestamp_one(USEC_TIMESTAMP_FORMATTABLE_MAX, TIMESTAMP_DATE, "Mon 2038-01-18");
+ test_format_timestamp_one(USEC_TIMESTAMP_FORMATTABLE_MAX + 1, TIMESTAMP_UTC, "--- XXXX-XX-XX XX:XX:XX UTC");
+ test_format_timestamp_one(USEC_TIMESTAMP_FORMATTABLE_MAX + 1, TIMESTAMP_US_UTC, "--- XXXX-XX-XX XX:XX:XX.XXXXXX UTC");
+ test_format_timestamp_one(USEC_TIMESTAMP_FORMATTABLE_MAX + 1, TIMESTAMP_DATE, "--- XXXX-XX-XX");
+#endif
+
+ test_format_timestamp_one(USEC_INFINITY, TIMESTAMP_UTC, NULL);
+}
+
+static void test_parse_timestamp_one(const char *str, usec_t max_diff, usec_t expected) {
+ usec_t usec = USEC_INFINITY;
+ int r;
+
+ r = parse_timestamp(str, &usec);
+ log_debug("/* %s(%s): max_diff="USEC_FMT", expected="USEC_FMT", result="USEC_FMT" */", __func__, str, max_diff, expected, usec);
+ assert_se(r >= 0);
+ assert_se(usec >= expected);
+ assert_se(usec_sub_unsigned(usec, expected) <= max_diff);
+}
+
+static bool time_is_zero(usec_t usec) {
+ const char *s;
+
+ s = FORMAT_TIMESTAMP(usec);
+ return strstr(s, " 00:00:00 ");
+}
+
+static bool timezone_equal(usec_t today, usec_t target) {
+ const char *s, *t, *sz, *tz;
+
+ s = FORMAT_TIMESTAMP(today);
+ t = FORMAT_TIMESTAMP(target);
+ assert_se(sz = strrchr(s, ' '));
+ assert_se(tz = strrchr(t, ' '));
+ log_debug("%s("USEC_FMT", "USEC_FMT") -> %s, %s", __func__, today, target, s, t);
+ return streq(sz, tz);
+}
+
+static void test_parse_timestamp_impl(const char *tz) {
+ usec_t today, today2, now_usec;
+
+ /* Invalid: Ensure that systemctl reboot --when=show and --when=cancel
+ * will not result in ambiguities */
+ assert_se(parse_timestamp("show", NULL) == -EINVAL);
+ assert_se(parse_timestamp("cancel", NULL) == -EINVAL);
+
+ /* UTC */
+ test_parse_timestamp_one("Thu 1970-01-01 00:01 UTC", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("Thu 1970-01-01 00:00:01 UTC", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("Thu 1970-01-01 00:00:01.001 UTC", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("Thu 1970-01-01 00:00:01.0010 UTC", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("Thu 70-01-01 00:01 UTC", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("Thu 70-01-01 00:00:01 UTC", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("Thu 70-01-01 00:00:01.001 UTC", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("Thu 70-01-01 00:00:01.0010 UTC", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("1970-01-01 00:01 UTC", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("1970-01-01 00:00:01 UTC", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("1970-01-01 00:00:01.001 UTC", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("1970-01-01 00:00:01.0010 UTC", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("70-01-01 00:01 UTC", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("70-01-01 00:00:01 UTC", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("70-01-01 00:00:01.001 UTC", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("70-01-01 00:00:01.0010 UTC", 0, USEC_PER_SEC + 1000);
+
+ /* Examples from RFC3339 */
+ test_parse_timestamp_one("1985-04-12T23:20:50.52Z", 0, 482196050 * USEC_PER_SEC + 520000);
+ test_parse_timestamp_one("1996-12-19T16:39:57-08:00", 0, 851042397 * USEC_PER_SEC + 000000);
+ test_parse_timestamp_one("1996-12-20T00:39:57Z", 0, 851042397 * USEC_PER_SEC + 000000);
+ test_parse_timestamp_one("1990-12-31T23:59:60Z", 0, 662688000 * USEC_PER_SEC + 000000);
+ test_parse_timestamp_one("1990-12-31T15:59:60-08:00", 0, 662688000 * USEC_PER_SEC + 000000);
+ assert_se(parse_timestamp("1937-01-01T12:00:27.87+00:20", NULL) == -EINVAL); /* we don't support pre-epoch timestamps */
+ /* We accept timestamps without seconds as well */
+ test_parse_timestamp_one("1996-12-20T00:39Z", 0, (851042397 - 57) * USEC_PER_SEC + 000000);
+ test_parse_timestamp_one("1990-12-31T15:59-08:00", 0, (662688000-60) * USEC_PER_SEC + 000000);
+ /* We drop day-of-week before parsing the timestamp */
+ test_parse_timestamp_one("Thu 1970-01-01T00:01 UTC", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("Thu 1970-01-01T00:00:01 UTC", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("Thu 1970-01-01T00:01Z", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("Thu 1970-01-01T00:00:01Z", 0, USEC_PER_SEC);
+ /* RFC3339-style timezones can be welded to all formats */
+ assert_se(parse_timestamp("today UTC", &today) == 0);
+ assert_se(parse_timestamp("todayZ", &today2) == 0);
+ assert_se(today == today2);
+ assert_se(parse_timestamp("today +0200", &today) == 0);
+ assert_se(parse_timestamp("today+02:00", &today2) == 0);
+ assert_se(today == today2);
+
+ /* https://ijmacd.github.io/rfc3339-iso8601/ */
+ test_parse_timestamp_one("2023-09-06 12:49:27-00:00", 0, 1694004567 * USEC_PER_SEC + 000000);
+ test_parse_timestamp_one("2023-09-06 12:49:27.284-00:00", 0, 1694004567 * USEC_PER_SEC + 284000);
+ test_parse_timestamp_one("2023-09-06 12:49:27.284029Z", 0, 1694004567 * USEC_PER_SEC + 284029);
+ test_parse_timestamp_one("2023-09-06 12:49:27.284Z", 0, 1694004567 * USEC_PER_SEC + 284000);
+ test_parse_timestamp_one("2023-09-06 12:49:27.28Z", 0, 1694004567 * USEC_PER_SEC + 280000);
+ test_parse_timestamp_one("2023-09-06 12:49:27.2Z", 0, 1694004567 * USEC_PER_SEC + 200000);
+ test_parse_timestamp_one("2023-09-06 12:49:27Z", 0, 1694004567 * USEC_PER_SEC + 000000);
+ test_parse_timestamp_one("2023-09-06 14:49:27+02:00", 0, 1694004567 * USEC_PER_SEC + 000000);
+ test_parse_timestamp_one("2023-09-06 14:49:27.2+02:00", 0, 1694004567 * USEC_PER_SEC + 200000);
+ test_parse_timestamp_one("2023-09-06 14:49:27.28+02:00", 0, 1694004567 * USEC_PER_SEC + 280000);
+ test_parse_timestamp_one("2023-09-06 14:49:27.284+02:00", 0, 1694004567 * USEC_PER_SEC + 284000);
+ test_parse_timestamp_one("2023-09-06 14:49:27.284029+02:00", 0, 1694004567 * USEC_PER_SEC + 284029);
+ test_parse_timestamp_one("2023-09-06T12:49:27+00:00", 0, 1694004567 * USEC_PER_SEC + 000000);
+ test_parse_timestamp_one("2023-09-06T12:49:27-00:00", 0, 1694004567 * USEC_PER_SEC + 000000);
+ test_parse_timestamp_one("2023-09-06T12:49:27.284+00:00", 0, 1694004567 * USEC_PER_SEC + 284000);
+ test_parse_timestamp_one("2023-09-06T12:49:27.284-00:00", 0, 1694004567 * USEC_PER_SEC + 284000);
+ test_parse_timestamp_one("2023-09-06T12:49:27.284029Z", 0, 1694004567 * USEC_PER_SEC + 284029);
+ test_parse_timestamp_one("2023-09-06T12:49:27.284Z", 0, 1694004567 * USEC_PER_SEC + 284000);
+ test_parse_timestamp_one("2023-09-06T12:49:27.28Z", 0, 1694004567 * USEC_PER_SEC + 280000);
+ test_parse_timestamp_one("2023-09-06T12:49:27.2Z", 0, 1694004567 * USEC_PER_SEC + 200000);
+ test_parse_timestamp_one("2023-09-06T12:49:27Z", 0, 1694004567 * USEC_PER_SEC + 000000);
+ test_parse_timestamp_one("2023-09-06T14:49:27+02:00", 0, 1694004567 * USEC_PER_SEC + 000000);
+ test_parse_timestamp_one("2023-09-06T14:49:27.284+02:00", 0, 1694004567 * USEC_PER_SEC + 284000);
+ test_parse_timestamp_one("2023-09-06T14:49:27.284029+02:00", 0, 1694004567 * USEC_PER_SEC + 284029);
+ test_parse_timestamp_one("2023-09-06T21:34:27+08:45", 0, 1694004567 * USEC_PER_SEC + 000000);
+
+ if (timezone_is_valid("Asia/Tokyo", LOG_DEBUG)) {
+ /* Asia/Tokyo (+0900) */
+ test_parse_timestamp_one("Thu 1970-01-01 09:01 Asia/Tokyo", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("Thu 1970-01-01 09:00:01 Asia/Tokyo", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("Thu 1970-01-01 09:00:01.001 Asia/Tokyo", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("Thu 1970-01-01 09:00:01.0010 Asia/Tokyo", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("Thu 70-01-01 09:01 Asia/Tokyo", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("Thu 70-01-01 09:00:01 Asia/Tokyo", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("Thu 70-01-01 09:00:01.001 Asia/Tokyo", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("Thu 70-01-01 09:00:01.0010 Asia/Tokyo", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("1970-01-01 09:01 Asia/Tokyo", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("1970-01-01 09:00:01 Asia/Tokyo", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("1970-01-01 09:00:01.001 Asia/Tokyo", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("1970-01-01 09:00:01.0010 Asia/Tokyo", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("70-01-01 09:01 Asia/Tokyo", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("70-01-01 09:00:01 Asia/Tokyo", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("70-01-01 09:00:01.001 Asia/Tokyo", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("70-01-01 09:00:01.0010 Asia/Tokyo", 0, USEC_PER_SEC + 1000);
+ }
+
+ if (streq_ptr(tz, "Asia/Tokyo")) {
+ /* JST (+0900) */
+ test_parse_timestamp_one("Thu 1970-01-01 09:01 JST", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("Thu 1970-01-01 09:00:01 JST", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("Thu 1970-01-01 09:00:01.001 JST", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("Thu 1970-01-01 09:00:01.0010 JST", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("Thu 70-01-01 09:01 JST", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("Thu 70-01-01 09:00:01 JST", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("Thu 70-01-01 09:00:01.001 JST", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("Thu 70-01-01 09:00:01.0010 JST", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("1970-01-01 09:01 JST", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("1970-01-01 09:00:01 JST", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("1970-01-01 09:00:01.001 JST", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("1970-01-01 09:00:01.0010 JST", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("70-01-01 09:01 JST", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("70-01-01 09:00:01 JST", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("70-01-01 09:00:01.001 JST", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("70-01-01 09:00:01.0010 JST", 0, USEC_PER_SEC + 1000);
+ }
+
+ if (timezone_is_valid("America/New_York", LOG_DEBUG)) {
+ /* America/New_York (-0500) */
+ test_parse_timestamp_one("Wed 1969-12-31 19:01 America/New_York", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("Wed 1969-12-31 19:00:01 America/New_York", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("Wed 1969-12-31 19:00:01.001 America/New_York", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("Wed 1969-12-31 19:00:01.0010 America/New_York", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("Wed 69-12-31 19:01 America/New_York", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("Wed 69-12-31 19:00:01 America/New_York", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("Wed 69-12-31 19:00:01.001 America/New_York", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("Wed 69-12-31 19:00:01.0010 America/New_York", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("1969-12-31 19:01 America/New_York", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("1969-12-31 19:00:01 America/New_York", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("1969-12-31 19:00:01.001 America/New_York", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("1969-12-31 19:00:01.0010 America/New_York", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("69-12-31 19:01 America/New_York", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("69-12-31 19:00:01 America/New_York", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("69-12-31 19:00:01.001 America/New_York", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("69-12-31 19:00:01.0010 America/New_York", 0, USEC_PER_SEC + 1000);
+ }
+
+ if (streq_ptr(tz, "America/New_York")) {
+ /* EST (-0500) */
+ test_parse_timestamp_one("Wed 1969-12-31 19:01 EST", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("Wed 1969-12-31 19:00:01 EST", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("Wed 1969-12-31 19:00:01.001 EST", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("Wed 1969-12-31 19:00:01.0010 EST", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("Wed 69-12-31 19:01 EST", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("Wed 69-12-31 19:00:01 EST", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("Wed 69-12-31 19:00:01.001 EST", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("Wed 69-12-31 19:00:01.0010 EST", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("1969-12-31 19:01 EST", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("1969-12-31 19:00:01 EST", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("1969-12-31 19:00:01.001 EST", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("1969-12-31 19:00:01.0010 EST", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("69-12-31 19:01 EST", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("69-12-31 19:00:01 EST", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("69-12-31 19:00:01.001 EST", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("69-12-31 19:00:01.0010 EST", 0, USEC_PER_SEC + 1000);
+ }
+
+ /* -06 */
+ test_parse_timestamp_one("Wed 1969-12-31 18:01 -06", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("Wed 1969-12-31 18:00:01 -06", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("Wed 1969-12-31 18:00:01.001 -06", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("Wed 1969-12-31 18:00:01.0010 -06", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("Wed 69-12-31 18:01 -06", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("Wed 69-12-31 18:00:01 -06", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("Wed 69-12-31 18:00:01.001 -06", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("Wed 69-12-31 18:00:01.0010 -06", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("1969-12-31 18:01 -06", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("1969-12-31 18:00:01 -06", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("1969-12-31 18:00:01.001 -06", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("1969-12-31 18:00:01.0010 -06", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("69-12-31 18:01 -06", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("69-12-31 18:00:01 -06", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("69-12-31 18:00:01.001 -06", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("69-12-31 18:00:01.0010 -06", 0, USEC_PER_SEC + 1000);
+
+ /* -0600 */
+ test_parse_timestamp_one("Wed 1969-12-31 18:01 -0600", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("Wed 1969-12-31 18:00:01 -0600", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("Wed 1969-12-31 18:00:01.001 -0600", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("Wed 1969-12-31 18:00:01.0010 -0600", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("Wed 69-12-31 18:01 -0600", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("Wed 69-12-31 18:00:01 -0600", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("Wed 69-12-31 18:00:01.001 -0600", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("Wed 69-12-31 18:00:01.0010 -0600", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("1969-12-31 18:01 -0600", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("1969-12-31 18:00:01 -0600", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("1969-12-31 18:00:01.001 -0600", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("1969-12-31 18:00:01.0010 -0600", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("69-12-31 18:01 -0600", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("69-12-31 18:00:01 -0600", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("69-12-31 18:00:01.001 -0600", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("69-12-31 18:00:01.0010 -0600", 0, USEC_PER_SEC + 1000);
+
+ /* -06:00 */
+ test_parse_timestamp_one("Wed 1969-12-31 18:01 -06:00", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("Wed 1969-12-31 18:00:01 -06:00", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("Wed 1969-12-31 18:00:01.001 -06:00", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("Wed 1969-12-31 18:00:01.0010 -06:00", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("Wed 69-12-31 18:01 -06:00", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("Wed 69-12-31 18:00:01 -06:00", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("Wed 69-12-31 18:00:01.001 -06:00", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("Wed 69-12-31 18:00:01.0010 -06:00", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("1969-12-31 18:01 -06:00", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("1969-12-31 18:00:01 -06:00", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("1969-12-31 18:00:01.001 -06:00", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("1969-12-31 18:00:01.0010 -06:00", 0, USEC_PER_SEC + 1000);
+
+ test_parse_timestamp_one("69-12-31 18:01 -06:00", 0, USEC_PER_MINUTE);
+ test_parse_timestamp_one("69-12-31 18:00:01 -06:00", 0, USEC_PER_SEC);
+ test_parse_timestamp_one("69-12-31 18:00:01.001 -06:00", 0, USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("69-12-31 18:00:01.0010 -06:00", 0, USEC_PER_SEC + 1000);
+
+ /* without date */
+ assert_se(parse_timestamp("today", &today) == 0);
+ if (time_is_zero(today)) {
+ test_parse_timestamp_one("00:01", 0, today + USEC_PER_MINUTE);
+ test_parse_timestamp_one("00:00:01", 0, today + USEC_PER_SEC);
+ test_parse_timestamp_one("00:00:01.001", 0, today + USEC_PER_SEC + 1000);
+ test_parse_timestamp_one("00:00:01.0010", 0, today + USEC_PER_SEC + 1000);
+
+ if (timezone_equal(today, today + USEC_PER_DAY) && time_is_zero(today + USEC_PER_DAY))
+ test_parse_timestamp_one("tomorrow", 0, today + USEC_PER_DAY);
+ if (timezone_equal(today, today - USEC_PER_DAY) && time_is_zero(today - USEC_PER_DAY))
+ test_parse_timestamp_one("yesterday", 0, today - USEC_PER_DAY);
+ }
+
+ /* relative */
+ assert_se(parse_timestamp("now", &now_usec) == 0);
+ test_parse_timestamp_one("+5hours", USEC_PER_MINUTE, now_usec + 5 * USEC_PER_HOUR);
+ if (now_usec >= 10 * USEC_PER_DAY)
+ test_parse_timestamp_one("-10days", USEC_PER_MINUTE, now_usec - 10 * USEC_PER_DAY);
+ test_parse_timestamp_one("2weeks left", USEC_PER_MINUTE, now_usec + 2 * USEC_PER_WEEK);
+ if (now_usec >= 30 * USEC_PER_MINUTE)
+ test_parse_timestamp_one("30minutes ago", USEC_PER_MINUTE, now_usec - 30 * USEC_PER_MINUTE);
+}
+
+TEST(parse_timestamp) {
+ test_parse_timestamp_impl(NULL);
+}
+
+static void test_parse_timestamp_with_tz_one(const char *tz) {
+ const char *saved_tz, *colon_tz;
+
+ if (!timezone_is_valid(tz, LOG_DEBUG))
+ return;
+
+ log_info("/* %s(%s) */", __func__, tz);
+
+ saved_tz = getenv("TZ");
+
+ assert_se(colon_tz = strjoina(":", tz));
+ assert_se(setenv("TZ", colon_tz, 1) >= 0);
+ tzset();
+ log_debug("%s: tzname[0]=%s, tzname[1]=%s", tz, strempty(tzname[0]), strempty(tzname[1]));
+
+ test_parse_timestamp_impl(tz);
+
+ assert_se(set_unset_env("TZ", saved_tz, true) == 0);
+ tzset();
+}
+
+TEST(parse_timestamp_with_tz) {
+ _cleanup_strv_free_ char **timezones = NULL;
+
+ test_parse_timestamp_with_tz_one("UTC");
+
+ if (!slow_tests_enabled())
+ return (void) log_tests_skipped("slow tests are disabled");
+
+ assert_se(get_timezones(&timezones) >= 0);
+ STRV_FOREACH(tz, timezones)
+ test_parse_timestamp_with_tz_one(*tz);
+}
+
+TEST(deserialize_dual_timestamp) {
+ int r;
+ dual_timestamp t;
+
+ r = deserialize_dual_timestamp("1234 5678", &t);
+ assert_se(r == 0);
+ assert_se(t.realtime == 1234);
+ assert_se(t.monotonic == 5678);
+
+ r = deserialize_dual_timestamp("1234x 5678", &t);
+ assert_se(r == -EINVAL);
+
+ r = deserialize_dual_timestamp("1234 5678y", &t);
+ assert_se(r == -EINVAL);
+
+ r = deserialize_dual_timestamp("-1234 5678", &t);
+ assert_se(r == -EINVAL);
+
+ r = deserialize_dual_timestamp("1234 -5678", &t);
+ assert_se(r == -EINVAL);
+
+ /* Check that output wasn't modified. */
+ assert_se(t.realtime == 1234);
+ assert_se(t.monotonic == 5678);
+
+ r = deserialize_dual_timestamp("+123 567", &t);
+ assert_se(r == 0);
+ assert_se(t.realtime == 123);
+ assert_se(t.monotonic == 567);
+
+ /* Check that we get "infinity" on overflow. */
+ r = deserialize_dual_timestamp("18446744073709551617 0", &t);
+ assert_se(r == 0);
+ assert_se(t.realtime == USEC_INFINITY);
+ assert_se(t.monotonic == 0);
+}
+
+static void assert_similar(usec_t a, usec_t b) {
+ usec_t d;
+
+ if (a > b)
+ d = a - b;
+ else
+ d = b - a;
+
+ assert_se(d < 10*USEC_PER_SEC);
+}
+
+TEST(usec_shift_clock) {
+ usec_t rt, mn, bt;
+
+ rt = now(CLOCK_REALTIME);
+ mn = now(CLOCK_MONOTONIC);
+ bt = now(CLOCK_BOOTTIME);
+
+ assert_se(usec_shift_clock(USEC_INFINITY, CLOCK_REALTIME, CLOCK_MONOTONIC) == USEC_INFINITY);
+
+ assert_similar(usec_shift_clock(rt + USEC_PER_HOUR, CLOCK_REALTIME, CLOCK_MONOTONIC), mn + USEC_PER_HOUR);
+ assert_similar(usec_shift_clock(rt + 2*USEC_PER_HOUR, CLOCK_REALTIME, CLOCK_BOOTTIME), bt + 2*USEC_PER_HOUR);
+ assert_se(usec_shift_clock(rt + 3*USEC_PER_HOUR, CLOCK_REALTIME, CLOCK_REALTIME_ALARM) == rt + 3*USEC_PER_HOUR);
+
+ assert_similar(usec_shift_clock(mn + 4*USEC_PER_HOUR, CLOCK_MONOTONIC, CLOCK_REALTIME_ALARM), rt + 4*USEC_PER_HOUR);
+ assert_similar(usec_shift_clock(mn + 5*USEC_PER_HOUR, CLOCK_MONOTONIC, CLOCK_BOOTTIME), bt + 5*USEC_PER_HOUR);
+ assert_se(usec_shift_clock(mn + 6*USEC_PER_HOUR, CLOCK_MONOTONIC, CLOCK_MONOTONIC) == mn + 6*USEC_PER_HOUR);
+
+ assert_similar(usec_shift_clock(bt + 7*USEC_PER_HOUR, CLOCK_BOOTTIME, CLOCK_MONOTONIC), mn + 7*USEC_PER_HOUR);
+ assert_similar(usec_shift_clock(bt + 8*USEC_PER_HOUR, CLOCK_BOOTTIME, CLOCK_REALTIME_ALARM), rt + 8*USEC_PER_HOUR);
+ assert_se(usec_shift_clock(bt + 9*USEC_PER_HOUR, CLOCK_BOOTTIME, CLOCK_BOOTTIME) == bt + 9*USEC_PER_HOUR);
+
+ if (mn > USEC_PER_MINUTE) {
+ assert_similar(usec_shift_clock(rt - 30 * USEC_PER_SEC, CLOCK_REALTIME_ALARM, CLOCK_MONOTONIC), mn - 30 * USEC_PER_SEC);
+ assert_similar(usec_shift_clock(rt - 50 * USEC_PER_SEC, CLOCK_REALTIME, CLOCK_BOOTTIME), bt - 50 * USEC_PER_SEC);
+ }
+}
+
+TEST(in_utc_timezone) {
+ const char *tz = getenv("TZ");
+
+ assert_se(setenv("TZ", ":UTC", 1) >= 0);
+ assert_se(in_utc_timezone());
+ assert_se(streq(tzname[0], "UTC"));
+ assert_se(streq(tzname[1], "UTC"));
+ assert_se(timezone == 0);
+ assert_se(daylight == 0);
+
+ assert_se(setenv("TZ", ":Europe/Berlin", 1) >= 0);
+ assert_se(!in_utc_timezone());
+ assert_se(streq(tzname[0], "CET"));
+ assert_se(streq(tzname[1], "CEST"));
+
+ assert_se(set_unset_env("TZ", tz, true) == 0);
+ tzset();
+}
+
+TEST(map_clock_usec) {
+ usec_t nowr, x, y, z;
+
+ x = nowr = now(CLOCK_REALTIME); /* right now */
+ y = map_clock_usec(x, CLOCK_REALTIME, CLOCK_MONOTONIC);
+ z = map_clock_usec(y, CLOCK_MONOTONIC, CLOCK_REALTIME);
+ /* Converting forth and back will introduce inaccuracies, since we cannot query both clocks atomically, but it should be small. Even on the slowest CI smaller than 1h */
+
+ assert_se((z > x ? z - x : x - z) < USEC_PER_HOUR);
+
+ assert_se(nowr < USEC_INFINITY - USEC_PER_DAY*7); /* overflow check */
+ x = nowr + USEC_PER_DAY*7; /* 1 week from now */
+ y = map_clock_usec(x, CLOCK_REALTIME, CLOCK_MONOTONIC);
+ assert_se(y > 0 && y < USEC_INFINITY);
+ z = map_clock_usec(y, CLOCK_MONOTONIC, CLOCK_REALTIME);
+ assert_se(z > 0 && z < USEC_INFINITY);
+ assert_se((z > x ? z - x : x - z) < USEC_PER_HOUR);
+
+ assert_se(nowr > USEC_PER_DAY * 7); /* underflow check */
+ x = nowr - USEC_PER_DAY*7; /* 1 week ago */
+ y = map_clock_usec(x, CLOCK_REALTIME, CLOCK_MONOTONIC);
+ if (y != 0) { /* might underflow if machine is not up long enough for the monotonic clock to be beyond 1w */
+ assert_se(y < USEC_INFINITY);
+ z = map_clock_usec(y, CLOCK_MONOTONIC, CLOCK_REALTIME);
+ assert_se(z > 0 && z < USEC_INFINITY);
+ assert_se((z > x ? z - x : x - z) < USEC_PER_HOUR);
+ }
+}
+
+static void test_timezone_offset_change_one(const char *utc, const char *pretty) {
+ usec_t x, y, z;
+ char *s;
+
+ assert_se(parse_timestamp(utc, &x) >= 0);
+
+ s = FORMAT_TIMESTAMP_STYLE(x, TIMESTAMP_UTC);
+ assert_se(parse_timestamp(s, &y) >= 0);
+ log_debug("%s -> " USEC_FMT " -> %s -> " USEC_FMT, utc, x, s, y);
+ assert_se(streq(s, utc));
+ assert_se(x == y);
+
+ assert_se(parse_timestamp(pretty, &y) >= 0);
+ s = FORMAT_TIMESTAMP_STYLE(y, TIMESTAMP_PRETTY);
+ assert_se(parse_timestamp(s, &z) >= 0);
+ log_debug("%s -> " USEC_FMT " -> %s -> " USEC_FMT, pretty, y, s, z);
+ assert_se(streq(s, pretty));
+ assert_se(x == y);
+ assert_se(x == z);
+}
+
+TEST(timezone_offset_change) {
+ const char *tz = getenv("TZ");
+
+ /* See issue #26370. */
+
+ if (timezone_is_valid("Africa/Casablanca", LOG_DEBUG)) {
+ assert_se(setenv("TZ", ":Africa/Casablanca", 1) >= 0);
+ tzset();
+ log_debug("Africa/Casablanca: tzname[0]=%s, tzname[1]=%s", strempty(tzname[0]), strempty(tzname[1]));
+
+ test_timezone_offset_change_one("Sun 2015-10-25 01:59:59 UTC", "Sun 2015-10-25 02:59:59 +01");
+ test_timezone_offset_change_one("Sun 2015-10-25 02:00:00 UTC", "Sun 2015-10-25 02:00:00 +00");
+ test_timezone_offset_change_one("Sun 2018-06-17 01:59:59 UTC", "Sun 2018-06-17 01:59:59 +00");
+ test_timezone_offset_change_one("Sun 2018-06-17 02:00:00 UTC", "Sun 2018-06-17 03:00:00 +01");
+ test_timezone_offset_change_one("Sun 2018-10-28 01:59:59 UTC", "Sun 2018-10-28 02:59:59 +01");
+ test_timezone_offset_change_one("Sun 2018-10-28 02:00:00 UTC", "Sun 2018-10-28 03:00:00 +01");
+ }
+
+ if (timezone_is_valid("Asia/Atyrau", LOG_DEBUG)) {
+ assert_se(setenv("TZ", ":Asia/Atyrau", 1) >= 0);
+ tzset();
+ log_debug("Asia/Atyrau: tzname[0]=%s, tzname[1]=%s", strempty(tzname[0]), strempty(tzname[1]));
+
+ test_timezone_offset_change_one("Sat 2004-03-27 21:59:59 UTC", "Sun 2004-03-28 01:59:59 +04");
+ test_timezone_offset_change_one("Sat 2004-03-27 22:00:00 UTC", "Sun 2004-03-28 03:00:00 +05");
+ test_timezone_offset_change_one("Sat 2004-10-30 21:59:59 UTC", "Sun 2004-10-31 02:59:59 +05");
+ test_timezone_offset_change_one("Sat 2004-10-30 22:00:00 UTC", "Sun 2004-10-31 03:00:00 +05");
+ }
+
+ if (timezone_is_valid("Chile/EasterIsland", LOG_DEBUG)) {
+ assert_se(setenv("TZ", ":Chile/EasterIsland", 1) >= 0);
+ tzset();
+ log_debug("Chile/EasterIsland: tzname[0]=%s, tzname[1]=%s", strempty(tzname[0]), strempty(tzname[1]));
+
+ test_timezone_offset_change_one("Sun 1981-10-11 03:59:59 UTC", "Sat 1981-10-10 20:59:59 -07");
+ test_timezone_offset_change_one("Sun 1981-10-11 04:00:00 UTC", "Sat 1981-10-10 22:00:00 -06");
+ test_timezone_offset_change_one("Sun 1982-03-14 02:59:59 UTC", "Sat 1982-03-13 20:59:59 -06");
+ test_timezone_offset_change_one("Sun 1982-03-14 03:00:00 UTC", "Sat 1982-03-13 21:00:00 -06");
+ }
+
+ assert_se(set_unset_env("TZ", tz, true) == 0);
+ tzset();
+}
+
+static int intro(void) {
+ /* Tests have hard-coded results that do not expect a specific timezone to be set by the caller */
+ assert_se(unsetenv("TZ") >= 0);
+
+ log_info("realtime=" USEC_FMT "\n"
+ "monotonic=" USEC_FMT "\n"
+ "boottime=" USEC_FMT "\n",
+ now(CLOCK_REALTIME),
+ now(CLOCK_MONOTONIC),
+ now(CLOCK_BOOTTIME));
+
+ /* Ensure time_t is signed */
+ assert_cc((time_t) -1 < (time_t) 1);
+
+ /* Ensure TIME_T_MAX works correctly */
+ uintmax_t x = TIME_T_MAX;
+ x++;
+ assert_se((time_t) x < 0);
+
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_INFO, intro);
diff --git a/src/test/test-tmpfile-util.c b/src/test/test-tmpfile-util.c
new file mode 100644
index 0000000..4859f62
--- /dev/null
+++ b/src/test/test-tmpfile-util.c
@@ -0,0 +1,306 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "format-util.h"
+#include "fs-util.h"
+#include "log.h"
+#include "path-util.h"
+#include "process-util.h"
+#include "string-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+static void test_tempfn_random_one(const char *p, const char *extra, const char *expect, int ret) {
+ _cleanup_free_ char *s = NULL;
+ int r;
+
+ r = tempfn_random(p, extra, &s);
+ log_info("%s+%s → %s vs. %s (%i/%s vs. %i/%s)",
+ p, strna(extra), strna(s), strna(expect),
+ r, STRERROR(r), ret, STRERROR(ret));
+
+ assert_se(!s == !expect);
+ if (s) {
+ const char *suffix;
+
+ assert_se(suffix = startswith(s, expect));
+ assert_se(in_charset(suffix, HEXDIGITS));
+ assert_se(strlen(suffix) == 16);
+ }
+ assert_se(ret == r);
+}
+
+TEST(tempfn_random) {
+ _cleanup_free_ char *dir = NULL, *p = NULL, *q = NULL;
+
+ test_tempfn_random_one("", NULL, NULL, -EINVAL);
+ test_tempfn_random_one(".", NULL, NULL, -EADDRNOTAVAIL);
+ test_tempfn_random_one("..", NULL, NULL, -EINVAL);
+ test_tempfn_random_one("/", NULL, NULL, -EADDRNOTAVAIL);
+ test_tempfn_random_one("foo", "hoge/aaa", NULL, -EINVAL);
+
+ test_tempfn_random_one("foo", NULL, ".#foo", 0);
+ test_tempfn_random_one("foo", "bar", ".#barfoo", 0);
+ test_tempfn_random_one("/tmp/foo", NULL, "/tmp/.#foo", 0);
+ test_tempfn_random_one("/tmp/foo", "bar", "/tmp/.#barfoo", 0);
+ test_tempfn_random_one("./foo", NULL, ".#foo", 0);
+ test_tempfn_random_one("./foo", "bar", ".#barfoo", 0);
+ test_tempfn_random_one("../foo", NULL, "../.#foo", 0);
+ test_tempfn_random_one("../foo", "bar", "../.#barfoo", 0);
+
+ test_tempfn_random_one("foo/", NULL, ".#foo", 0);
+ test_tempfn_random_one("foo/", "bar", ".#barfoo", 0);
+ test_tempfn_random_one("/tmp/foo/", NULL, "/tmp/.#foo", 0);
+ test_tempfn_random_one("/tmp/foo/", "bar", "/tmp/.#barfoo", 0);
+ test_tempfn_random_one("./foo/", NULL, ".#foo", 0);
+ test_tempfn_random_one("./foo/", "bar", ".#barfoo", 0);
+ test_tempfn_random_one("../foo/", NULL, "../.#foo", 0);
+ test_tempfn_random_one("../foo/", "bar", "../.#barfoo", 0);
+
+ assert_se(dir = new(char, PATH_MAX - 20));
+ memset(dir, 'x', PATH_MAX - 21);
+ dir[PATH_MAX - 21] = '\0';
+ for (size_t i = 0; i < PATH_MAX - 21; i += NAME_MAX + 1)
+ dir[i] = '/';
+
+ assert_se(p = path_join(dir, "a"));
+ assert_se(q = path_join(dir, ".#a"));
+
+ test_tempfn_random_one(p, NULL, q, 0);
+ test_tempfn_random_one(p, "b", NULL, -EINVAL);
+
+ p = mfree(p);
+ q = mfree(q);
+
+ assert_se(p = new(char, NAME_MAX + 1));
+ memset(p, 'x', NAME_MAX);
+ p[NAME_MAX] = '\0';
+
+ assert_se(q = new(char, NAME_MAX + 1));
+ memset(stpcpy(q, ".#"), 'x', NAME_MAX - STRLEN(".#") - 16);
+ q[NAME_MAX - 16] = '\0';
+
+ test_tempfn_random_one(p, NULL, q, 0);
+
+ memset(stpcpy(q, ".#hoge"), 'x', NAME_MAX - STRLEN(".#hoge") - 16);
+ q[NAME_MAX - 16] = '\0';
+
+ test_tempfn_random_one(p, "hoge", q, 0);
+}
+
+static void test_tempfn_xxxxxx_one(const char *p, const char *extra, const char *expect, int ret) {
+ _cleanup_free_ char *s = NULL;
+ int r;
+
+ r = tempfn_xxxxxx(p, extra, &s);
+ log_info("%s+%s → %s vs. %s (%i/%s vs. %i/%s)",
+ p, strna(extra), strna(s), strna(expect),
+ r, STRERROR(r), ret, STRERROR(ret));
+
+ assert_se(!s == !expect);
+ if (s) {
+ const char *suffix;
+
+ assert_se(suffix = startswith(s, expect));
+ assert_se(streq(suffix, "XXXXXX"));
+ }
+ assert_se(ret == r);
+}
+
+TEST(tempfn_xxxxxx) {
+ _cleanup_free_ char *dir = NULL, *p = NULL, *q = NULL;
+
+ test_tempfn_xxxxxx_one("", NULL, NULL, -EINVAL);
+ test_tempfn_xxxxxx_one(".", NULL, NULL, -EADDRNOTAVAIL);
+ test_tempfn_xxxxxx_one("..", NULL, NULL, -EINVAL);
+ test_tempfn_xxxxxx_one("/", NULL, NULL, -EADDRNOTAVAIL);
+ test_tempfn_xxxxxx_one("foo", "hoge/aaa", NULL, -EINVAL);
+
+ test_tempfn_xxxxxx_one("foo", NULL, ".#foo", 0);
+ test_tempfn_xxxxxx_one("foo", "bar", ".#barfoo", 0);
+ test_tempfn_xxxxxx_one("/tmp/foo", NULL, "/tmp/.#foo", 0);
+ test_tempfn_xxxxxx_one("/tmp/foo", "bar", "/tmp/.#barfoo", 0);
+ test_tempfn_xxxxxx_one("./foo", NULL, ".#foo", 0);
+ test_tempfn_xxxxxx_one("./foo", "bar", ".#barfoo", 0);
+ test_tempfn_xxxxxx_one("../foo", NULL, "../.#foo", 0);
+ test_tempfn_xxxxxx_one("../foo", "bar", "../.#barfoo", 0);
+
+ test_tempfn_xxxxxx_one("foo/", NULL, ".#foo", 0);
+ test_tempfn_xxxxxx_one("foo/", "bar", ".#barfoo", 0);
+ test_tempfn_xxxxxx_one("/tmp/foo/", NULL, "/tmp/.#foo", 0);
+ test_tempfn_xxxxxx_one("/tmp/foo/", "bar", "/tmp/.#barfoo", 0);
+ test_tempfn_xxxxxx_one("./foo/", NULL, ".#foo", 0);
+ test_tempfn_xxxxxx_one("./foo/", "bar", ".#barfoo", 0);
+ test_tempfn_xxxxxx_one("../foo/", NULL, "../.#foo", 0);
+ test_tempfn_xxxxxx_one("../foo/", "bar", "../.#barfoo", 0);
+
+ assert_se(dir = new(char, PATH_MAX - 10));
+ memset(dir, 'x', PATH_MAX - 11);
+ dir[PATH_MAX - 11] = '\0';
+ for (size_t i = 0; i < PATH_MAX - 11; i += NAME_MAX + 1)
+ dir[i] = '/';
+
+ assert_se(p = path_join(dir, "a"));
+ assert_se(q = path_join(dir, ".#a"));
+
+ test_tempfn_xxxxxx_one(p, NULL, q, 0);
+ test_tempfn_xxxxxx_one(p, "b", NULL, -EINVAL);
+
+ p = mfree(p);
+ q = mfree(q);
+
+ assert_se(p = new(char, NAME_MAX + 1));
+ memset(p, 'x', NAME_MAX);
+ p[NAME_MAX] = '\0';
+
+ assert_se(q = new(char, NAME_MAX + 1));
+ memset(stpcpy(q, ".#"), 'x', NAME_MAX - STRLEN(".#") - 6);
+ q[NAME_MAX - 6] = '\0';
+
+ test_tempfn_xxxxxx_one(p, NULL, q, 0);
+
+ memset(stpcpy(q, ".#hoge"), 'x', NAME_MAX - STRLEN(".#hoge") - 6);
+ q[NAME_MAX - 6] = '\0';
+
+ test_tempfn_xxxxxx_one(p, "hoge", q, 0);
+}
+
+static void test_tempfn_random_child_one(const char *p, const char *extra, const char *expect, int ret) {
+ _cleanup_free_ char *s = NULL;
+ int r;
+
+ r = tempfn_random_child(p, extra, &s);
+ log_info_errno(r, "%s+%s → %s vs. %s (%i/%s vs. %i/%s)",
+ p, strna(extra), strna(s), strna(expect),
+ r, STRERROR(r), ret, STRERROR(ret));
+
+ assert_se(!s == !expect);
+ if (s) {
+ const char *suffix;
+
+ assert_se(suffix = startswith(s, expect));
+ assert_se(in_charset(suffix, HEXDIGITS));
+ assert_se(strlen(suffix) == 16);
+ }
+ assert_se(ret == r);
+}
+
+TEST(tempfn_random_child) {
+ _cleanup_free_ char *dir = NULL, *p = NULL, *q = NULL;
+
+ test_tempfn_random_child_one("", NULL, ".#", 0);
+ test_tempfn_random_child_one(".", NULL, ".#", 0);
+ test_tempfn_random_child_one("..", NULL, "../.#", 0);
+ test_tempfn_random_child_one("/", NULL, "/.#", 0);
+ test_tempfn_random_child_one("foo", "hoge/aaa", NULL, -EINVAL);
+
+ test_tempfn_random_child_one("foo", NULL, "foo/.#", 0);
+ test_tempfn_random_child_one("foo", "bar", "foo/.#bar", 0);
+ test_tempfn_random_child_one("/tmp/foo", NULL, "/tmp/foo/.#", 0);
+ test_tempfn_random_child_one("/tmp/foo", "bar", "/tmp/foo/.#bar", 0);
+ test_tempfn_random_child_one("./foo", NULL, "foo/.#", 0);
+ test_tempfn_random_child_one("./foo", "bar", "foo/.#bar", 0);
+ test_tempfn_random_child_one("../foo", NULL, "../foo/.#", 0);
+ test_tempfn_random_child_one("../foo", "bar", "../foo/.#bar", 0);
+
+ test_tempfn_random_child_one("foo/", NULL, "foo/.#", 0);
+ test_tempfn_random_child_one("foo/", "bar", "foo/.#bar", 0);
+ test_tempfn_random_child_one("/tmp/foo/", NULL, "/tmp/foo/.#", 0);
+ test_tempfn_random_child_one("/tmp/foo/", "bar", "/tmp/foo/.#bar", 0);
+ test_tempfn_random_child_one("./foo/", NULL, "foo/.#", 0);
+ test_tempfn_random_child_one("./foo/", "bar", "foo/.#bar", 0);
+ test_tempfn_random_child_one("../foo/", NULL, "../foo/.#", 0);
+ test_tempfn_random_child_one("../foo/", "bar", "../foo/.#bar", 0);
+
+ assert_se(dir = new(char, PATH_MAX - 21));
+ memset(dir, 'x', PATH_MAX - 22);
+ dir[PATH_MAX - 22] = '\0';
+ for (size_t i = 0; i < PATH_MAX - 22; i += NAME_MAX + 1)
+ dir[i] = '/';
+
+ assert_se(p = path_join(dir, "a"));
+ assert_se(q = path_join(p, ".#"));
+
+ test_tempfn_random_child_one(p, NULL, q, 0);
+ test_tempfn_random_child_one(p, "b", NULL, -EINVAL);
+
+ p = mfree(p);
+ q = mfree(q);
+
+ assert_se(p = new(char, NAME_MAX + 1));
+ memset(p, 'x', NAME_MAX);
+ p[NAME_MAX] = '\0';
+
+ assert_se(q = path_join(p, ".#"));
+
+ test_tempfn_random_child_one(p, NULL, q, 0);
+
+ assert_se(strextend(&q, "hoge"));
+ test_tempfn_random_child_one(p, "hoge", q, 0);
+}
+
+TEST(link_tmpfile) {
+ _cleanup_free_ char *cmd = NULL, *cmd2 = NULL, *ans = NULL, *ans2 = NULL, *d = NULL, *tmp = NULL, *line = NULL;
+ _cleanup_close_ int fd = -EBADF, fd2 = -EBADF;
+ const char *p = saved_argv[1] ?: "/tmp";
+ char *pattern;
+
+ pattern = strjoina(p, "/systemd-test-XXXXXX");
+
+ fd = open_tmpfile_unlinkable(p, O_RDWR|O_CLOEXEC);
+ assert_se(fd >= 0);
+
+ assert_se(asprintf(&cmd, "ls -l /proc/"PID_FMT"/fd/%d", getpid_cached(), fd) > 0);
+ (void) system(cmd);
+ assert_se(readlink_malloc(cmd + 6, &ans) >= 0);
+ log_debug("link1: %s", ans);
+ assert_se(endswith(ans, " (deleted)"));
+
+ fd2 = mkostemp_safe(pattern);
+ assert_se(fd2 >= 0);
+ assert_se(unlink(pattern) == 0);
+
+ assert_se(asprintf(&cmd2, "ls -l /proc/"PID_FMT"/fd/%d", getpid_cached(), fd2) > 0);
+ (void) system(cmd2);
+ assert_se(readlink_malloc(cmd2 + 6, &ans2) >= 0);
+ log_debug("link2: %s", ans2);
+ assert_se(endswith(ans2, " (deleted)"));
+
+ pattern = strjoina(p, "/tmpfiles-test");
+ assert_se(tempfn_random(pattern, NULL, &d) >= 0);
+
+ fd = safe_close(fd);
+ fd = open_tmpfile_linkable(d, O_RDWR|O_CLOEXEC, &tmp);
+ assert_se(fd >= 0);
+ assert_se(write(fd, "foobar\n", 7) == 7);
+
+ assert_se(touch(d) >= 0);
+ assert_se(link_tmpfile(fd, tmp, d, /* flags= */ 0) == -EEXIST);
+ assert_se(unlink(d) >= 0);
+ assert_se(link_tmpfile(fd, tmp, d, /* flags= */ 0) >= 0);
+
+ assert_se(read_one_line_file(d, &line) >= 0);
+ assert_se(streq(line, "foobar"));
+
+ fd = safe_close(fd);
+ tmp = mfree(tmp);
+
+ fd = open_tmpfile_linkable(d, O_RDWR|O_CLOEXEC, &tmp);
+ assert_se(fd >= 0);
+
+ assert_se(write(fd, "waumiau\n", 8) == 8);
+
+ assert_se(link_tmpfile(fd, tmp, d, /* flags= */ 0) == -EEXIST);
+ assert_se(link_tmpfile(fd, tmp, d, LINK_TMPFILE_REPLACE) >= 0);
+
+ line = mfree(line);
+ assert_se(read_one_line_file(d, &line) >= 0);
+ assert_se(streq(line, "waumiau"));
+
+ assert_se(unlink(d) >= 0);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-tpm2.c b/src/test/test-tpm2.c
new file mode 100644
index 0000000..e3c7da8
--- /dev/null
+++ b/src/test/test-tpm2.c
@@ -0,0 +1,1324 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "hexdecoct.h"
+#include "macro.h"
+#include "tests.h"
+#include "tpm2-util.h"
+#include "virt.h"
+
+TEST(tpm2_pcr_index_from_string) {
+ assert_se(tpm2_pcr_index_from_string("platform-code") == 0);
+ assert_se(tpm2_pcr_index_from_string("0") == 0);
+ assert_se(tpm2_pcr_index_from_string("platform-config") == 1);
+ assert_se(tpm2_pcr_index_from_string("1") == 1);
+ assert_se(tpm2_pcr_index_from_string("external-code") == 2);
+ assert_se(tpm2_pcr_index_from_string("2") == 2);
+ assert_se(tpm2_pcr_index_from_string("external-config") == 3);
+ assert_se(tpm2_pcr_index_from_string("3") == 3);
+ assert_se(tpm2_pcr_index_from_string("boot-loader-code") == 4);
+ assert_se(tpm2_pcr_index_from_string("4") == 4);
+ assert_se(tpm2_pcr_index_from_string("boot-loader-config") == 5);
+ assert_se(tpm2_pcr_index_from_string("5") == 5);
+ assert_se(tpm2_pcr_index_from_string("secure-boot-policy") == 7);
+ assert_se(tpm2_pcr_index_from_string("7") == 7);
+ assert_se(tpm2_pcr_index_from_string("kernel-initrd") == 9);
+ assert_se(tpm2_pcr_index_from_string("9") == 9);
+ assert_se(tpm2_pcr_index_from_string("ima") == 10);
+ assert_se(tpm2_pcr_index_from_string("10") == 10);
+ assert_se(tpm2_pcr_index_from_string("kernel-boot") == 11);
+ assert_se(tpm2_pcr_index_from_string("11") == 11);
+ assert_se(tpm2_pcr_index_from_string("kernel-config") == 12);
+ assert_se(tpm2_pcr_index_from_string("12") == 12);
+ assert_se(tpm2_pcr_index_from_string("sysexts") == 13);
+ assert_se(tpm2_pcr_index_from_string("13") == 13);
+ assert_se(tpm2_pcr_index_from_string("shim-policy") == 14);
+ assert_se(tpm2_pcr_index_from_string("14") == 14);
+ assert_se(tpm2_pcr_index_from_string("system-identity") == 15);
+ assert_se(tpm2_pcr_index_from_string("15") == 15);
+ assert_se(tpm2_pcr_index_from_string("debug") == 16);
+ assert_se(tpm2_pcr_index_from_string("16") == 16);
+ assert_se(tpm2_pcr_index_from_string("application-support") == 23);
+ assert_se(tpm2_pcr_index_from_string("23") == 23);
+ assert_se(tpm2_pcr_index_from_string("hello") == -EINVAL);
+ assert_se(tpm2_pcr_index_from_string("8") == 8);
+ assert_se(tpm2_pcr_index_from_string("44") == -EINVAL);
+ assert_se(tpm2_pcr_index_from_string("-5") == -EINVAL);
+ assert_se(tpm2_pcr_index_from_string("24") == -EINVAL);
+}
+
+TEST(tpm2_util_pbkdf2_hmac_sha256) {
+
+ /*
+ * The test vectors from RFC 6070 [1] are for dkLen of 20 as it's SHA1
+ * other RFCs I bumped into had various differing dkLen and iter counts,
+ * so this was generated using Python's hmacmodule.
+ *
+ * 1. https://www.rfc-editor.org/rfc/rfc6070.html#page-2
+ */
+ static const struct {
+ const uint8_t pass[256];
+ size_t passlen;
+ const uint8_t salt[256];
+ size_t saltlen;
+ uint8_t expected[SHA256_DIGEST_SIZE];
+ } test_vectors[] = {
+ { .pass={'f', 'o', 'o', 'p', 'a', 's', 's'}, .passlen=7, .salt={'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5'}, .saltlen=16, .expected={0xCB, 0xEA, 0x27, 0x23, 0x9A, 0x65, 0x99, 0xF6, 0x8C, 0x26, 0x54, 0x80, 0x5C, 0x63, 0x61, 0xD2, 0x91, 0x0A, 0x60, 0x3F, 0xC2, 0xF5, 0xF0, 0xAB, 0x55, 0x8B, 0x46, 0x07, 0x60, 0x93, 0xAB, 0xCB} },
+ { .pass={'f', 'o', 'o', 'p', 'a', 's', 's'}, .passlen=7, .salt={0x00, 'h', 'f', 's', 'd', 'j', 'h', 'f', 'd', 'j', 'h', 'j', 'd', 'f', 's'}, .saltlen=15, .expected={0x2B, 0xDF, 0x52, 0x29, 0x48, 0x3F, 0x98, 0x25, 0x01, 0x19, 0xB4, 0x42, 0xBC, 0xA7, 0x38, 0x5D, 0xCD, 0x08, 0xBD, 0xDC, 0x33, 0xBF, 0x32, 0x5E, 0x31, 0x87, 0x54, 0xFF, 0x2C, 0x23, 0x68, 0xFF} },
+ { .pass={'f', 'o', 'o', 'p', 'a', 's', 's'}, .passlen=7, .salt={'m', 'y', 's', 'a', 0x00, 'l', 't'}, .saltlen=7, .expected={0x7C, 0x24, 0xB4, 0x4D, 0x30, 0x11, 0x53, 0x24, 0x87, 0x56, 0x24, 0x10, 0xBA, 0x9F, 0xF2, 0x4E, 0xBB, 0xF5, 0x03, 0x56, 0x2B, 0xB1, 0xA1, 0x92, 0x8B, 0x5F, 0x32, 0x02, 0x23, 0x1F, 0x79, 0xE6} },
+ { .pass={'p', 'a', 's', 's', 'w', 'i', 't', 'h', 'n', 'u', 'l', 'l', 0x00, 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'}, .passlen=21, .salt={'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5'}, .saltlen=16, .expected={0xE9, 0x53, 0xB7, 0x1D, 0xAB, 0xD1, 0xC1, 0xF3, 0xC4, 0x7F, 0x18, 0x96, 0xDD, 0xD7, 0x6B, 0xC6, 0x6A, 0xBD, 0xFB, 0x12, 0x7C, 0xF8, 0x68, 0xDC, 0x6E, 0xEF, 0x29, 0xCC, 0x1B, 0x30, 0x5B, 0x74} },
+ { .pass={'p', 'a', 's', 's', 'w', 'i', 't', 'h', 'n', 'u', 'l', 'l', 0x00, 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'}, .passlen=21, .salt={0x00, 'h', 'f', 's', 'd', 'j', 'h', 'f', 'd', 'j', 'h', 'j', 'd', 'f', 's'}, .saltlen=15, .expected={0x51, 0xA3, 0x82, 0xA5, 0x2F, 0x48, 0x84, 0xB3, 0x02, 0x0D, 0xC2, 0x42, 0x9A, 0x8F, 0x86, 0xCC, 0x66, 0xFD, 0x65, 0x87, 0x89, 0x07, 0x2B, 0x07, 0x82, 0x42, 0xD6, 0x6D, 0x43, 0xB8, 0xFD, 0xCF} },
+ { .pass={'p', 'a', 's', 's', 'w', 'i', 't', 'h', 'n', 'u', 'l', 'l', 0x00, 'p', 'a', 's', 's', 'w', 'o', 'r', 'd'}, .passlen=21, .salt={'m', 'y', 's', 'a', 0x00, 'l', 't'}, .saltlen=7, .expected={0xEC, 0xFB, 0x5D, 0x5F, 0xF6, 0xA6, 0xE0, 0x79, 0x50, 0x64, 0x36, 0x64, 0xA3, 0x9A, 0x5C, 0xF3, 0x7A, 0x87, 0x0B, 0x64, 0x51, 0x59, 0x75, 0x64, 0x8B, 0x78, 0x2B, 0x62, 0x8F, 0x68, 0xD9, 0xCC} },
+ { .pass={0x00, 'p', 'a', 's', 's'}, .passlen=5, .salt={'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5'}, .saltlen=16, .expected={0x8A, 0x9A, 0x47, 0x9A, 0x91, 0x22, 0x2F, 0x56, 0x29, 0x4F, 0x26, 0x00, 0xE7, 0xB3, 0xEB, 0x63, 0x6D, 0x51, 0xF2, 0x60, 0x17, 0x08, 0x20, 0x70, 0x82, 0x8F, 0xA3, 0xD7, 0xBE, 0x2B, 0xD5, 0x5D} },
+ { .pass={0x00, 'p', 'a', 's', 's'}, .passlen=5, .salt={0x00, 'h', 'f', 's', 'd', 'j', 'h', 'f', 'd', 'j', 'h', 'j', 'd', 'f', 's'}, .saltlen=15, .expected={0x72, 0x3A, 0xF5, 0xF7, 0xCD, 0x6C, 0x12, 0xDD, 0x53, 0x28, 0x46, 0x0C, 0x19, 0x0E, 0xF2, 0x91, 0xDE, 0xEA, 0xF9, 0x6F, 0x74, 0x32, 0x34, 0x3F, 0x84, 0xED, 0x8D, 0x2A, 0xDE, 0xC9, 0xC6, 0x34} },
+ { .pass={0x00, 'p', 'a', 's', 's'}, .passlen=5, .salt={'m', 'y', 's', 'a', 0x00, 'l', 't'}, .saltlen=7, .expected={0xE3, 0x07, 0x12, 0xBE, 0xEE, 0xF5, 0x5D, 0x18, 0x72, 0xF4, 0xCF, 0xF1, 0x20, 0x6B, 0xD6, 0x66, 0xCD, 0x7C, 0xE7, 0x4F, 0xC2, 0x16, 0x70, 0x5B, 0x9B, 0x2F, 0x7D, 0xE2, 0x3B, 0x42, 0x3A, 0x1B} },
+ };
+
+ uint8_t res[SHA256_DIGEST_SIZE];
+ for(size_t i = 0; i < sizeof(test_vectors)/sizeof(test_vectors[0]); i++) {
+
+ int rc = tpm2_util_pbkdf2_hmac_sha256(
+ test_vectors[i].pass,
+ test_vectors[i].passlen,
+ test_vectors[i].salt,
+ test_vectors[i].saltlen,
+ res);
+ assert_se(rc == 0);
+ assert_se(memcmp(test_vectors[i].expected, res, SHA256_DIGEST_SIZE) == 0);
+ }
+}
+
+#if HAVE_TPM2
+
+#define POISON(type) \
+ ({ \
+ type _p; \
+ memset(&_p, 0xaa, sizeof(_p)); \
+ _p; \
+ })
+#define POISON_TPML POISON(TPML_PCR_SELECTION)
+#define POISON_TPMS POISON(TPMS_PCR_SELECTION)
+#define POISON_U32 POISON(uint32_t)
+
+static void assert_tpms_pcr_selection_eq(TPMS_PCR_SELECTION *a, TPMS_PCR_SELECTION *b) {
+ assert_se(a);
+ assert_se(b);
+
+ assert_se(a->hash == b->hash);
+ assert_se(a->sizeofSelect == b->sizeofSelect);
+
+ for (size_t i = 0; i < a->sizeofSelect; i++)
+ assert_se(a->pcrSelect[i] == b->pcrSelect[i]);
+}
+
+static void assert_tpml_pcr_selection_eq(TPML_PCR_SELECTION *a, TPML_PCR_SELECTION *b) {
+ assert_se(a);
+ assert_se(b);
+
+ assert_se(a->count == b->count);
+ for (size_t i = 0; i < a->count; i++)
+ assert_tpms_pcr_selection_eq(&a->pcrSelections[i], &b->pcrSelections[i]);
+}
+
+static void verify_tpms_pcr_selection(TPMS_PCR_SELECTION *s, uint32_t mask, TPMI_ALG_HASH hash) {
+ assert_se(s->hash == hash);
+ assert_se(s->sizeofSelect == 3);
+ assert_se(s->pcrSelect[0] == (mask & 0xff));
+ assert_se(s->pcrSelect[1] == ((mask >> 8) & 0xff));
+ assert_se(s->pcrSelect[2] == ((mask >> 16) & 0xff));
+ assert_se(s->pcrSelect[3] == 0);
+
+ assert_se(tpm2_tpms_pcr_selection_to_mask(s) == mask);
+}
+
+static void verify_tpml_pcr_selection(TPML_PCR_SELECTION *l, TPMS_PCR_SELECTION s[], size_t count) {
+ assert_se(l->count == count);
+ for (size_t i = 0; i < count; i++) {
+ assert_tpms_pcr_selection_eq(&s[i], &l->pcrSelections[i]);
+
+ TPMI_ALG_HASH hash = l->pcrSelections[i].hash;
+ verify_tpms_pcr_selection(&l->pcrSelections[i], tpm2_tpml_pcr_selection_to_mask(l, hash), hash);
+ }
+}
+
+static void _test_pcr_selection_mask_hash(uint32_t mask, TPMI_ALG_HASH hash) {
+ TPMS_PCR_SELECTION s = POISON_TPMS;
+ tpm2_tpms_pcr_selection_from_mask(mask, hash, &s);
+ verify_tpms_pcr_selection(&s, mask, hash);
+
+ TPML_PCR_SELECTION l = POISON_TPML;
+ tpm2_tpml_pcr_selection_from_mask(mask, hash, &l);
+ verify_tpml_pcr_selection(&l, &s, 1);
+ verify_tpms_pcr_selection(&l.pcrSelections[0], mask, hash);
+
+ uint32_t test_masks[] = {
+ 0x0, 0x1, 0x100, 0x10000, 0xf0f0f0, 0xaaaaaa, 0xffffff,
+ };
+ for (unsigned i = 0; i < ELEMENTSOF(test_masks); i++) {
+ uint32_t test_mask = test_masks[i];
+
+ TPMS_PCR_SELECTION a = POISON_TPMS, b = POISON_TPMS, test_s = POISON_TPMS;
+ tpm2_tpms_pcr_selection_from_mask(test_mask, hash, &test_s);
+
+ a = s;
+ b = test_s;
+ tpm2_tpms_pcr_selection_add(&a, &b);
+ verify_tpms_pcr_selection(&a, UPDATE_FLAG(mask, test_mask, true), hash);
+ verify_tpms_pcr_selection(&b, test_mask, hash);
+
+ a = s;
+ b = test_s;
+ tpm2_tpms_pcr_selection_sub(&a, &b);
+ verify_tpms_pcr_selection(&a, UPDATE_FLAG(mask, test_mask, false), hash);
+ verify_tpms_pcr_selection(&b, test_mask, hash);
+
+ a = s;
+ b = test_s;
+ tpm2_tpms_pcr_selection_move(&a, &b);
+ verify_tpms_pcr_selection(&a, UPDATE_FLAG(mask, test_mask, true), hash);
+ verify_tpms_pcr_selection(&b, 0, hash);
+ }
+}
+
+TEST(tpms_pcr_selection_mask_and_hash) {
+ TPMI_ALG_HASH HASH_ALGS[] = { TPM2_ALG_SHA1, TPM2_ALG_SHA256, };
+
+ for (unsigned i = 0; i < ELEMENTSOF(HASH_ALGS); i++)
+ for (uint32_t m2 = 0; m2 <= 0xffffff; m2 += 0x50000)
+ for (uint32_t m1 = 0; m1 <= 0xffff; m1 += 0x500)
+ for (uint32_t m0 = 0; m0 <= 0xff; m0 += 0x5)
+ _test_pcr_selection_mask_hash(m0 | m1 | m2, HASH_ALGS[i]);
+}
+
+static void _test_tpms_sw(
+ TPMI_ALG_HASH hash,
+ uint32_t mask,
+ const char *expected_str,
+ size_t expected_weight) {
+
+ TPMS_PCR_SELECTION s = POISON_TPMS;
+ tpm2_tpms_pcr_selection_from_mask(mask, hash, &s);
+
+ _cleanup_free_ char *tpms_str = tpm2_tpms_pcr_selection_to_string(&s);
+ assert_se(streq(tpms_str, expected_str));
+
+ assert_se(tpm2_tpms_pcr_selection_weight(&s) == expected_weight);
+ assert_se(tpm2_tpms_pcr_selection_is_empty(&s) == (expected_weight == 0));
+}
+
+TEST(tpms_pcr_selection_string_and_weight) {
+ TPMI_ALG_HASH sha1 = TPM2_ALG_SHA1, sha256 = TPM2_ALG_SHA256;
+
+ _test_tpms_sw(sha1, 0, "sha1()", 0);
+ _test_tpms_sw(sha1, 1, "sha1(0)", 1);
+ _test_tpms_sw(sha1, 0xf, "sha1(0+1+2+3)", 4);
+ _test_tpms_sw(sha1, 0x00ff00, "sha1(8+9+10+11+12+13+14+15)", 8);
+ _test_tpms_sw(sha1, 0xffffff, "sha1(0+1+2+3+4+5+6+7+8+9+10+11+12+13+14+15+16+17+18+19+20+21+22+23)", 24);
+ _test_tpms_sw(sha256, 0, "sha256()", 0);
+ _test_tpms_sw(sha256, 1, "sha256(0)", 1);
+ _test_tpms_sw(sha256, 7, "sha256(0+1+2)", 3);
+ _test_tpms_sw(sha256, 0xf00000, "sha256(20+21+22+23)", 4);
+ _test_tpms_sw(sha256, 0xffffff, "sha256(0+1+2+3+4+5+6+7+8+9+10+11+12+13+14+15+16+17+18+19+20+21+22+23)", 24);
+}
+
+static void _tpml_pcr_selection_add_tpms(TPMS_PCR_SELECTION s[], size_t count, TPML_PCR_SELECTION *ret) {
+ for (size_t i = 0; i < count; i++)
+ tpm2_tpml_pcr_selection_add_tpms_pcr_selection(ret, &s[i]);
+}
+
+static void _tpml_pcr_selection_sub_tpms(TPMS_PCR_SELECTION s[], size_t count, TPML_PCR_SELECTION *ret) {
+ for (size_t i = 0; i < count; i++)
+ tpm2_tpml_pcr_selection_sub_tpms_pcr_selection(ret, &s[i]);
+}
+
+static void _test_tpml_sw(
+ TPMS_PCR_SELECTION s[],
+ size_t count,
+ size_t expected_count,
+ const char *expected_str,
+ size_t expected_weight) {
+
+ TPML_PCR_SELECTION l = {};
+ _tpml_pcr_selection_add_tpms(s, count, &l);
+ assert_se(l.count == expected_count);
+
+ _cleanup_free_ char *tpml_str = tpm2_tpml_pcr_selection_to_string(&l);
+ assert_se(streq(tpml_str, expected_str));
+
+ assert_se(tpm2_tpml_pcr_selection_weight(&l) == expected_weight);
+ assert_se(tpm2_tpml_pcr_selection_is_empty(&l) == (expected_weight == 0));
+}
+
+TEST(tpml_pcr_selection_string_and_weight) {
+ size_t size = 0xaa;
+ TPMI_ALG_HASH sha1 = TPM2_ALG_SHA1,
+ sha256 = TPM2_ALG_SHA256,
+ sha384 = TPM2_ALG_SHA384,
+ sha512 = TPM2_ALG_SHA512;
+ TPMS_PCR_SELECTION s[4] = { POISON_TPMS, POISON_TPMS, POISON_TPMS, POISON_TPMS, };
+
+ size = 0;
+ tpm2_tpms_pcr_selection_from_mask(0x000002, sha1 , &s[size++]);
+ tpm2_tpms_pcr_selection_from_mask(0x0080f0, sha384, &s[size++]);
+ tpm2_tpms_pcr_selection_from_mask(0x010100, sha512, &s[size++]);
+ tpm2_tpms_pcr_selection_from_mask(0xff0000, sha256, &s[size++]);
+ _test_tpml_sw(s,
+ size,
+ /* expected_count= */ 4,
+ "[sha1(1),sha384(4+5+6+7+15),sha512(8+16),sha256(16+17+18+19+20+21+22+23)]",
+ /* expected_weight= */ 16);
+
+ size = 0;
+ tpm2_tpms_pcr_selection_from_mask(0x0403aa, sha512, &s[size++]);
+ tpm2_tpms_pcr_selection_from_mask(0x0080f0, sha256, &s[size++]);
+ _test_tpml_sw(s,
+ size,
+ /* expected_count= */ 2,
+ "[sha512(1+3+5+7+8+9+18),sha256(4+5+6+7+15)]",
+ /* expected_weight= */ 12);
+
+ size = 0;
+ /* Empty hashes should be ignored */
+ tpm2_tpms_pcr_selection_from_mask(0x0300ce, sha384, &s[size++]);
+ tpm2_tpms_pcr_selection_from_mask(0xffffff, sha512, &s[size++]);
+ tpm2_tpms_pcr_selection_from_mask(0x000000, sha1 , &s[size++]);
+ tpm2_tpms_pcr_selection_from_mask(0x330010, sha256, &s[size++]);
+ _test_tpml_sw(s,
+ size,
+ /* expected_count= */ 3,
+ "[sha384(1+2+3+6+7+16+17),sha512(0+1+2+3+4+5+6+7+8+9+10+11+12+13+14+15+16+17+18+19+20+21+22+23),sha256(4+16+17+20+21)]",
+ /* expected_weight= */ 36);
+
+ size = 0;
+ /* Verify same-hash entries are properly combined. */
+ tpm2_tpms_pcr_selection_from_mask(0x000001, sha1 , &s[size++]);
+ tpm2_tpms_pcr_selection_from_mask(0x000001, sha256, &s[size++]);
+ tpm2_tpms_pcr_selection_from_mask(0x000010, sha1 , &s[size++]);
+ tpm2_tpms_pcr_selection_from_mask(0x000010, sha256, &s[size++]);
+ _test_tpml_sw(s,
+ size,
+ /* expected_count= */ 2,
+ "[sha1(0+4),sha256(0+4)]",
+ /* expected_weight= */ 4);
+}
+
+/* Test tpml add/sub by changing the tpms individually */
+static void _test_tpml_addsub_tpms(
+ TPML_PCR_SELECTION *start,
+ TPMS_PCR_SELECTION add[],
+ size_t add_count,
+ TPMS_PCR_SELECTION expected1[],
+ size_t expected1_count,
+ TPMS_PCR_SELECTION sub[],
+ size_t sub_count,
+ TPMS_PCR_SELECTION expected2[],
+ size_t expected2_count) {
+
+ TPML_PCR_SELECTION l = *start;
+
+ _tpml_pcr_selection_add_tpms(add, add_count, &l);
+ verify_tpml_pcr_selection(&l, expected1, expected1_count);
+
+ _tpml_pcr_selection_sub_tpms(sub, sub_count, &l);
+ verify_tpml_pcr_selection(&l, expected2, expected2_count);
+}
+
+/* Test tpml add/sub by creating new tpmls */
+static void _test_tpml_addsub_tpml(
+ TPML_PCR_SELECTION *start,
+ TPMS_PCR_SELECTION add[],
+ size_t add_count,
+ TPMS_PCR_SELECTION expected1[],
+ size_t expected1_count,
+ TPMS_PCR_SELECTION sub[],
+ size_t sub_count,
+ TPMS_PCR_SELECTION expected2[],
+ size_t expected2_count) {
+
+ TPML_PCR_SELECTION l = {};
+ tpm2_tpml_pcr_selection_add(&l, start);
+ assert_tpml_pcr_selection_eq(&l, start);
+
+ TPML_PCR_SELECTION addl = {};
+ _tpml_pcr_selection_add_tpms(add, add_count, &addl);
+ tpm2_tpml_pcr_selection_add(&l, &addl);
+
+ TPML_PCR_SELECTION e1 = {};
+ _tpml_pcr_selection_add_tpms(expected1, expected1_count, &e1);
+ assert_tpml_pcr_selection_eq(&l, &e1);
+
+ TPML_PCR_SELECTION subl = {};
+ _tpml_pcr_selection_add_tpms(sub, sub_count, &subl);
+ tpm2_tpml_pcr_selection_sub(&l, &subl);
+
+ TPML_PCR_SELECTION e2 = {};
+ _tpml_pcr_selection_add_tpms(expected2, expected2_count, &e2);
+ assert_tpml_pcr_selection_eq(&l, &e2);
+}
+
+#define _test_tpml_addsub(...) \
+ ({ \
+ _test_tpml_addsub_tpms(__VA_ARGS__); \
+ _test_tpml_addsub_tpml(__VA_ARGS__); \
+ })
+
+TEST(tpml_pcr_selection_add_sub) {
+ size_t add_count = 0xaa, expected1_count = 0xaa, sub_count = 0xaa, expected2_count = 0xaa;
+ TPMI_ALG_HASH sha1 = TPM2_ALG_SHA1,
+ sha256 = TPM2_ALG_SHA256,
+ sha384 = TPM2_ALG_SHA384,
+ sha512 = TPM2_ALG_SHA512;
+ TPML_PCR_SELECTION l = POISON_TPML;
+ TPMS_PCR_SELECTION add[4] = { POISON_TPMS, POISON_TPMS, POISON_TPMS, POISON_TPMS, },
+ sub[4] = { POISON_TPMS, POISON_TPMS, POISON_TPMS, POISON_TPMS, },
+ expected1[4] = { POISON_TPMS, POISON_TPMS, POISON_TPMS, POISON_TPMS, },
+ expected2[4] = { POISON_TPMS, POISON_TPMS, POISON_TPMS, POISON_TPMS, };
+
+ l = (TPML_PCR_SELECTION){};
+ add_count = 0;
+ expected1_count = 0;
+ sub_count = 0;
+ expected2_count = 0;
+ tpm2_tpms_pcr_selection_from_mask(0x010101, sha256, &add[add_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0x101010, sha256, &add[add_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0x0000ff, sha512, &add[add_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0x111111, sha256, &expected1[expected1_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0x0000ff, sha512, &expected1[expected1_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0x000001, sha256, &sub[sub_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0xff0000, sha512, &sub[sub_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0x111110, sha256, &expected2[expected2_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0x0000ff, sha512, &expected2[expected2_count++]);
+ _test_tpml_addsub(&l,
+ add, add_count,
+ expected1, expected1_count,
+ sub, sub_count,
+ expected2, expected2_count);
+
+ l = (TPML_PCR_SELECTION){
+ .count = 1,
+ .pcrSelections[0].hash = sha1,
+ .pcrSelections[0].sizeofSelect = 3,
+ .pcrSelections[0].pcrSelect[0] = 0xf0,
+ };
+ add_count = 0;
+ expected1_count = 0;
+ sub_count = 0;
+ expected2_count = 0;
+ tpm2_tpms_pcr_selection_from_mask(0xff0000, sha256, &add[add_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0xffff00, sha384, &add[add_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0x0000ff, sha512, &add[add_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0xf00000, sha1 , &add[add_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0xf000f0, sha1 , &expected1[expected1_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0xff0000, sha256, &expected1[expected1_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0xffff00, sha384, &expected1[expected1_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0x0000ff, sha512, &expected1[expected1_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0x00ffff, sha256, &sub[sub_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0xf000f0, sha1 , &expected2[expected2_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0xff0000, sha256, &expected2[expected2_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0xffff00, sha384, &expected2[expected2_count++]);
+ tpm2_tpms_pcr_selection_from_mask(0x0000ff, sha512, &expected2[expected2_count++]);
+ _test_tpml_addsub(&l,
+ add, add_count,
+ expected1, expected1_count,
+ sub, sub_count,
+ expected2, expected2_count);
+}
+
+static bool digest_check(const TPM2B_DIGEST *digest, const char *expect) {
+ _cleanup_free_ char *h = NULL;
+
+ assert_se(digest);
+ assert_se(expect);
+
+ h = hexmem(digest->buffer, digest->size);
+ assert_se(h);
+
+ return strcaseeq(expect, h);
+}
+
+static void digest_init(TPM2B_DIGEST *digest, const char *hash) {
+ assert_se(strlen(hash) <= sizeof(digest->buffer) * 2);
+
+ DEFINE_HEX_PTR(h, hash);
+
+ /* Make sure the length matches a known hash algorithm */
+ assert_se(IN_SET(h_len, TPM2_SHA1_DIGEST_SIZE, TPM2_SHA256_DIGEST_SIZE, TPM2_SHA384_DIGEST_SIZE, TPM2_SHA512_DIGEST_SIZE));
+
+ *digest = TPM2B_DIGEST_MAKE(h, h_len);
+
+ assert_se(digest_check(digest, hash));
+}
+
+TEST(digest_many) {
+ TPM2B_DIGEST d, d0, d1, d2, d3, d4;
+
+ digest_init(&d0, "0000000000000000000000000000000000000000000000000000000000000000");
+ digest_init(&d1, "17b7703d9d00776310ba032e88c1a8c2a9c630ebdd799db622f6631530789175");
+ digest_init(&d2, "12998c017066eb0d2a70b94e6ed3192985855ce390f321bbdb832022888bd251");
+ digest_init(&d3, "c3a65887fedd3fb4f5d0047e906dff830bcbd1293160909eb4b05f485e7387ad");
+ digest_init(&d4, "6491fb4bc08fc0b2ef47fc63db57e249917885e69d8c0d99667df83a59107a33");
+
+ /* tpm2_digest_init, tpm2_digest_rehash */
+ d = (TPM2B_DIGEST){ .size = 1, .buffer = { 2, }, };
+ assert_se(tpm2_digest_init(TPM2_ALG_SHA256, &d) == 0);
+ assert_se(digest_check(&d, "0000000000000000000000000000000000000000000000000000000000000000"));
+ assert_se(tpm2_digest_rehash(TPM2_ALG_SHA256, &d) == 0);
+ assert_se(digest_check(&d, "66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925"));
+
+ d = d1;
+ assert_se(tpm2_digest_rehash(TPM2_ALG_SHA256, &d) == 0);
+ assert_se(digest_check(&d, "ab55014b5ace12ba70c3acc887db571585a83539aad3633d252a710f268f405c"));
+ assert_se(tpm2_digest_init(TPM2_ALG_SHA256, &d) == 0);
+ assert_se(digest_check(&d, "0000000000000000000000000000000000000000000000000000000000000000"));
+
+ /* tpm2_digest_many_digests */
+ assert_se(tpm2_digest_many_digests(TPM2_ALG_SHA256, &d, &d2, 1, false) == 0);
+ assert_se(digest_check(&d, "56571a1be3fbeab18d215f549095915a004b5788ca0d535be668559129a76f25"));
+ assert_se(tpm2_digest_many_digests(TPM2_ALG_SHA256, &d, &d2, 1, true) == 0);
+ assert_se(digest_check(&d, "99dedaee8f4d8d10a8be184399fde8740d5e17ff783ee5c288a4486e4ce3a1fe"));
+
+ const TPM2B_DIGEST da1[] = { d2, d3, };
+ assert_se(tpm2_digest_many_digests(TPM2_ALG_SHA256, &d, da1, ELEMENTSOF(da1), false) == 0);
+ assert_se(digest_check(&d, "525aa13ef9a61827778ec3acf16fbb23b65ae8770b8fb2684d3a33f9457dd6d8"));
+ assert_se(tpm2_digest_many_digests(TPM2_ALG_SHA256, &d, da1, ELEMENTSOF(da1), true) == 0);
+ assert_se(digest_check(&d, "399ca2aa98963d1bd81a2b58a7e5cda24bba1be88fb4da9aa73d97706846566b"));
+
+ const TPM2B_DIGEST da2[] = { d3, d2, d0 };
+ assert_se(tpm2_digest_many_digests(TPM2_ALG_SHA256, &d, da2, ELEMENTSOF(da2), false) == 0);
+ assert_se(digest_check(&d, "b26fd22db74d4cd896bff01c61aa498a575e4a553a7fb5a322a5fee36954313e"));
+ assert_se(tpm2_digest_many_digests(TPM2_ALG_SHA256, &d, da2, ELEMENTSOF(da2), true) == 0);
+ assert_se(digest_check(&d, "091e79a5b09d4048df49a680f966f3ff67910afe185c3baf9704c9ca45bcf259"));
+
+ const TPM2B_DIGEST da3[] = { d4, d4, d4, d4, d3, d4, d4, d4, d4, };
+ assert_se(tpm2_digest_many_digests(TPM2_ALG_SHA256, &d, da3, ELEMENTSOF(da3), false) == 0);
+ assert_se(digest_check(&d, "8eca947641b6002df79dfb571a7f78b7d0a61370a366f722386dfbe444d18830"));
+ assert_se(tpm2_digest_many_digests(TPM2_ALG_SHA256, &d, da3, ELEMENTSOF(da3), true) == 0);
+ assert_se(digest_check(&d, "f9ba17bc0bbe8794e9bcbf112e4d59a11eb68fffbcd5516a746e4857829dff04"));
+
+ /* tpm2_digest_buffer */
+ const uint8_t b1[] = { 1, 2, 3, 4, };
+ assert_se(tpm2_digest_buffer(TPM2_ALG_SHA256, &d, b1, ELEMENTSOF(b1), false) == 0);
+ assert_se(digest_check(&d, "9f64a747e1b97f131fabb6b447296c9b6f0201e79fb3c5356e6c77e89b6a806a"));
+ assert_se(tpm2_digest_buffer(TPM2_ALG_SHA256, &d, b1, ELEMENTSOF(b1), true) == 0);
+ assert_se(digest_check(&d, "ff3bd307b287e9b29bb572f6ccfd19deb0106d0c4c3c5cfe8a1d03a396092ed4"));
+
+ const void *b2 = d2.buffer;
+ assert_se(tpm2_digest_buffer(TPM2_ALG_SHA256, &d, b2, d2.size, false) == 0);
+ assert_se(digest_check(&d, "56571a1be3fbeab18d215f549095915a004b5788ca0d535be668559129a76f25"));
+ assert_se(tpm2_digest_buffer(TPM2_ALG_SHA256, &d, b2, d2.size, true) == 0);
+ assert_se(digest_check(&d, "99dedaee8f4d8d10a8be184399fde8740d5e17ff783ee5c288a4486e4ce3a1fe"));
+
+ /* tpm2_digest_many */
+ const struct iovec iov1[] = {
+ IOVEC_MAKE((void*) b1, ELEMENTSOF(b1)),
+ IOVEC_MAKE(d2.buffer, d2.size),
+ IOVEC_MAKE(d3.buffer, d3.size),
+ };
+ assert_se(tpm2_digest_many(TPM2_ALG_SHA256, &d, iov1, ELEMENTSOF(iov1), false) == 0);
+ assert_se(digest_check(&d, "cd7bde4a047af976b6f1b282309976229be59f96a78aa186de32a1aee488ab09"));
+ assert_se(tpm2_digest_many(TPM2_ALG_SHA256, &d, iov1, ELEMENTSOF(iov1), true) == 0);
+ assert_se(digest_check(&d, "02ecb0628264235111e0053e271092981c8b15d59cd46617836bee3149a4ecb0"));
+}
+
+static void check_parse_pcr_argument(
+ const char *arg,
+ const Tpm2PCRValue *prev_values,
+ size_t n_prev_values,
+ const Tpm2PCRValue *expected_values,
+ size_t n_expected_values) {
+
+ _cleanup_free_ Tpm2PCRValue *values = NULL;
+ size_t n_values = 0;
+
+ if (n_prev_values > 0) {
+ assert_se(GREEDY_REALLOC_APPEND(values, n_values, prev_values, n_prev_values));
+ assert_se(tpm2_parse_pcr_argument_append(arg, &values, &n_values) == 0);
+ } else
+ assert_se(tpm2_parse_pcr_argument(arg, &values, &n_values) == 0);
+
+ assert_se(n_values == n_expected_values);
+ for (size_t i = 0; i < n_values; i++) {
+ const Tpm2PCRValue *v = &values[i], *e = &expected_values[i];
+ //tpm2_log_debug_pcr_value(e, "Expected value");
+ //tpm2_log_debug_pcr_value(v, "Actual value");
+
+ assert_se(v->index == e->index);
+ assert_se(v->hash == e->hash);
+ assert_se(v->value.size == e->value.size);
+ assert_se(memcmp(v->value.buffer, e->value.buffer, e->value.size) == 0);
+ }
+
+ size_t hash_count;
+ assert_se(tpm2_pcr_values_hash_count(expected_values, n_expected_values, &hash_count) == 0);
+ if (hash_count == 1) {
+ uint32_t mask = UINT32_MAX, expected_mask = 0;
+
+ if (n_prev_values > 0)
+ assert_se(tpm2_pcr_values_to_mask(prev_values, n_prev_values, prev_values[0].hash, &mask) == 0);
+
+ assert_se(tpm2_pcr_values_to_mask(expected_values, n_expected_values, expected_values[0].hash, &expected_mask) == 0);
+
+ assert_se(tpm2_parse_pcr_argument_to_mask(arg, &mask) == 0);
+ assert_se(mask == expected_mask);
+ }
+
+ size_t old_n_values = n_values;
+ assert_se(tpm2_parse_pcr_argument_append("", &values, &n_values) == 0);
+ assert_se(values);
+ assert_se(n_values == old_n_values);
+}
+
+static void check_parse_pcr_argument_to_mask(const char *arg, int mask) {
+ uint32_t m = 0;
+ int r = tpm2_parse_pcr_argument_to_mask(arg, &m);
+
+ if (mask < 0)
+ assert_se(mask == r);
+ else
+ assert_se((uint32_t) mask == m);
+}
+
+TEST(parse_pcr_argument) {
+ _cleanup_free_ Tpm2PCRValue *t0p = NULL;
+ size_t n_t0p;
+ assert_se(tpm2_parse_pcr_argument("", &t0p, &n_t0p) == 0);
+ assert_se(n_t0p == 0);
+ assert_se(tpm2_parse_pcr_argument_append("", &t0p, &n_t0p) == 0);
+ assert_se(n_t0p == 0);
+ uint32_t m0 = 0xf;
+ assert_se(tpm2_parse_pcr_argument_to_mask("", &m0) == 0);
+ assert_se(m0 == 0);
+ assert_se(tpm2_parse_pcr_argument_to_mask("", &m0) == 0);
+ assert_se(m0 == 0);
+
+ Tpm2PCRValue t1[] = {
+ TPM2_PCR_VALUE_MAKE(0, 0, {}),
+ TPM2_PCR_VALUE_MAKE(4, 0, {}),
+ TPM2_PCR_VALUE_MAKE(7, 0, {}),
+ TPM2_PCR_VALUE_MAKE(11, 0, {}),
+ };
+ check_parse_pcr_argument("0,4,7,11", NULL, 0, t1, ELEMENTSOF(t1));
+ check_parse_pcr_argument("11,4,7,0", NULL, 0, t1, ELEMENTSOF(t1));
+ check_parse_pcr_argument("7,4,0,11", NULL, 0, t1, ELEMENTSOF(t1));
+ check_parse_pcr_argument("11,7,4,0", NULL, 0, t1, ELEMENTSOF(t1));
+ check_parse_pcr_argument("0+4+7+11", NULL, 0, t1, ELEMENTSOF(t1));
+ check_parse_pcr_argument("0,4+7,11", NULL, 0, t1, ELEMENTSOF(t1));
+
+ Tpm2PCRValue t2[] = {
+ TPM2_PCR_VALUE_MAKE(0, TPM2_ALG_SHA1, {}),
+ TPM2_PCR_VALUE_MAKE(4, TPM2_ALG_SHA1, {}),
+ TPM2_PCR_VALUE_MAKE(7, TPM2_ALG_SHA1, {}),
+ TPM2_PCR_VALUE_MAKE(11, TPM2_ALG_SHA1, {}),
+ };
+ check_parse_pcr_argument("0:sha1,4,7,11", NULL, 0, t2, ELEMENTSOF(t2));
+ check_parse_pcr_argument("11,4,7,0:sha1", NULL, 0, t2, ELEMENTSOF(t2));
+ check_parse_pcr_argument("7,4:sha1,0,11", NULL, 0, t2, ELEMENTSOF(t2));
+ check_parse_pcr_argument("0:sha1,4:sha1,7:sha1,11:sha1", NULL, 0, t2, ELEMENTSOF(t2));
+ check_parse_pcr_argument("0:sha1+4:sha1,11:sha1+7:sha1", NULL, 0, t2, ELEMENTSOF(t2));
+
+ Tpm2PCRValue t3[] = {
+ TPM2_PCR_VALUE_MAKE(0, TPM2_ALG_SHA1, {}),
+ TPM2_PCR_VALUE_MAKE(1, TPM2_ALG_SHA1, {}),
+ TPM2_PCR_VALUE_MAKE(2, TPM2_ALG_SHA1, {}),
+ TPM2_PCR_VALUE_MAKE(3, TPM2_ALG_SHA1, {}),
+ TPM2_PCR_VALUE_MAKE(4, TPM2_ALG_SHA1, {}),
+ TPM2_PCR_VALUE_MAKE(7, TPM2_ALG_SHA1, {}),
+ TPM2_PCR_VALUE_MAKE(11, TPM2_ALG_SHA1, {}),
+ TPM2_PCR_VALUE_MAKE(12, TPM2_ALG_SHA1, {}),
+ };
+ check_parse_pcr_argument("1,2,3,12", t2, ELEMENTSOF(t2), t3, ELEMENTSOF(t3));
+ check_parse_pcr_argument("12,2,3,1", t2, ELEMENTSOF(t2), t3, ELEMENTSOF(t3));
+ check_parse_pcr_argument("1,2,3,12:sha1", t1, ELEMENTSOF(t1), t3, ELEMENTSOF(t3));
+ check_parse_pcr_argument("1,2,3,12:sha1", t2, ELEMENTSOF(t2), t3, ELEMENTSOF(t3));
+ check_parse_pcr_argument("1:sha1,2,3,12", t1, ELEMENTSOF(t1), t3, ELEMENTSOF(t3));
+ check_parse_pcr_argument("1:sha1,2,3,12", t2, ELEMENTSOF(t2), t3, ELEMENTSOF(t3));
+ check_parse_pcr_argument("1:sha1,2:sha1,3:sha1,12:sha1", t1, ELEMENTSOF(t1), t3, ELEMENTSOF(t3));
+ check_parse_pcr_argument("1:sha1,2:sha1,3:sha1,12:sha1", t2, ELEMENTSOF(t2), t3, ELEMENTSOF(t3));
+
+ TPM2B_DIGEST d4;
+ digest_init(&d4, "FCE7F1083082B16CFE2B085DD7858BB11A37C09B78E36C79E5A2FD529353C4E2");
+ Tpm2PCRValue t4[] = {
+ TPM2_PCR_VALUE_MAKE(0, TPM2_ALG_SHA256, {}),
+ TPM2_PCR_VALUE_MAKE(1, TPM2_ALG_SHA256, d4),
+ TPM2_PCR_VALUE_MAKE(2, TPM2_ALG_SHA256, {}),
+ TPM2_PCR_VALUE_MAKE(3, TPM2_ALG_SHA256, {}),
+ TPM2_PCR_VALUE_MAKE(4, TPM2_ALG_SHA256, {}),
+ TPM2_PCR_VALUE_MAKE(7, TPM2_ALG_SHA256, {}),
+ TPM2_PCR_VALUE_MAKE(11, TPM2_ALG_SHA256, {}),
+ TPM2_PCR_VALUE_MAKE(12, TPM2_ALG_SHA256, {}),
+ };
+ check_parse_pcr_argument("1:sha256=0xFCE7F1083082B16CFE2B085DD7858BB11A37C09B78E36C79E5A2FD529353C4E2,2,3,12", t1, ELEMENTSOF(t1), t4, ELEMENTSOF(t4));
+ check_parse_pcr_argument("12,2,3,1:sha256=FCE7F1083082B16CFE2B085DD7858BB11A37C09B78E36C79E5A2FD529353C4E2", t1, ELEMENTSOF(t1), t4, ELEMENTSOF(t4));
+ check_parse_pcr_argument("12,2,3,1:sha256=0xFCE7F1083082B16CFE2B085DD7858BB11A37C09B78E36C79E5A2FD529353C4E2", t1, ELEMENTSOF(t1), t4, ELEMENTSOF(t4));
+ check_parse_pcr_argument("1:sha256=0xFCE7F1083082B16CFE2B085DD7858BB11A37C09B78E36C79E5A2FD529353C4E2,2,3,12:SHA256", t1, ELEMENTSOF(t1), t4, ELEMENTSOF(t4));
+ check_parse_pcr_argument("1:sha256=0xFCE7F1083082B16CFE2B085DD7858BB11A37C09B78E36C79E5A2FD529353C4E2,2,3,12", t1, ELEMENTSOF(t1), t4, ELEMENTSOF(t4));
+ check_parse_pcr_argument("1:sha256=FCE7F1083082B16CFE2B085DD7858BB11A37C09B78E36C79E5A2FD529353C4E2,2:sha256,3:sha256,12:sha256", t1, ELEMENTSOF(t1), t4, ELEMENTSOF(t4));
+ check_parse_pcr_argument("1:sha256=0xFCE7F1083082B16CFE2B085DD7858BB11A37C09B78E36C79E5A2FD529353C4E2,2:sha256,3:sha256,12:sha256", t1, ELEMENTSOF(t1), t4, ELEMENTSOF(t4));
+
+ TPM2B_DIGEST d5;
+ digest_init(&d5, "0F21EADB7F27377668E3C8069BE88D116491FBEE");
+ Tpm2PCRValue t5[] = {
+ TPM2_PCR_VALUE_MAKE(1, TPM2_ALG_SHA1, d5),
+ TPM2_PCR_VALUE_MAKE(0, TPM2_ALG_SHA256, {}),
+ TPM2_PCR_VALUE_MAKE(1, TPM2_ALG_SHA256, d4),
+ TPM2_PCR_VALUE_MAKE(2, TPM2_ALG_SHA256, {}),
+ TPM2_PCR_VALUE_MAKE(3, TPM2_ALG_SHA256, {}),
+ TPM2_PCR_VALUE_MAKE(4, TPM2_ALG_SHA256, {}),
+ TPM2_PCR_VALUE_MAKE(7, TPM2_ALG_SHA256, {}),
+ TPM2_PCR_VALUE_MAKE(11, TPM2_ALG_SHA256, {}),
+ TPM2_PCR_VALUE_MAKE(12, TPM2_ALG_SHA256, {}),
+ TPM2_PCR_VALUE_MAKE(5, TPM2_ALG_SHA384, {}),
+ TPM2_PCR_VALUE_MAKE(6, TPM2_ALG_SHA512, {}),
+ };
+ check_parse_pcr_argument("0,1:sha256=0xFCE7F1083082B16CFE2B085DD7858BB11A37C09B78E36C79E5A2FD529353C4E2,1:sha1=0F21EADB7F27377668E3C8069BE88D116491FBEE,2,3,4,7,11,12,5:sha384,6:sha512", NULL, 0, t5, ELEMENTSOF(t5));
+ check_parse_pcr_argument("1:sha1=0F21EADB7F27377668E3C8069BE88D116491FBEE,6:sha512,5:sha384", t4, ELEMENTSOF(t4), t5, ELEMENTSOF(t5));
+
+ Tpm2PCRValue *v = NULL;
+ size_t n_v = 0;
+ assert_se(tpm2_parse_pcr_argument("1,100", &v, &n_v) < 0);
+ assert_se(tpm2_parse_pcr_argument("1,2=123456abc", &v, &n_v) < 0);
+ assert_se(tpm2_parse_pcr_argument("1,2:invalid", &v, &n_v) < 0);
+ assert_se(tpm2_parse_pcr_argument("1:sha1=invalid", &v, &n_v) < 0);
+ assert_se(v == NULL);
+ assert_se(n_v == 0);
+
+ check_parse_pcr_argument_to_mask("", 0x0);
+ check_parse_pcr_argument_to_mask("0", 0x1);
+ check_parse_pcr_argument_to_mask("1", 0x2);
+ check_parse_pcr_argument_to_mask("0,1", 0x3);
+ check_parse_pcr_argument_to_mask("0+1", 0x3);
+ check_parse_pcr_argument_to_mask("0-1", -EINVAL);
+ check_parse_pcr_argument_to_mask("foo", -EINVAL);
+ check_parse_pcr_argument_to_mask("0,1,2", 0x7);
+ check_parse_pcr_argument_to_mask("0+1+2", 0x7);
+ check_parse_pcr_argument_to_mask("0+1,2", 0x7);
+ check_parse_pcr_argument_to_mask("0,1+2", 0x7);
+ check_parse_pcr_argument_to_mask("0,2", 0x5);
+ check_parse_pcr_argument_to_mask("0+2", 0x5);
+ check_parse_pcr_argument_to_mask("7+application-support", 0x800080);
+ check_parse_pcr_argument_to_mask("8+boot-loader-code", 0x110);
+ check_parse_pcr_argument_to_mask("7,shim-policy,4", 0x4090);
+ check_parse_pcr_argument_to_mask("sysexts,shim-policy+kernel-boot", 0x6800);
+ check_parse_pcr_argument_to_mask("sysexts,shim+kernel-boot", -EINVAL);
+ check_parse_pcr_argument_to_mask("sysexts+17+23", 0x822000);
+ check_parse_pcr_argument_to_mask("6+boot-loader-code,44", -EINVAL);
+ check_parse_pcr_argument_to_mask("debug+24", -EINVAL);
+}
+
+static const TPMT_PUBLIC test_rsa_template = {
+ .type = TPM2_ALG_RSA,
+ .nameAlg = TPM2_ALG_SHA256,
+ .objectAttributes = TPMA_OBJECT_RESTRICTED|TPMA_OBJECT_DECRYPT|TPMA_OBJECT_FIXEDTPM|TPMA_OBJECT_FIXEDPARENT|TPMA_OBJECT_SENSITIVEDATAORIGIN|TPMA_OBJECT_USERWITHAUTH,
+ .parameters.rsaDetail = {
+ .symmetric = {
+ .algorithm = TPM2_ALG_AES,
+ .keyBits.aes = 128,
+ .mode.aes = TPM2_ALG_CFB,
+ },
+ .scheme.scheme = TPM2_ALG_NULL,
+ .keyBits = 2048,
+ },
+};
+
+static const TPMT_PUBLIC test_ecc_template = {
+ .type = TPM2_ALG_ECC,
+ .nameAlg = TPM2_ALG_SHA256,
+ .objectAttributes = TPMA_OBJECT_RESTRICTED|TPMA_OBJECT_DECRYPT|TPMA_OBJECT_FIXEDTPM|TPMA_OBJECT_FIXEDPARENT|TPMA_OBJECT_SENSITIVEDATAORIGIN|TPMA_OBJECT_USERWITHAUTH,
+ .parameters.eccDetail = {
+ .symmetric = {
+ .algorithm = TPM2_ALG_AES,
+ .keyBits.aes = 128,
+ .mode.aes = TPM2_ALG_CFB,
+ },
+ .scheme.scheme = TPM2_ALG_NULL,
+ .curveID = TPM2_ECC_NIST_P256,
+ .kdf.scheme = TPM2_ALG_NULL,
+ },
+};
+
+static const TPMT_PUBLIC *test_templates[] = {
+ &test_rsa_template,
+ &test_ecc_template,
+};
+
+static void tpm2b_public_rsa_init(TPM2B_PUBLIC *public, const char *rsa_n) {
+ TPMT_PUBLIC tpmt = test_rsa_template;
+
+ DEFINE_HEX_PTR(key, rsa_n);
+ tpmt.unique.rsa = TPM2B_PUBLIC_KEY_RSA_MAKE(key, key_len);
+
+ public->size = sizeof(tpmt);
+ public->publicArea = tpmt;
+}
+
+static void tpm2b_public_ecc_init(TPM2B_PUBLIC *public, TPMI_ECC_CURVE curve, const char *x, const char *y) {
+ TPMT_PUBLIC tpmt = test_ecc_template;
+ tpmt.parameters.eccDetail.curveID = curve;
+
+ DEFINE_HEX_PTR(buf_x, x);
+ tpmt.unique.ecc.x = TPM2B_ECC_PARAMETER_MAKE(buf_x, buf_x_len);
+
+ DEFINE_HEX_PTR(buf_y, y);
+ tpmt.unique.ecc.y = TPM2B_ECC_PARAMETER_MAKE(buf_y, buf_y_len);
+
+ public->size = sizeof(tpmt);
+ public->publicArea = tpmt;
+}
+
+#if HAVE_OPENSSL
+TEST(tpm2b_public_to_openssl_pkey) {
+ DEFINE_HEX_PTR(msg, "edc64c6523778961fe9ba03ab7d624b27ca1dd5b01e7734cc6c891d50db04269");
+ TPM2B_PUBLIC public;
+
+ /* RSA */
+ tpm2b_public_rsa_init(&public, "d71cff5bba2173f0434a389171048e7da8cf8409b892c62946481cc383089bc754324620967fea3d00a02a717cdda4bfe1525ad957d294b88434e0a3933e86fb40f234e4935fd2ba27eb1d21da87efa466b74eb4ad18d26059904643441cf402ee933d138a2151f40459c49d87fef59e2cb822768b2d8689a9b58f82bf9a37e70693f2b2d40dfa388d365c1b1f029a14c4fc8dadb68978ef377d20ff2ca24e7078464c705eab42f531557c9c6dc0df66b506d0c26ef604f8110c64867099267453c71871e7ed22505a09daf102afc34355209ca7680eccc0ed368d148f402fa58cbb6c9d52351f535f09e4e24ad805e149f130edaa2f5e7efed3a4d2d03adb85");
+ _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey_rsa = NULL;
+ assert_se(tpm2_tpm2b_public_to_openssl_pkey(&public, &pkey_rsa) >= 0);
+
+ _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx_rsa = EVP_PKEY_CTX_new((EVP_PKEY*) pkey_rsa, NULL);
+ assert_se(ctx_rsa);
+ assert_se(EVP_PKEY_verify_init(ctx_rsa) == 1);
+ assert_se(EVP_PKEY_CTX_set_signature_md(ctx_rsa, EVP_sha256()) > 0);
+
+ DEFINE_HEX_PTR(sig_rsa, "9f70a9e68911be3ec464cae91126328307bf355872127e042d6c61e0a80982872c151033bcf727abfae5fc9500c923120011e7ef4aa5fc690a59a034697b6022c141b4b209e2df6f4b282288cd9181073fbe7158ce113c79d87623423c1f3996ff931e59cc91db74f8e8656215b1436fc93ddec0f1f8fa8510826e674b250f047e6cba94c95ff98072a286baca94646b577974a1e00d56c21944e38960d8ee90511a2f938e5cf1ac7b7cc7ff8e3ac001d321254d3e4f988b90e9f6f873c26ecd0a12a626b3474833cdbb9e9f793238f6c97ee5b75a1a89bb7a7858d34ecfa6d34ac58d95085e6c4fbbebd47a4364be2725c2c6b3fa15d916f3c0b62a66fe76ae");
+ assert_se(EVP_PKEY_verify(ctx_rsa, sig_rsa, sig_rsa_len, (unsigned char*) msg, msg_len) == 1);
+
+ /* ECC */
+ tpm2b_public_ecc_init(&public, TPM2_ECC_NIST_P256, "6fc0ecf3645c673ab7e86d1ec5b315afb950257c5f68ab23296160006711fac2", "8dd2ef7a2c9ecede91493ba98c8fb3f893aff325c6a1e0f752c657b2d6ca1413");
+ _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey_ecc = NULL;
+ assert_se(tpm2_tpm2b_public_to_openssl_pkey(&public, &pkey_ecc) >= 0);
+
+ _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx_ecc = EVP_PKEY_CTX_new((EVP_PKEY*) pkey_ecc, NULL);
+ assert_se(ctx_ecc);
+ assert_se(EVP_PKEY_verify_init(ctx_ecc) == 1);
+
+ DEFINE_HEX_PTR(sig_ecc, "304602210092447ac0b5b32e90923f79bb4aba864b9c546a9900cf193a83243d35d189a2110221009a8b4df1dfa85e225eff9c606694d4d205a7a3968c9552f50bc2790209a90001");
+ assert_se(EVP_PKEY_verify(ctx_ecc, sig_ecc, sig_ecc_len, (unsigned char*) msg, msg_len) == 1);
+}
+
+static void get_tpm2b_public_from_pem(const void *pem, size_t pem_size, TPM2B_PUBLIC *ret) {
+ _cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL;
+ TPM2B_PUBLIC p1 = {}, p2 = {};
+
+ assert(pem);
+ assert(ret);
+
+ assert_se(openssl_pkey_from_pem(pem, pem_size, &pkey) >= 0);
+ assert_se(tpm2_tpm2b_public_from_openssl_pkey(pkey, &p1) >= 0);
+ assert_se(tpm2_tpm2b_public_from_pem(pem, pem_size, &p2) >= 0);
+ assert_se(memcmp_nn(&p1, sizeof(p1), &p2, sizeof(p2)) == 0);
+
+ *ret = p1;
+}
+
+static void check_tpm2b_public_fingerprint(const TPM2B_PUBLIC *public, const char *hexfp) {
+ DEFINE_HEX_PTR(expected, hexfp);
+ _cleanup_free_ void *fp = NULL;
+ size_t fp_size;
+
+ assert_se(tpm2_tpm2b_public_to_fingerprint(public, &fp, &fp_size) >= 0);
+ assert_se(memcmp_nn(fp, fp_size, expected, expected_len) == 0);
+}
+
+static void check_tpm2b_public_name(const TPM2B_PUBLIC *public, const char *hexname) {
+ DEFINE_HEX_PTR(expected, hexname);
+ TPM2B_NAME name = {};
+
+ assert_se(tpm2_calculate_pubkey_name(&public->publicArea, &name) >= 0);
+ assert_se(memcmp_nn(name.name, name.size, expected, expected_len) == 0);
+}
+
+static void check_tpm2b_public_from_ecc_pem(const char *pem, const char *hexx, const char *hexy, const char *hexfp, const char *hexname) {
+ TPM2B_PUBLIC public = {};
+ TPMT_PUBLIC *p = &public.publicArea;
+
+ DEFINE_HEX_PTR(key, pem);
+ get_tpm2b_public_from_pem(key, key_len, &public);
+
+ assert_se(p->type == TPM2_ALG_ECC);
+ assert_se(p->parameters.eccDetail.curveID == TPM2_ECC_NIST_P256);
+
+ DEFINE_HEX_PTR(expected_x, hexx);
+ assert_se(memcmp_nn(p->unique.ecc.x.buffer, p->unique.ecc.x.size, expected_x, expected_x_len) == 0);
+
+ DEFINE_HEX_PTR(expected_y, hexy);
+ assert_se(memcmp_nn(p->unique.ecc.y.buffer, p->unique.ecc.y.size, expected_y, expected_y_len) == 0);
+
+ check_tpm2b_public_fingerprint(&public, hexfp);
+ check_tpm2b_public_name(&public, hexname);
+}
+
+static void check_tpm2b_public_from_rsa_pem(const char *pem, const char *hexn, uint32_t exponent, const char *hexfp, const char *hexname) {
+ TPM2B_PUBLIC public = {};
+ TPMT_PUBLIC *p = &public.publicArea;
+
+ DEFINE_HEX_PTR(key, pem);
+ get_tpm2b_public_from_pem(key, key_len, &public);
+
+ assert_se(p->type == TPM2_ALG_RSA);
+
+ DEFINE_HEX_PTR(expected_n, hexn);
+ assert_se(memcmp_nn(p->unique.rsa.buffer, p->unique.rsa.size, expected_n, expected_n_len) == 0);
+
+ assert_se(p->parameters.rsaDetail.keyBits == expected_n_len * 8);
+
+ assert_se(p->parameters.rsaDetail.exponent == exponent);
+
+ check_tpm2b_public_fingerprint(&public, hexfp);
+ check_tpm2b_public_name(&public, hexname);
+}
+
+TEST(tpm2b_public_from_openssl_pkey) {
+ /* standard ECC key */
+ check_tpm2b_public_from_ecc_pem("2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d466b77457759484b6f5a497a6a3043415159494b6f5a497a6a30444151634451674145726a6e4575424c73496c3972687068777976584e50686a346a426e500a44586e794a304b395579724e6764365335413532542b6f5376746b436a365a726c34685847337741515558706f426c532b7448717452714c35513d3d0a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a",
+ "ae39c4b812ec225f6b869870caf5cd3e18f88c19cf0d79f22742bd532acd81de",
+ "92e40e764fea12bed9028fa66b9788571b7c004145e9a01952fad1eab51a8be5",
+ "cd3373293b62a52b48c12100e80ea9bfd806266ce76893a5ec31cb128052d97c",
+ "000b5c127e4dbaf8fb7bac641e8db25a84a48db876ca7ee3bd317ae1a4554ff72f17");
+
+ /* standard RSA key */
+ check_tpm2b_public_from_rsa_pem("2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d494942496a414e42676b71686b6947397730424151454641414f43415138414d49494243674b4341514541795639434950652f505852337a436f63787045300a6a575262546c3568585844436b472f584b79374b6d2f4439584942334b734f5a31436a5937375571372f674359363170697838697552756a73413464503165380a593445336c68556d374a332b6473766b626f4b64553243626d52494c2f6675627771694c4d587a41673342575278747234547545443533527a373634554650640a307a70304b68775231496230444c67772f344e67566f314146763378784b4d6478774d45683567676b73733038326332706c354a504e32587677426f744e6b4d0a5471526c745a4a35355244436170696e7153334577376675646c4e735851357746766c7432377a7637344b585165616d704c59433037584f6761304c676c536b0a79754774586b6a50542f735542544a705374615769674d5a6f714b7479563463515a58436b4a52684459614c47587673504233687a766d5671636e6b47654e540a65774944415141420a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a",
+ "c95f4220f7bf3d7477cc2a1cc691348d645b4e5e615d70c2906fd72b2eca9bf0fd5c80772ac399d428d8efb52aeff80263ad698b1f22b91ba3b00e1d3f57bc638137961526ec9dfe76cbe46e829d53609b99120bfdfb9bc2a88b317cc0837056471b6be13b840f9dd1cfbeb85053ddd33a742a1c11d486f40cb830ff8360568d4016fdf1c4a31dc7030487982092cb34f36736a65e493cdd97bf0068b4d90c4ea465b59279e510c26a98a7a92dc4c3b7ee76536c5d0e7016f96ddbbcefef829741e6a6a4b602d3b5ce81ad0b8254a4cae1ad5e48cf4ffb140532694ad6968a0319a2a2adc95e1c4195c29094610d868b197bec3c1de1cef995a9c9e419e3537b",
+ 0x10001,
+ "d9186d13a7fd5b3644cee05448f49ad3574e82a2942ff93cf89598d36cca78a9",
+ "000be1bd75c7976e7a30e9e82223b81a9eff0d42c30618e588db592ed5da94455e81");
+
+ /* RSA key with non-default (i.e. not 0x10001) exponent */
+ check_tpm2b_public_from_rsa_pem("2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d494942496a414e42676b71686b6947397730424151454641414f43415138414d49494243674b434151454179566c7551664b75565171596a5a71436a657a760a364e4a6f58654c736f702f72765375666330773769544d4f73566741557462515452505451725874397065537a4370524467634378656b6a544144577279304b0a6d59786a7a3634776c6a7030463959383068636a6b6b4b3759414d333054664c4648656c2b377574427370777142467a6e2b385a6659567353434b397354706f0a316c61376e5347514e7451576f36444a366c525a336a676d6d584f61544654416145304a432b7046584273564471736d46326438362f314e51714a755a5154520a575852636954704e58357649792f37766b6c5a6a685569526c78764e594f4e3070636476534a37364e74496e447a3048506f775a38705a454f4d2f4a454f59780a617a4c4a6a644936446b355279593578325a7949375074566a3057537242524f4d696f2b674c6556457a43343456336438315a38445138564e334c69625130330a70514944415141460a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a",
+ "c9596e41f2ae550a988d9a828decefe8d2685de2eca29febbd2b9f734c3b89330eb1580052d6d04d13d342b5edf69792cc2a510e0702c5e9234c00d6af2d0a998c63cfae30963a7417d63cd217239242bb600337d137cb1477a5fbbbad06ca70a811739fef197d856c4822bdb13a68d656bb9d219036d416a3a0c9ea5459de382699739a4c54c0684d090bea455c1b150eab2617677cebfd4d42a26e6504d159745c893a4d5f9bc8cbfeef925663854891971bcd60e374a5c76f489efa36d2270f3d073e8c19f2964438cfc910e6316b32c98dd23a0e4e51c98e71d99c88ecfb558f4592ac144e322a3e80b7951330b8e15dddf3567c0d0f153772e26d0d37a5",
+ 0x10005,
+ "c8ca80a687d5972e1d961aaa2cfde2ff2e7a20d85e3ea0382804e70e013d65af",
+ "000beb8974d36d8cf58fdc87460dda00319e10c94c1b9f222ac9ce29d1c4776246cc");
+}
+#endif
+
+static void check_name(const TPM2B_NAME *name, const char *expect) {
+ assert_se(name->size == SHA256_DIGEST_SIZE + 2);
+
+ DEFINE_HEX_PTR(e, expect);
+ assert_se(name->size == e_len);
+ assert_se(memcmp(name->name, e, e_len) == 0);
+}
+
+TEST(calculate_pubkey_name) {
+ TPM2B_PUBLIC public;
+ TPM2B_NAME name;
+
+ /* RSA */
+ tpm2b_public_rsa_init(&public, "9ec7341c52093ac40a1965a5df10432513c539adcf905e30577ab6ebc88ffe53cd08cef12ed9bec6125432f4fada3629b8b96d31b8f507aa35029188fe396da823fcb236027f7fbb01b0da3d87be7f999390449ced604bdf7e26c48657cc0671000f1147da195c3861c96642e54427cb7a11572e07567ec3fd6316978abc4bd92b27bb0a0e4958e599804eeb41d682b3b7fc1f960209f80a4fb8a1b64abfd96bf5d554e73cdd6ad1c8becb4fcf5e8f0c3e621d210e5e2f308f6520ad9a966779231b99f06c5989e5a23a9415c8808ab89ce81117632e2f8461cd4428bded40979236aeadafe8de3f51660a45e1dbc87694e6a36360201cca3ff9e7263e712727");
+ assert_se(tpm2_calculate_pubkey_name(&public.publicArea, &name) >= 0);
+ check_name(&name, "000be78f74a470dd92e979ca067cdb2293a35f075e8560b436bd2ccea5da21486a07");
+
+ /* ECC */
+ tpm2b_public_ecc_init(&public, TPM2_ECC_NIST_P256, "238e02ee4fd5598add6b502429f1815418515e4b0d6551c8e816b38cb15451d1", "70c2d491769775ec43ccd5a571c429233e9d30cf0f486c2e01acd6cb32ba93b6");
+ assert_se(tpm2_calculate_pubkey_name(&public.publicArea, &name) >= 0);
+ check_name(&name, "000b302787187ba19c82011c987bd2dcdbb652b3a543ccc5cb0b49c33d4caae604a6");
+}
+
+TEST(calculate_policy_auth_value) {
+ TPM2B_DIGEST d;
+
+ digest_init(&d, "0000000000000000000000000000000000000000000000000000000000000000");
+ assert_se(tpm2_calculate_policy_auth_value(&d) == 0);
+ assert_se(digest_check(&d, "8fcd2169ab92694e0c633f1ab772842b8241bbc20288981fc7ac1eddc1fddb0e"));
+ assert_se(tpm2_calculate_policy_auth_value(&d) == 0);
+ assert_se(digest_check(&d, "759ebd5ed65100e0b4aa2d04b4b789c2672d92ecc9cdda4b5fa16a303132e008"));
+}
+
+TEST(calculate_policy_authorize) {
+ TPM2B_PUBLIC public;
+ TPM2B_DIGEST d;
+
+ /* RSA */
+ tpm2b_public_rsa_init(&public, "9ec7341c52093ac40a1965a5df10432513c539adcf905e30577ab6ebc88ffe53cd08cef12ed9bec6125432f4fada3629b8b96d31b8f507aa35029188fe396da823fcb236027f7fbb01b0da3d87be7f999390449ced604bdf7e26c48657cc0671000f1147da195c3861c96642e54427cb7a11572e07567ec3fd6316978abc4bd92b27bb0a0e4958e599804eeb41d682b3b7fc1f960209f80a4fb8a1b64abfd96bf5d554e73cdd6ad1c8becb4fcf5e8f0c3e621d210e5e2f308f6520ad9a966779231b99f06c5989e5a23a9415c8808ab89ce81117632e2f8461cd4428bded40979236aeadafe8de3f51660a45e1dbc87694e6a36360201cca3ff9e7263e712727");
+ digest_init(&d, "0000000000000000000000000000000000000000000000000000000000000000");
+ assert_se(tpm2_calculate_policy_authorize(&public, NULL, &d) == 0);
+ assert_se(digest_check(&d, "95213a3784eaab04f427bc7e8851c2f1df0903be8e42428ec25dcefd907baff1"));
+ assert_se(tpm2_calculate_policy_authorize(&public, NULL, &d) == 0);
+ assert_se(digest_check(&d, "95213a3784eaab04f427bc7e8851c2f1df0903be8e42428ec25dcefd907baff1"));
+
+ /* ECC */
+ tpm2b_public_ecc_init(&public, TPM2_ECC_NIST_P256, "423a89da6f0998f510489ab9682706e762031ef8f9faef2a185eff67065a187e", "996f73291670cef9e303d6cd9fa19ddf2c9c1fb1e283324ca9acca07c405c8d0");
+ digest_init(&d, "0000000000000000000000000000000000000000000000000000000000000000");
+ assert_se(tpm2_calculate_policy_authorize(&public, NULL, &d) == 0);
+ assert_se(digest_check(&d, "2a5b705e83f949c27ac4d2e79e54fb5fb0a60f0b37bbd54a0ee1022ba00d3628"));
+ assert_se(tpm2_calculate_policy_authorize(&public, NULL, &d) == 0);
+ assert_se(digest_check(&d, "2a5b705e83f949c27ac4d2e79e54fb5fb0a60f0b37bbd54a0ee1022ba00d3628"));
+}
+
+TEST(calculate_policy_pcr) {
+ TPM2B_DIGEST d, dN[16];
+
+ digest_init(&dN[ 0], "2124793cbbe60c3a8637d3b84a5d054e87c351e1469a285acc04755e8b204dec");
+ digest_init(&dN[ 1], "bf7592f18adcfdc549fc0b94939f5069a24697f9cff4a0dca29014767b97559d");
+ digest_init(&dN[ 2], "4b00cff9dee3a364979b2dc241b34568a8ad49fcf2713df259e47dff8875feed");
+ digest_init(&dN[ 3], "3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969");
+ digest_init(&dN[ 4], "368f85b3013041dfe203faaa364f00b07c5da7b1e5f1dbf2efb06fa6b9bd92de");
+ digest_init(&dN[ 5], "c97c40369691c8e4aa78fb3a52655cd193b780a838b8e23f5f476576919db5e5");
+ digest_init(&dN[ 6], "3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969");
+ digest_init(&dN[ 7], "aa1154c9e0a774854ccbed4c8ce7e9b906b3d700a1a8db1772d0341a62dbe51b");
+ digest_init(&dN[ 8], "cfde439a2c06af3479ca6bdc60429b90553d65300c5cfcc40004a08c6b5ad81a");
+ digest_init(&dN[ 9], "9c2bac22ef5ec84fcdb71c3ebf776cba1247e5da980e5ee08e45666a2edf0b8b");
+ digest_init(&dN[10], "9885873f4d7348199ad286f8f2476d4f866940950f6f9fb9f945ed352dbdcbd2");
+ digest_init(&dN[11], "42400ab950d21aa79d12cc4fdef67d1087a39ad64900619831c0974dbae54e44");
+ digest_init(&dN[12], "767d064382e56ca1ad3bdcc6bc596112e6c2008b593d3570d24c2bfa64c4628c");
+ digest_init(&dN[13], "30c16133175959408c9745d8dafadef5daf4b39cb2be04df0d60089bd46d3cc4");
+ digest_init(&dN[14], "e3991b7ddd47be7e92726a832d6874c5349b52b789fa0db8b558c69fea29574e");
+ digest_init(&dN[15], "852dae3ecb992bdeb13d6002fefeeffdd90feca8b378d56681ef2c885d0e5137");
+
+ digest_init(&d, "0000000000000000000000000000000000000000000000000000000000000000");
+ Tpm2PCRValue v1[] = {
+ TPM2_PCR_VALUE_MAKE(4, TPM2_ALG_SHA256, dN[4]),
+ TPM2_PCR_VALUE_MAKE(7, TPM2_ALG_SHA256, dN[7]),
+ TPM2_PCR_VALUE_MAKE(8, TPM2_ALG_SHA256, dN[8]),
+ };
+ assert_se(tpm2_calculate_policy_pcr(v1, ELEMENTSOF(v1), &d) == 0);
+ assert_se(digest_check(&d, "76532a0e16f7e6bf6b02918c11f75d99d729fab0cc81d0df2c4284a2c4fe6e05"));
+ assert_se(tpm2_calculate_policy_pcr(v1, ELEMENTSOF(v1), &d) == 0);
+ assert_se(digest_check(&d, "97e64bcabb64c1fa4b726528644926c8029f5b4458b0575c98c04fe225629a0b"));
+
+ digest_init(&d, "0000000000000000000000000000000000000000000000000000000000000000");
+ Tpm2PCRValue v2[] = {
+ TPM2_PCR_VALUE_MAKE( 0, TPM2_ALG_SHA256, dN[ 0]),
+ TPM2_PCR_VALUE_MAKE( 1, TPM2_ALG_SHA256, dN[ 1]),
+ TPM2_PCR_VALUE_MAKE( 2, TPM2_ALG_SHA256, dN[ 2]),
+ TPM2_PCR_VALUE_MAKE( 3, TPM2_ALG_SHA256, dN[ 3]),
+ TPM2_PCR_VALUE_MAKE( 4, TPM2_ALG_SHA256, dN[ 4]),
+ TPM2_PCR_VALUE_MAKE( 5, TPM2_ALG_SHA256, dN[ 5]),
+ TPM2_PCR_VALUE_MAKE( 6, TPM2_ALG_SHA256, dN[ 6]),
+ TPM2_PCR_VALUE_MAKE( 7, TPM2_ALG_SHA256, dN[ 7]),
+ TPM2_PCR_VALUE_MAKE( 8, TPM2_ALG_SHA256, dN[ 8]),
+ TPM2_PCR_VALUE_MAKE( 9, TPM2_ALG_SHA256, dN[ 9]),
+ TPM2_PCR_VALUE_MAKE(10, TPM2_ALG_SHA256, dN[10]),
+ TPM2_PCR_VALUE_MAKE(11, TPM2_ALG_SHA256, dN[11]),
+ TPM2_PCR_VALUE_MAKE(12, TPM2_ALG_SHA256, dN[12]),
+ TPM2_PCR_VALUE_MAKE(13, TPM2_ALG_SHA256, dN[13]),
+ TPM2_PCR_VALUE_MAKE(14, TPM2_ALG_SHA256, dN[14]),
+ TPM2_PCR_VALUE_MAKE(15, TPM2_ALG_SHA256, dN[15]),
+ };
+ assert_se(tpm2_calculate_policy_pcr(v2, ELEMENTSOF(v2), &d) == 0);
+ assert_se(digest_check(&d, "22be4f1674f792d6345cea9427701068f0e8d9f42755dcc0e927e545a68f9c13"));
+ assert_se(tpm2_calculate_policy_pcr(v2, ELEMENTSOF(v2), &d) == 0);
+ assert_se(digest_check(&d, "7481fd1b116078eb3ac2456e4ad542c9b46b9b8eb891335771ca8e7c8f8e4415"));
+}
+
+static void check_srk_rsa_template(TPMT_PUBLIC *template) {
+ assert_se(template->type == TPM2_ALG_RSA);
+ assert_se(template->nameAlg == TPM2_ALG_SHA256);
+ assert_se(template->objectAttributes == 0x30472);
+ assert_se(template->parameters.rsaDetail.symmetric.algorithm == TPM2_ALG_AES);
+ assert_se(template->parameters.rsaDetail.symmetric.keyBits.sym == 128);
+ assert_se(template->parameters.rsaDetail.symmetric.mode.sym == TPM2_ALG_CFB);
+ assert_se(template->parameters.rsaDetail.scheme.scheme == TPM2_ALG_NULL);
+ assert_se(template->parameters.rsaDetail.keyBits == 2048);
+}
+
+static void check_srk_ecc_template(TPMT_PUBLIC *template) {
+ assert_se(template->type == TPM2_ALG_ECC);
+ assert_se(template->nameAlg == TPM2_ALG_SHA256);
+ assert_se(template->objectAttributes == 0x30472);
+ assert_se(template->parameters.eccDetail.symmetric.algorithm == TPM2_ALG_AES);
+ assert_se(template->parameters.eccDetail.symmetric.keyBits.sym == 128);
+ assert_se(template->parameters.eccDetail.symmetric.mode.sym == TPM2_ALG_CFB);
+ assert_se(template->parameters.eccDetail.scheme.scheme == TPM2_ALG_NULL);
+ assert_se(template->parameters.eccDetail.kdf.scheme == TPM2_ALG_NULL);
+ assert_se(template->parameters.eccDetail.curveID == TPM2_ECC_NIST_P256);
+}
+
+TEST(tpm2_get_srk_template) {
+ TPMT_PUBLIC template;
+
+ assert_se(tpm2_get_srk_template(TPM2_ALG_RSA, &template) >= 0);
+ check_srk_rsa_template(&template);
+
+ assert_se(tpm2_get_srk_template(TPM2_ALG_ECC, &template) >= 0);
+ check_srk_ecc_template(&template);
+}
+
+static void check_best_srk_template(Tpm2Context *c) {
+ TEST_LOG_FUNC();
+
+ TPMT_PUBLIC template;
+ assert_se(tpm2_get_best_srk_template(c, &template) >= 0);
+
+ assert_se(IN_SET(template.type, TPM2_ALG_ECC, TPM2_ALG_RSA));
+
+ if (template.type == TPM2_ALG_RSA)
+ check_srk_rsa_template(&template);
+ else
+ check_srk_ecc_template(&template);
+}
+
+static void check_test_parms(Tpm2Context *c) {
+ assert(c);
+
+ TEST_LOG_FUNC();
+
+ TPMU_PUBLIC_PARMS parms = {
+ .symDetail.sym = {
+ .algorithm = TPM2_ALG_AES,
+ .keyBits.aes = 128,
+ .mode.aes = TPM2_ALG_CFB,
+ },
+ };
+
+ /* Test with invalid parms */
+ assert_se(!tpm2_test_parms(c, TPM2_ALG_CFB, &parms));
+
+ TPMU_PUBLIC_PARMS invalid_parms = parms;
+ invalid_parms.symDetail.sym.keyBits.aes = 1;
+ assert_se(!tpm2_test_parms(c, TPM2_ALG_SYMCIPHER, &invalid_parms));
+
+ /* Test with valid parms */
+ assert_se(tpm2_test_parms(c, TPM2_ALG_SYMCIPHER, &parms));
+}
+
+static void check_supports_alg(Tpm2Context *c) {
+ assert(c);
+
+ TEST_LOG_FUNC();
+
+ /* Test invalid algs */
+ assert_se(!tpm2_supports_alg(c, TPM2_ALG_ERROR));
+ assert_se(!tpm2_supports_alg(c, TPM2_ALG_LAST + 1));
+
+ /* Test valid algs */
+ assert_se(tpm2_supports_alg(c, TPM2_ALG_RSA));
+ assert_se(tpm2_supports_alg(c, TPM2_ALG_AES));
+ assert_se(tpm2_supports_alg(c, TPM2_ALG_CFB));
+}
+
+static void check_supports_command(Tpm2Context *c) {
+ assert(c);
+
+ TEST_LOG_FUNC();
+
+ /* Test invalid commands. TPM specification Part 2 ("Structures") section "TPM_CC (Command Codes)"
+ * states bits 31:30 and 28:16 are reserved and must be 0. */
+ assert_se(!tpm2_supports_command(c, UINT32_C(0x80000000)));
+ assert_se(!tpm2_supports_command(c, UINT32_C(0x40000000)));
+ assert_se(!tpm2_supports_command(c, UINT32_C(0x00100000)));
+ assert_se(!tpm2_supports_command(c, UINT32_C(0x80000144)));
+ assert_se(!tpm2_supports_command(c, UINT32_C(0x40000144)));
+ assert_se(!tpm2_supports_command(c, UINT32_C(0x00100144)));
+
+ /* Test valid commands. We should be able to expect all TPMs support these. */
+ assert_se(tpm2_supports_command(c, TPM2_CC_Startup));
+ assert_se(tpm2_supports_command(c, TPM2_CC_StartAuthSession));
+ assert_se(tpm2_supports_command(c, TPM2_CC_Create));
+ assert_se(tpm2_supports_command(c, TPM2_CC_CreatePrimary));
+ assert_se(tpm2_supports_command(c, TPM2_CC_Unseal));
+}
+
+static void check_get_or_create_srk(Tpm2Context *c) {
+ TEST_LOG_FUNC();
+
+ _cleanup_free_ TPM2B_PUBLIC *public = NULL;
+ _cleanup_free_ TPM2B_NAME *name = NULL, *qname = NULL;
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *handle = NULL;
+ assert_se(tpm2_get_or_create_srk(c, NULL, &public, &name, &qname, &handle) >= 0);
+ assert_se(public && name && qname && handle);
+
+ _cleanup_free_ TPM2B_PUBLIC *public2 = NULL;
+ _cleanup_free_ TPM2B_NAME *name2 = NULL, *qname2 = NULL;
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *handle2 = NULL;
+ assert_se(tpm2_get_srk(c, NULL, &public2, &name2, &qname2, &handle2) >= 0);
+ assert_se(public2 && name2 && qname2 && handle2);
+
+ assert_se(memcmp_nn(public, sizeof(*public), public2, sizeof(*public2)) == 0);
+ assert_se(memcmp_nn(name->name, name->size, name2->name, name2->size) == 0);
+ assert_se(memcmp_nn(qname->name, qname->size, qname2->name, qname2->size) == 0);
+}
+
+#if HAVE_OPENSSL && OPENSSL_VERSION_MAJOR >= 3
+static void calculate_seal_and_unseal(
+ Tpm2Context *c,
+ TPM2_HANDLE parent_index,
+ const TPM2B_PUBLIC *parent_public) {
+
+ _cleanup_free_ char *secret_string = NULL;
+ assert_se(asprintf(&secret_string, "The classified documents are in room %x", parent_index) > 0);
+ size_t secret_size = strlen(secret_string) + 1;
+
+ _cleanup_free_ void *blob = NULL;
+ size_t blob_size = 0;
+ _cleanup_free_ void *serialized_parent = NULL;
+ size_t serialized_parent_size;
+ assert_se(tpm2_calculate_seal(
+ parent_index,
+ parent_public,
+ /* attributes= */ NULL,
+ secret_string, secret_size,
+ /* policy= */ NULL,
+ /* pin= */ NULL,
+ /* ret_secret= */ NULL, /* ret_secret_size= */ 0,
+ &blob, &blob_size,
+ &serialized_parent, &serialized_parent_size) >= 0);
+
+ _cleanup_free_ void *unsealed_secret = NULL;
+ size_t unsealed_secret_size;
+ assert_se(tpm2_unseal(
+ c,
+ /* hash_pcr_mask= */ 0,
+ /* pcr_bank= */ 0,
+ /* pubkey= */ NULL, /* pubkey_size= */ 0,
+ /* pubkey_pcr_mask= */ 0,
+ /* signature= */ NULL,
+ /* pin= */ NULL,
+ /* pcrlock_policy= */ NULL,
+ /* primary_alg= */ 0,
+ blob, blob_size,
+ /* known_policy_hash= */ NULL, /* known_policy_hash_size= */ 0,
+ serialized_parent, serialized_parent_size,
+ &unsealed_secret, &unsealed_secret_size) >= 0);
+
+ assert_se(memcmp_nn(secret_string, secret_size, unsealed_secret, unsealed_secret_size) == 0);
+
+ char unsealed_string[unsealed_secret_size];
+ assert_se(snprintf(unsealed_string, unsealed_secret_size, "%s", (char*) unsealed_secret) == (int) unsealed_secret_size - 1);
+ log_debug("Unsealed secret is: %s", unsealed_string);
+}
+
+static int check_calculate_seal(Tpm2Context *c) {
+ assert(c);
+ int r;
+
+ if (detect_virtualization() == VIRTUALIZATION_NONE && !slow_tests_enabled()) {
+ log_notice("Skipping slow calculate seal TPM2 tests. Physical system detected, and slow tests disabled.");
+ return 0;
+ }
+
+ TEST_LOG_FUNC();
+
+ _cleanup_free_ TPM2B_PUBLIC *srk_public = NULL;
+ assert_se(tpm2_get_srk(c, NULL, &srk_public, NULL, NULL, NULL) >= 0);
+ calculate_seal_and_unseal(c, TPM2_SRK_HANDLE, srk_public);
+
+ TPMI_ALG_ASYM test_algs[] = { TPM2_ALG_RSA, TPM2_ALG_ECC, };
+ for (unsigned i = 0; i < ELEMENTSOF(test_algs); i++) {
+ TPMI_ALG_ASYM alg = test_algs[i];
+
+ TPM2B_PUBLIC template = { .size = sizeof(TPMT_PUBLIC), };
+ assert_se(tpm2_get_srk_template(alg, &template.publicArea) >= 0);
+
+ _cleanup_free_ TPM2B_PUBLIC *public = NULL;
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *handle = NULL;
+ assert_se(tpm2_create_primary(c, NULL, &template, NULL, &public, &handle) >= 0);
+
+ /* Once our minimum libtss2-esys version is 2.4.0 or later, this can assume
+ * tpm2_index_from_handle() should always work. */
+ TPM2_HANDLE index;
+ r = tpm2_index_from_handle(c, handle, &index);
+ if (r == -EOPNOTSUPP)
+ return log_tests_skipped("libtss2-esys version too old to support tpm2_index_from_handle()");
+ assert_se(r >= 0);
+
+ calculate_seal_and_unseal(c, index, public);
+ }
+
+ return 0;
+}
+#endif /* HAVE_OPENSSL && OPENSSL_VERSION_MAJOR >= 3 */
+
+static void check_seal_unseal_for_handle(Tpm2Context *c, TPM2_HANDLE handle) {
+ TPM2B_DIGEST policy = TPM2B_DIGEST_MAKE(NULL, TPM2_SHA256_DIGEST_SIZE);
+
+ assert(c);
+
+ log_debug("Check seal/unseal for handle 0x%" PRIx32, handle);
+
+ _cleanup_free_ void *secret = NULL, *blob = NULL, *srk = NULL, *unsealed_secret = NULL;
+ size_t secret_size, blob_size, srk_size, unsealed_secret_size;
+ assert_se(tpm2_seal(
+ c,
+ handle,
+ &policy,
+ /* pin= */ NULL,
+ &secret, &secret_size,
+ &blob, &blob_size,
+ /* ret_primary_alg= */ NULL,
+ &srk, &srk_size) >= 0);
+
+ assert_se(tpm2_unseal(
+ c,
+ /* hash_pcr_mask= */ 0,
+ /* pcr_bank= */ 0,
+ /* pubkey= */ NULL, /* pubkey_size= */ 0,
+ /* pubkey_pcr_mask= */ 0,
+ /* signature= */ NULL,
+ /* pin= */ NULL,
+ /* pcrlock_policy= */ NULL,
+ /* primary_alg= */ 0,
+ blob, blob_size,
+ /* policy_hash= */ NULL, /* policy_hash_size= */ 0,
+ srk, srk_size,
+ &unsealed_secret, &unsealed_secret_size) >= 0);
+
+ assert_se(memcmp_nn(secret, secret_size, unsealed_secret, unsealed_secret_size) == 0);
+}
+
+static void check_seal_unseal(Tpm2Context *c) {
+ int r;
+
+ assert(c);
+
+ if (detect_virtualization() == VIRTUALIZATION_NONE && !slow_tests_enabled()) {
+ log_notice("Skipping slow seal/unseal TPM2 tests. Physical system detected, and slow tests disabled.");
+ return;
+ }
+
+ TEST_LOG_FUNC();
+
+ check_seal_unseal_for_handle(c, 0);
+ check_seal_unseal_for_handle(c, TPM2_SRK_HANDLE);
+
+ FOREACH_ARRAY(template, test_templates, ELEMENTSOF(test_templates)) {
+ TPM2B_PUBLIC public = {
+ .publicArea = **template,
+ .size = sizeof(**template),
+ };
+ _cleanup_(tpm2_handle_freep) Tpm2Handle *transient_handle = NULL;
+ assert_se(tpm2_create_primary(
+ c,
+ /* session= */ NULL,
+ &public,
+ /* sensitive= */ NULL,
+ /* ret_public= */ NULL,
+ &transient_handle) >= 0);
+
+ TPMI_DH_PERSISTENT transient_handle_index;
+ r = tpm2_index_from_handle(c, transient_handle, &transient_handle_index);
+ if (r == -EOPNOTSUPP) {
+ /* libesys too old */
+ log_tests_skipped("libesys too old for tpm2_index_from_handle");
+ return;
+ }
+ assert_se(r >= 0);
+
+ check_seal_unseal_for_handle(c, transient_handle_index);
+ }
+}
+
+TEST_RET(tests_which_require_tpm) {
+ _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL;
+ int r = 0;
+
+ if (tpm2_context_new(NULL, &c) < 0)
+ return log_tests_skipped("Could not find TPM");
+
+ check_test_parms(c);
+ check_supports_alg(c);
+ check_supports_command(c);
+ check_best_srk_template(c);
+ check_get_or_create_srk(c);
+ check_seal_unseal(c);
+
+#if HAVE_OPENSSL && OPENSSL_VERSION_MAJOR >= 3 /* calculating sealed object requires openssl >= 3 */
+ r = check_calculate_seal(c);
+#endif
+
+ return r;
+}
+
+#endif /* HAVE_TPM2 */
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-udev-util.c b/src/test/test-udev-util.c
new file mode 100644
index 0000000..cb80c69
--- /dev/null
+++ b/src/test/test-udev-util.c
@@ -0,0 +1,63 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "macro.h"
+#include "string-util.h"
+#include "tests.h"
+#include "udev-util.h"
+
+static void test_udev_replace_whitespace_one_len(const char *str, size_t len, const char *expected) {
+ _cleanup_free_ char *result = NULL;
+ int r;
+
+ result = new(char, len + 1);
+ assert_se(result);
+ r = udev_replace_whitespace(str, result, len);
+ assert_se((size_t) r == strlen(expected));
+ assert_se(streq(result, expected));
+}
+
+static void test_udev_replace_whitespace_one(const char *str, const char *expected) {
+ test_udev_replace_whitespace_one_len(str, strlen(str), expected);
+}
+
+TEST(udev_replace_whitespace) {
+ test_udev_replace_whitespace_one("hogehoge", "hogehoge");
+ test_udev_replace_whitespace_one("hoge hoge", "hoge_hoge");
+ test_udev_replace_whitespace_one(" hoge hoge ", "hoge_hoge");
+ test_udev_replace_whitespace_one(" ", "");
+ test_udev_replace_whitespace_one("hoge ", "hoge");
+
+ test_udev_replace_whitespace_one_len("hoge hoge ", 9, "hoge_hoge");
+ test_udev_replace_whitespace_one_len("hoge hoge ", 8, "hoge_hog");
+ test_udev_replace_whitespace_one_len("hoge hoge ", 7, "hoge_ho");
+ test_udev_replace_whitespace_one_len("hoge hoge ", 6, "hoge_h");
+ test_udev_replace_whitespace_one_len("hoge hoge ", 5, "hoge");
+ test_udev_replace_whitespace_one_len("hoge hoge ", 4, "hoge");
+ test_udev_replace_whitespace_one_len("hoge hoge ", 3, "hog");
+ test_udev_replace_whitespace_one_len("hoge hoge ", 2, "ho");
+ test_udev_replace_whitespace_one_len("hoge hoge ", 1, "h");
+ test_udev_replace_whitespace_one_len("hoge hoge ", 0, "");
+
+ test_udev_replace_whitespace_one_len(" hoge hoge ", 16, "hoge_hoge");
+ test_udev_replace_whitespace_one_len(" hoge hoge ", 15, "hoge_hoge");
+ test_udev_replace_whitespace_one_len(" hoge hoge ", 14, "hoge_hog");
+ test_udev_replace_whitespace_one_len(" hoge hoge ", 13, "hoge_ho");
+ test_udev_replace_whitespace_one_len(" hoge hoge ", 12, "hoge_h");
+ test_udev_replace_whitespace_one_len(" hoge hoge ", 11, "hoge");
+ test_udev_replace_whitespace_one_len(" hoge hoge ", 10, "hoge");
+ test_udev_replace_whitespace_one_len(" hoge hoge ", 9, "hoge");
+ test_udev_replace_whitespace_one_len(" hoge hoge ", 8, "hoge");
+ test_udev_replace_whitespace_one_len(" hoge hoge ", 7, "hog");
+ test_udev_replace_whitespace_one_len(" hoge hoge ", 6, "ho");
+ test_udev_replace_whitespace_one_len(" hoge hoge ", 5, "h");
+ test_udev_replace_whitespace_one_len(" hoge hoge ", 4, "");
+ test_udev_replace_whitespace_one_len(" hoge hoge ", 3, "");
+ test_udev_replace_whitespace_one_len(" hoge hoge ", 2, "");
+ test_udev_replace_whitespace_one_len(" hoge hoge ", 1, "");
+ test_udev_replace_whitespace_one_len(" hoge hoge ", 0, "");
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-uid-alloc-range.c b/src/test/test-uid-alloc-range.c
new file mode 100644
index 0000000..cd06463
--- /dev/null
+++ b/src/test/test-uid-alloc-range.c
@@ -0,0 +1,93 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <unistd.h>
+#include <sys/types.h>
+
+#include "fd-util.h"
+#include "fileio.h"
+#include "format-util.h"
+#include "fs-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+#include "uid-alloc-range.h"
+
+static void test_read_login_defs_one(const char *path) {
+ log_info("/* %s(\"%s\") */", __func__, path ?: "<custom>");
+
+ _cleanup_(unlink_tempfilep) char name[] = "/tmp/test-user-record.XXXXXX";
+ _cleanup_fclose_ FILE *f = NULL;
+ if (!path) {
+ assert_se(fmkostemp_safe(name, "r+", &f) == 0);
+ fprintf(f,
+ "SYS_UID_MIN "UID_FMT"\n"
+ "SYS_UID_MAX "UID_FMT"\n"
+ "SYS_GID_MIN "GID_FMT"\n"
+ "SYS_GID_MAX "GID_FMT"\n",
+ (uid_t) (SYSTEM_ALLOC_UID_MIN + 5),
+ (uid_t) (SYSTEM_UID_MAX + 5),
+ (gid_t) (SYSTEM_ALLOC_GID_MIN + 5),
+ (gid_t) (SYSTEM_GID_MAX + 5));
+ assert_se(fflush_and_check(f) >= 0);
+ }
+
+ UGIDAllocationRange defs;
+ assert_se(read_login_defs(&defs, path ?: name, NULL) >= 0);
+
+ log_info("system_alloc_uid_min="UID_FMT, defs.system_alloc_uid_min);
+ log_info("system_uid_max="UID_FMT, defs.system_uid_max);
+ log_info("system_alloc_gid_min="GID_FMT, defs.system_alloc_gid_min);
+ log_info("system_gid_max="GID_FMT, defs.system_gid_max);
+
+ if (!path) {
+ uid_t offset = ENABLE_COMPAT_MUTABLE_UID_BOUNDARIES ? 5 : 0;
+ assert_se(defs.system_alloc_uid_min == SYSTEM_ALLOC_UID_MIN + offset);
+ assert_se(defs.system_uid_max == SYSTEM_UID_MAX + offset);
+ assert_se(defs.system_alloc_gid_min == SYSTEM_ALLOC_GID_MIN + offset);
+ assert_se(defs.system_gid_max == SYSTEM_GID_MAX + offset);
+ } else if (streq(path, "/dev/null")) {
+ assert_se(defs.system_alloc_uid_min == SYSTEM_ALLOC_UID_MIN);
+ assert_se(defs.system_uid_max == SYSTEM_UID_MAX);
+ assert_se(defs.system_alloc_gid_min == SYSTEM_ALLOC_GID_MIN);
+ assert_se(defs.system_gid_max == SYSTEM_GID_MAX);
+ }
+}
+
+TEST(read_login_defs) {
+ test_read_login_defs_one("/dev/null");
+ test_read_login_defs_one("/etc/login.defs");
+ test_read_login_defs_one(NULL);
+}
+
+TEST(acquire_ugid_allocation_range) {
+ const UGIDAllocationRange *defs;
+ assert_se(defs = acquire_ugid_allocation_range());
+
+ log_info("system_alloc_uid_min="UID_FMT, defs->system_alloc_uid_min);
+ log_info("system_uid_max="UID_FMT, defs->system_uid_max);
+ log_info("system_alloc_gid_min="GID_FMT, defs->system_alloc_gid_min);
+ log_info("system_gid_max="GID_FMT, defs->system_gid_max);
+}
+
+TEST(uid_is_system) {
+ uid_t uid = 0;
+ log_info("uid_is_system("UID_FMT") = %s", uid, yes_no(uid_is_system(uid)));
+
+ uid = 999;
+ log_info("uid_is_system("UID_FMT") = %s", uid, yes_no(uid_is_system(uid)));
+
+ uid = getuid();
+ log_info("uid_is_system("UID_FMT") = %s", uid, yes_no(uid_is_system(uid)));
+}
+
+TEST(gid_is_system) {
+ gid_t gid = 0;
+ log_info("gid_is_system("GID_FMT") = %s", gid, yes_no(gid_is_system(gid)));
+
+ gid = 999;
+ log_info("gid_is_system("GID_FMT") = %s", gid, yes_no(gid_is_system(gid)));
+
+ gid = getgid();
+ log_info("gid_is_system("GID_FMT") = %s", gid, yes_no(gid_is_system(gid)));
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-uid-range.c b/src/test/test-uid-range.c
new file mode 100644
index 0000000..186f6ee
--- /dev/null
+++ b/src/test/test-uid-range.c
@@ -0,0 +1,175 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stddef.h>
+
+#include "alloc-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+#include "uid-range.h"
+#include "user-util.h"
+#include "virt.h"
+
+TEST(uid_range) {
+ _cleanup_(uid_range_freep) UidRange *p = NULL;
+ uid_t search;
+
+ assert_se(uid_range_covers(p, 0, 0));
+ assert_se(!uid_range_covers(p, 0, 1));
+ assert_se(!uid_range_covers(p, 100, UINT32_MAX));
+
+ assert_se(uid_range_add_str(&p, "500-999") >= 0);
+ assert_se(p);
+ assert_se(p->n_entries == 1);
+ assert_se(p->entries[0].start == 500);
+ assert_se(p->entries[0].nr == 500);
+
+ assert_se(!uid_range_contains(p, 499));
+ assert_se(uid_range_contains(p, 500));
+ assert_se(uid_range_contains(p, 999));
+ assert_se(!uid_range_contains(p, 1000));
+
+ assert_se(!uid_range_covers(p, 100, 150));
+ assert_se(!uid_range_covers(p, 400, 200));
+ assert_se(!uid_range_covers(p, 499, 1));
+ assert_se(uid_range_covers(p, 500, 1));
+ assert_se(uid_range_covers(p, 501, 10));
+ assert_se(uid_range_covers(p, 999, 1));
+ assert_se(!uid_range_covers(p, 999, 2));
+ assert_se(!uid_range_covers(p, 1000, 1));
+ assert_se(!uid_range_covers(p, 1000, 100));
+ assert_se(!uid_range_covers(p, 1001, 100));
+
+ search = UID_INVALID;
+ assert_se(uid_range_next_lower(p, &search));
+ assert_se(search == 999);
+ assert_se(uid_range_next_lower(p, &search));
+ assert_se(search == 998);
+ search = 501;
+ assert_se(uid_range_next_lower(p, &search));
+ assert_se(search == 500);
+ assert_se(uid_range_next_lower(p, &search) == -EBUSY);
+
+ assert_se(uid_range_add_str(&p, "1000") >= 0);
+ assert_se(p->n_entries == 1);
+ assert_se(p->entries[0].start == 500);
+ assert_se(p->entries[0].nr == 501);
+
+ assert_se(uid_range_add_str(&p, "30-40") >= 0);
+ assert_se(p->n_entries == 2);
+ assert_se(p->entries[0].start == 30);
+ assert_se(p->entries[0].nr == 11);
+ assert_se(p->entries[1].start == 500);
+ assert_se(p->entries[1].nr == 501);
+
+ assert_se(uid_range_add_str(&p, "60-70") >= 0);
+ assert_se(p->n_entries == 3);
+ assert_se(p->entries[0].start == 30);
+ assert_se(p->entries[0].nr == 11);
+ assert_se(p->entries[1].start == 60);
+ assert_se(p->entries[1].nr == 11);
+ assert_se(p->entries[2].start == 500);
+ assert_se(p->entries[2].nr == 501);
+
+ assert_se(uid_range_add_str(&p, "20-2000") >= 0);
+ assert_se(p->n_entries == 1);
+ assert_se(p->entries[0].start == 20);
+ assert_se(p->entries[0].nr == 1981);
+
+ assert_se(uid_range_add_str(&p, "2002") >= 0);
+ assert_se(p->n_entries == 2);
+ assert_se(p->entries[0].start == 20);
+ assert_se(p->entries[0].nr == 1981);
+ assert_se(p->entries[1].start == 2002);
+ assert_se(p->entries[1].nr == 1);
+
+ assert_se(uid_range_add_str(&p, "2001") >= 0);
+ assert_se(p->n_entries == 1);
+ assert_se(p->entries[0].start == 20);
+ assert_se(p->entries[0].nr == 1983);
+}
+
+TEST(load_userns) {
+ _cleanup_(uid_range_freep) UidRange *p = NULL;
+ _cleanup_(unlink_and_freep) char *fn = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ int r;
+
+ r = uid_range_load_userns(&p, NULL);
+ if (r < 0 && ERRNO_IS_NOT_SUPPORTED(r))
+ return;
+
+ assert_se(r >= 0);
+ assert_se(uid_range_contains(p, getuid()));
+
+ r = running_in_userns();
+ if (r == 0) {
+ assert_se(p->n_entries == 1);
+ assert_se(p->entries[0].start == 0);
+ assert_se(p->entries[0].nr == UINT32_MAX);
+
+ assert_se(uid_range_covers(p, 0, UINT32_MAX));
+ }
+
+ assert_se(fopen_temporary_child(NULL, &f, &fn) >= 0);
+ fputs("0 0 20\n"
+ "100 0 20\n", f);
+ assert_se(fflush_and_check(f) >= 0);
+
+ p = uid_range_free(p);
+
+ assert_se(uid_range_load_userns(&p, fn) >= 0);
+
+ assert_se(uid_range_contains(p, 0));
+ assert_se(uid_range_contains(p, 19));
+ assert_se(!uid_range_contains(p, 20));
+
+ assert_se(!uid_range_contains(p, 99));
+ assert_se(uid_range_contains(p, 100));
+ assert_se(uid_range_contains(p, 119));
+ assert_se(!uid_range_contains(p, 120));
+}
+
+TEST(uid_range_coalesce) {
+ _cleanup_(uid_range_freep) UidRange *p = NULL;
+
+ for (size_t i = 0; i < 10; i++) {
+ assert_se(uid_range_add_internal(&p, i * 10, 10, /* coalesce = */ false) >= 0);
+ assert_se(uid_range_add_internal(&p, i * 10 + 5, 10, /* coalesce = */ false) >= 0);
+ }
+
+ assert_se(uid_range_add_internal(&p, 100, 1, /* coalesce = */ true) >= 0);
+ assert_se(p->n_entries == 1);
+ assert_se(p->entries[0].start == 0);
+ assert_se(p->entries[0].nr == 105);
+
+ p = uid_range_free(p);
+
+ for (size_t i = 0; i < 10; i++) {
+ assert_se(uid_range_add_internal(&p, (10 - i) * 10, 10, /* coalesce = */ false) >= 0);
+ assert_se(uid_range_add_internal(&p, (10 - i) * 10 + 5, 10, /* coalesce = */ false) >= 0);
+ }
+
+ assert_se(uid_range_add_internal(&p, 100, 1, /* coalesce = */ true) >= 0);
+ assert_se(p->n_entries == 1);
+ assert_se(p->entries[0].start == 10);
+ assert_se(p->entries[0].nr == 105);
+
+ p = uid_range_free(p);
+
+ for (size_t i = 0; i < 10; i++) {
+ assert_se(uid_range_add_internal(&p, i * 10, 10, /* coalesce = */ false) >= 0);
+ assert_se(uid_range_add_internal(&p, i * 10 + 5, 10, /* coalesce = */ false) >= 0);
+ assert_se(uid_range_add_internal(&p, (10 - i) * 10, 10, /* coalesce = */ false) >= 0);
+ assert_se(uid_range_add_internal(&p, (10 - i) * 10 + 5, 10, /* coalesce = */ false) >= 0);
+ }
+ assert_se(uid_range_add_internal(&p, 100, 1, /* coalesce = */ true) >= 0);
+ assert_se(p->n_entries == 1);
+ assert_se(p->entries[0].start == 0);
+ assert_se(p->entries[0].nr == 115);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-umask-util.c b/src/test/test-umask-util.c
new file mode 100644
index 0000000..8316dfb
--- /dev/null
+++ b/src/test/test-umask-util.c
@@ -0,0 +1,57 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "process-util.h"
+#include "tests.h"
+#include "umask-util.h"
+
+int main(int argc, char *argv[]) {
+ size_t n;
+ mode_t u, t;
+
+ test_setup_logging(LOG_DEBUG);
+
+ u = umask(0111);
+
+ n = 0;
+ WITH_UMASK(0123) {
+ assert_se(umask(000) == 0123);
+ n++;
+
+ assert_se(get_process_umask(0, &t) == 0);
+ assert_se(t == 000);
+ }
+
+ assert_se(n == 1);
+ assert_se(umask(u) == 0111);
+
+ assert_se(get_process_umask(getpid_cached(), &t) == 0);
+ assert_se(t == u);
+
+ WITH_UMASK(0135) {
+ assert_se(umask(000) == 0135);
+ n++;
+
+ assert_se(get_process_umask(0, &t) == 0);
+ assert_se(t == 000);
+ }
+
+ assert_se(n == 2);
+ assert_se(umask(0111) == u);
+
+ assert_se(get_process_umask(0, &t) == 0);
+ assert_se(t == 0111);
+
+ WITH_UMASK(0315) {
+ assert_se(umask(000) == 0315);
+ n++;
+ break;
+ }
+
+ assert_se(n == 3);
+ assert_se(umask(u) == 0111);
+
+ assert_se(get_process_umask(0, &t) == 0);
+ assert_se(t == u);
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/test/test-unaligned.c b/src/test/test-unaligned.c
new file mode 100644
index 0000000..728c193
--- /dev/null
+++ b/src/test/test-unaligned.c
@@ -0,0 +1,168 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "memory-util.h"
+#include "sparse-endian.h"
+#include "tests.h"
+#include "unaligned.h"
+
+static uint8_t data[] = {
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+};
+
+TEST(be) {
+ uint8_t scratch[16];
+
+ assert_se(unaligned_read_be16(&data[0]) == 0x0001);
+ assert_se(unaligned_read_be16(&data[1]) == 0x0102);
+
+ assert_se(unaligned_read_be32(&data[0]) == 0x00010203);
+ assert_se(unaligned_read_be32(&data[1]) == 0x01020304);
+ assert_se(unaligned_read_be32(&data[2]) == 0x02030405);
+ assert_se(unaligned_read_be32(&data[3]) == 0x03040506);
+
+ assert_se(unaligned_read_be64(&data[0]) == 0x0001020304050607);
+ assert_se(unaligned_read_be64(&data[1]) == 0x0102030405060708);
+ assert_se(unaligned_read_be64(&data[2]) == 0x0203040506070809);
+ assert_se(unaligned_read_be64(&data[3]) == 0x030405060708090a);
+ assert_se(unaligned_read_be64(&data[4]) == 0x0405060708090a0b);
+ assert_se(unaligned_read_be64(&data[5]) == 0x05060708090a0b0c);
+ assert_se(unaligned_read_be64(&data[6]) == 0x060708090a0b0c0d);
+ assert_se(unaligned_read_be64(&data[7]) == 0x0708090a0b0c0d0e);
+
+ zero(scratch);
+ unaligned_write_be16(&scratch[0], 0x0001);
+ assert_se(memcmp(&scratch[0], &data[0], sizeof(uint16_t)) == 0);
+ zero(scratch);
+ unaligned_write_be16(&scratch[1], 0x0102);
+ assert_se(memcmp(&scratch[1], &data[1], sizeof(uint16_t)) == 0);
+
+ zero(scratch);
+ unaligned_write_be32(&scratch[0], 0x00010203);
+ assert_se(memcmp(&scratch[0], &data[0], sizeof(uint32_t)) == 0);
+ zero(scratch);
+ unaligned_write_be32(&scratch[1], 0x01020304);
+ assert_se(memcmp(&scratch[1], &data[1], sizeof(uint32_t)) == 0);
+ zero(scratch);
+ unaligned_write_be32(&scratch[2], 0x02030405);
+ assert_se(memcmp(&scratch[2], &data[2], sizeof(uint32_t)) == 0);
+ zero(scratch);
+ unaligned_write_be32(&scratch[3], 0x03040506);
+ assert_se(memcmp(&scratch[3], &data[3], sizeof(uint32_t)) == 0);
+
+ zero(scratch);
+ unaligned_write_be64(&scratch[0], 0x0001020304050607);
+ assert_se(memcmp(&scratch[0], &data[0], sizeof(uint64_t)) == 0);
+ zero(scratch);
+ unaligned_write_be64(&scratch[1], 0x0102030405060708);
+ assert_se(memcmp(&scratch[1], &data[1], sizeof(uint64_t)) == 0);
+ zero(scratch);
+ unaligned_write_be64(&scratch[2], 0x0203040506070809);
+ assert_se(memcmp(&scratch[2], &data[2], sizeof(uint64_t)) == 0);
+ zero(scratch);
+ unaligned_write_be64(&scratch[3], 0x030405060708090a);
+ assert_se(memcmp(&scratch[3], &data[3], sizeof(uint64_t)) == 0);
+ zero(scratch);
+ unaligned_write_be64(&scratch[4], 0x0405060708090a0b);
+ assert_se(memcmp(&scratch[4], &data[4], sizeof(uint64_t)) == 0);
+ zero(scratch);
+ unaligned_write_be64(&scratch[5], 0x05060708090a0b0c);
+ assert_se(memcmp(&scratch[5], &data[5], sizeof(uint64_t)) == 0);
+ zero(scratch);
+ unaligned_write_be64(&scratch[6], 0x060708090a0b0c0d);
+ assert_se(memcmp(&scratch[6], &data[6], sizeof(uint64_t)) == 0);
+ zero(scratch);
+ unaligned_write_be64(&scratch[7], 0x0708090a0b0c0d0e);
+ assert_se(memcmp(&scratch[7], &data[7], sizeof(uint64_t)) == 0);
+}
+
+TEST(le) {
+ uint8_t scratch[16];
+
+ assert_se(unaligned_read_le16(&data[0]) == 0x0100);
+ assert_se(unaligned_read_le16(&data[1]) == 0x0201);
+
+ assert_se(unaligned_read_le32(&data[0]) == 0x03020100);
+ assert_se(unaligned_read_le32(&data[1]) == 0x04030201);
+ assert_se(unaligned_read_le32(&data[2]) == 0x05040302);
+ assert_se(unaligned_read_le32(&data[3]) == 0x06050403);
+
+ assert_se(unaligned_read_le64(&data[0]) == 0x0706050403020100);
+ assert_se(unaligned_read_le64(&data[1]) == 0x0807060504030201);
+ assert_se(unaligned_read_le64(&data[2]) == 0x0908070605040302);
+ assert_se(unaligned_read_le64(&data[3]) == 0x0a09080706050403);
+ assert_se(unaligned_read_le64(&data[4]) == 0x0b0a090807060504);
+ assert_se(unaligned_read_le64(&data[5]) == 0x0c0b0a0908070605);
+ assert_se(unaligned_read_le64(&data[6]) == 0x0d0c0b0a09080706);
+ assert_se(unaligned_read_le64(&data[7]) == 0x0e0d0c0b0a090807);
+
+ zero(scratch);
+ unaligned_write_le16(&scratch[0], 0x0100);
+ assert_se(memcmp(&scratch[0], &data[0], sizeof(uint16_t)) == 0);
+ zero(scratch);
+ unaligned_write_le16(&scratch[1], 0x0201);
+ assert_se(memcmp(&scratch[1], &data[1], sizeof(uint16_t)) == 0);
+
+ zero(scratch);
+ unaligned_write_le32(&scratch[0], 0x03020100);
+
+ assert_se(memcmp(&scratch[0], &data[0], sizeof(uint32_t)) == 0);
+ zero(scratch);
+ unaligned_write_le32(&scratch[1], 0x04030201);
+ assert_se(memcmp(&scratch[1], &data[1], sizeof(uint32_t)) == 0);
+ zero(scratch);
+ unaligned_write_le32(&scratch[2], 0x05040302);
+ assert_se(memcmp(&scratch[2], &data[2], sizeof(uint32_t)) == 0);
+ zero(scratch);
+ unaligned_write_le32(&scratch[3], 0x06050403);
+ assert_se(memcmp(&scratch[3], &data[3], sizeof(uint32_t)) == 0);
+
+ zero(scratch);
+ unaligned_write_le64(&scratch[0], 0x0706050403020100);
+ assert_se(memcmp(&scratch[0], &data[0], sizeof(uint64_t)) == 0);
+ zero(scratch);
+ unaligned_write_le64(&scratch[1], 0x0807060504030201);
+ assert_se(memcmp(&scratch[1], &data[1], sizeof(uint64_t)) == 0);
+ zero(scratch);
+ unaligned_write_le64(&scratch[2], 0x0908070605040302);
+ assert_se(memcmp(&scratch[2], &data[2], sizeof(uint64_t)) == 0);
+ zero(scratch);
+ unaligned_write_le64(&scratch[3], 0x0a09080706050403);
+ assert_se(memcmp(&scratch[3], &data[3], sizeof(uint64_t)) == 0);
+ zero(scratch);
+ unaligned_write_le64(&scratch[4], 0x0B0A090807060504);
+ assert_se(memcmp(&scratch[4], &data[4], sizeof(uint64_t)) == 0);
+ zero(scratch);
+ unaligned_write_le64(&scratch[5], 0x0c0b0a0908070605);
+ assert_se(memcmp(&scratch[5], &data[5], sizeof(uint64_t)) == 0);
+ zero(scratch);
+ unaligned_write_le64(&scratch[6], 0x0d0c0b0a09080706);
+ assert_se(memcmp(&scratch[6], &data[6], sizeof(uint64_t)) == 0);
+ zero(scratch);
+ unaligned_write_le64(&scratch[7], 0x0e0d0c0b0a090807);
+ assert_se(memcmp(&scratch[7], &data[7], sizeof(uint64_t)) == 0);
+}
+
+TEST(ne) {
+ uint16_t x = 4711;
+ uint32_t y = 123456;
+ uint64_t z = 9876543210;
+
+ /* Note that we don't bother actually testing alignment issues in this function, after all the _ne() functions
+ * are just aliases for the _le() or _be() implementations, which we test extensively above. Hence, in this
+ * function, just ensure that they map to the right version on the local architecture. */
+
+ assert_se(unaligned_read_ne16(&x) == 4711);
+ assert_se(unaligned_read_ne32(&y) == 123456);
+ assert_se(unaligned_read_ne64(&z) == 9876543210);
+
+ unaligned_write_ne16(&x, 1);
+ unaligned_write_ne32(&y, 2);
+ unaligned_write_ne64(&z, 3);
+
+ assert_se(x == 1);
+ assert_se(y == 2);
+ assert_se(z == 3);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-unit-file.c b/src/test/test-unit-file.c
new file mode 100644
index 0000000..9f8787b
--- /dev/null
+++ b/src/test/test-unit-file.c
@@ -0,0 +1,110 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "initrd-util.h"
+#include "path-lookup.h"
+#include "set.h"
+#include "special.h"
+#include "strv.h"
+#include "tests.h"
+#include "unit-file.h"
+
+TEST(unit_validate_alias_symlink_and_warn) {
+ assert_se(unit_validate_alias_symlink_or_warn(LOG_INFO, "/path/a.service", "/other/b.service") == 0);
+ assert_se(unit_validate_alias_symlink_or_warn(LOG_INFO, "/path/a.service", "/other/b.socket") == -EXDEV);
+ assert_se(unit_validate_alias_symlink_or_warn(LOG_INFO, "/path/a.service", "/other/b.foobar") == -EXDEV);
+ assert_se(unit_validate_alias_symlink_or_warn(LOG_INFO, "/path/a@.service", "/other/b@.service") == 0);
+ assert_se(unit_validate_alias_symlink_or_warn(LOG_INFO, "/path/a@.service", "/other/b@.socket") == -EXDEV);
+ assert_se(unit_validate_alias_symlink_or_warn(LOG_INFO, "/path/a@XXX.service", "/other/b@YYY.service") == -EXDEV);
+ assert_se(unit_validate_alias_symlink_or_warn(LOG_INFO, "/path/a@XXX.service", "/other/b@YYY.socket") == -EXDEV);
+ assert_se(unit_validate_alias_symlink_or_warn(LOG_INFO, "/path/a@.service", "/other/b@YYY.service") == -EXDEV);
+ assert_se(unit_validate_alias_symlink_or_warn(LOG_INFO, "/path/a@XXX.service", "/other/b@XXX.service") == 0);
+ assert_se(unit_validate_alias_symlink_or_warn(LOG_INFO, "/path/a@XXX.service", "/other/b@.service") == 0);
+ assert_se(unit_validate_alias_symlink_or_warn(LOG_INFO, "/path/a@.service", "/other/b.service") == -EXDEV);
+ assert_se(unit_validate_alias_symlink_or_warn(LOG_INFO, "/path/a.service", "/other/b@.service") == -EXDEV);
+ assert_se(unit_validate_alias_symlink_or_warn(LOG_INFO, "/path/a@.slice", "/other/b.slice") == -EINVAL);
+ assert_se(unit_validate_alias_symlink_or_warn(LOG_INFO, "/path/a.slice", "/other/b.slice") == -EINVAL);
+}
+
+TEST(unit_file_build_name_map) {
+ _cleanup_(lookup_paths_free) LookupPaths lp = {};
+ _cleanup_hashmap_free_ Hashmap *unit_ids = NULL;
+ _cleanup_hashmap_free_ Hashmap *unit_names = NULL;
+ const char *k, *dst;
+ char **v, **ids;
+ usec_t mtime = 0;
+ int r;
+
+ ids = strv_skip(saved_argv, 1);
+
+ assert_se(lookup_paths_init(&lp, RUNTIME_SCOPE_SYSTEM, 0, NULL) >= 0);
+
+ assert_se(unit_file_build_name_map(&lp, &mtime, &unit_ids, &unit_names, NULL) == 1);
+
+ HASHMAP_FOREACH_KEY(dst, k, unit_ids)
+ log_info("ids: %s → %s", k, dst);
+
+ HASHMAP_FOREACH_KEY(v, k, unit_names) {
+ _cleanup_free_ char *j = strv_join(v, ", ");
+ log_info("aliases: %s ← %s", k, j);
+ }
+
+ char buf[FORMAT_TIMESTAMP_MAX];
+ log_debug("Last modification time: %s", format_timestamp(buf, sizeof buf, mtime));
+
+ r = unit_file_build_name_map(&lp, &mtime, &unit_ids, &unit_names, NULL);
+ assert_se(IN_SET(r, 0, 1));
+ if (r == 0)
+ log_debug("Cache rebuild skipped based on mtime.");
+
+ STRV_FOREACH(id, ids) {
+ const char *fragment, *name;
+ _cleanup_set_free_free_ Set *names = NULL;
+ log_info("*** %s ***", *id);
+ r = unit_file_find_fragment(unit_ids,
+ unit_names,
+ *id,
+ &fragment,
+ &names);
+ assert_se(r == 0);
+ log_info("fragment: %s", fragment);
+ log_info("names:");
+ SET_FOREACH(name, names)
+ log_info(" %s", name);
+ }
+
+ /* Make sure everything still works if we don't collect names. */
+ STRV_FOREACH(id, ids) {
+ const char *fragment;
+ log_info("*** %s ***", *id);
+ r = unit_file_find_fragment(unit_ids,
+ unit_names,
+ *id,
+ &fragment,
+ NULL);
+ assert_se(r == 0);
+ log_info("fragment: %s", fragment);
+ }
+}
+
+TEST(runlevel_to_target) {
+ in_initrd_force(false);
+ assert_se(streq_ptr(runlevel_to_target(NULL), NULL));
+ assert_se(streq_ptr(runlevel_to_target("unknown-runlevel"), NULL));
+ assert_se(streq_ptr(runlevel_to_target("rd.unknown-runlevel"), NULL));
+ assert_se(streq_ptr(runlevel_to_target("3"), SPECIAL_MULTI_USER_TARGET));
+ assert_se(streq_ptr(runlevel_to_target("rd.rescue"), NULL));
+
+ in_initrd_force(true);
+ assert_se(streq_ptr(runlevel_to_target(NULL), NULL));
+ assert_se(streq_ptr(runlevel_to_target("unknown-runlevel"), NULL));
+ assert_se(streq_ptr(runlevel_to_target("rd.unknown-runlevel"), NULL));
+ assert_se(streq_ptr(runlevel_to_target("3"), NULL));
+ assert_se(streq_ptr(runlevel_to_target("rd.rescue"), SPECIAL_RESCUE_TARGET));
+}
+
+static int intro(void) {
+ log_show_color(true);
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_DEBUG, intro);
diff --git a/src/test/test-unit-name.c b/src/test/test-unit-name.c
new file mode 100644
index 0000000..8e9332c
--- /dev/null
+++ b/src/test/test-unit-name.c
@@ -0,0 +1,1009 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "sd-id128.h"
+
+#include "alloc-util.h"
+#include "all-units.h"
+#include "glob-util.h"
+#include "format-util.h"
+#include "hostname-util.h"
+#include "macro.h"
+#include "manager.h"
+#include "path-util.h"
+#include "rm-rf.h"
+#include "special.h"
+#include "specifier.h"
+#include "string-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+#include "unit-def.h"
+#include "unit-name.h"
+#include "unit-printf.h"
+#include "unit.h"
+#include "user-util.h"
+
+static char *runtime_dir = NULL;
+
+STATIC_DESTRUCTOR_REGISTER(runtime_dir, rm_rf_physical_and_freep);
+
+static void test_unit_name_is_valid_one(const char *name, UnitNameFlags flags, bool expected) {
+ log_info("%s ( %s%s%s ): %s",
+ name,
+ (flags & UNIT_NAME_PLAIN) ? "plain" : "",
+ (flags & UNIT_NAME_INSTANCE) ? " instance" : "",
+ (flags & UNIT_NAME_TEMPLATE) ? " template" : "",
+ yes_no(expected));
+ assert_se(unit_name_is_valid(name, flags) == expected);
+}
+
+TEST(unit_name_is_valid) {
+ test_unit_name_is_valid_one("foo.service", UNIT_NAME_ANY, true);
+ test_unit_name_is_valid_one("foo.service", UNIT_NAME_PLAIN, true);
+ test_unit_name_is_valid_one("foo.service", UNIT_NAME_INSTANCE, false);
+ test_unit_name_is_valid_one("foo.service", UNIT_NAME_TEMPLATE, false);
+ test_unit_name_is_valid_one("foo.service", UNIT_NAME_INSTANCE|UNIT_NAME_TEMPLATE, false);
+
+ test_unit_name_is_valid_one("foo@bar.service", UNIT_NAME_ANY, true);
+ test_unit_name_is_valid_one("foo@bar.service", UNIT_NAME_PLAIN, false);
+ test_unit_name_is_valid_one("foo@bar.service", UNIT_NAME_INSTANCE, true);
+ test_unit_name_is_valid_one("foo@bar.service", UNIT_NAME_TEMPLATE, false);
+ test_unit_name_is_valid_one("foo@bar.service", UNIT_NAME_INSTANCE|UNIT_NAME_TEMPLATE, true);
+
+ test_unit_name_is_valid_one("foo@bar@bar.service", UNIT_NAME_ANY, true);
+ test_unit_name_is_valid_one("foo@bar@bar.service", UNIT_NAME_PLAIN, false);
+ test_unit_name_is_valid_one("foo@bar@bar.service", UNIT_NAME_INSTANCE, true);
+ test_unit_name_is_valid_one("foo@bar@bar.service", UNIT_NAME_TEMPLATE, false);
+ test_unit_name_is_valid_one("foo@bar@bar.service", UNIT_NAME_INSTANCE|UNIT_NAME_TEMPLATE, true);
+
+ test_unit_name_is_valid_one("foo@.service", UNIT_NAME_ANY, true);
+ test_unit_name_is_valid_one("foo@.service", UNIT_NAME_PLAIN, false);
+ test_unit_name_is_valid_one("foo@.service", UNIT_NAME_INSTANCE, false);
+ test_unit_name_is_valid_one("foo@.service", UNIT_NAME_TEMPLATE, true);
+ test_unit_name_is_valid_one("foo@.service", UNIT_NAME_INSTANCE|UNIT_NAME_TEMPLATE, true);
+ test_unit_name_is_valid_one(".test.service", UNIT_NAME_PLAIN, true);
+ test_unit_name_is_valid_one(".test@.service", UNIT_NAME_TEMPLATE, true);
+ test_unit_name_is_valid_one("_strange::::.service", UNIT_NAME_ANY, true);
+
+ test_unit_name_is_valid_one(".service", UNIT_NAME_ANY, false);
+ test_unit_name_is_valid_one("", UNIT_NAME_ANY, false);
+ test_unit_name_is_valid_one("foo.waldo", UNIT_NAME_ANY, false);
+ test_unit_name_is_valid_one("@.service", UNIT_NAME_ANY, false);
+ test_unit_name_is_valid_one("@piep.service", UNIT_NAME_ANY, false);
+
+ test_unit_name_is_valid_one("user@1000.slice", UNIT_NAME_ANY, true);
+ test_unit_name_is_valid_one("user@1000.slice", UNIT_NAME_INSTANCE, true);
+ test_unit_name_is_valid_one("user@1000.slice", UNIT_NAME_TEMPLATE, false);
+
+ test_unit_name_is_valid_one("foo@%i.service", UNIT_NAME_ANY, false);
+ test_unit_name_is_valid_one("foo@%i.service", UNIT_NAME_INSTANCE, false);
+ test_unit_name_is_valid_one("foo@%%i.service", UNIT_NAME_INSTANCE, false);
+ test_unit_name_is_valid_one("foo@%%i%f.service", UNIT_NAME_INSTANCE, false);
+ test_unit_name_is_valid_one("foo@%F.service", UNIT_NAME_INSTANCE, false);
+
+ test_unit_name_is_valid_one("foo.target.wants/plain.service", UNIT_NAME_ANY, false);
+ test_unit_name_is_valid_one("foo.target.conf/foo.conf", UNIT_NAME_ANY, false);
+ test_unit_name_is_valid_one("foo.target.requires/plain.socket", UNIT_NAME_ANY, false);
+}
+
+static void test_unit_name_replace_instance_one(const char *pattern, const char *repl, const char *expected, int ret) {
+ _cleanup_free_ char *t = NULL;
+ assert_se(unit_name_replace_instance(pattern, repl, &t) == ret);
+ puts(strna(t));
+ assert_se(streq_ptr(t, expected));
+}
+
+TEST(unit_name_replace_instance) {
+ test_unit_name_replace_instance_one("foo@.service", "waldo", "foo@waldo.service", 0);
+ test_unit_name_replace_instance_one("foo@xyz.service", "waldo", "foo@waldo.service", 0);
+ test_unit_name_replace_instance_one("xyz", "waldo", NULL, -EINVAL);
+ test_unit_name_replace_instance_one("", "waldo", NULL, -EINVAL);
+ test_unit_name_replace_instance_one("foo.service", "waldo", NULL, -EINVAL);
+ test_unit_name_replace_instance_one(".service", "waldo", NULL, -EINVAL);
+ test_unit_name_replace_instance_one("foo@", "waldo", NULL, -EINVAL);
+ test_unit_name_replace_instance_one("@bar", "waldo", NULL, -EINVAL);
+}
+
+static void test_unit_name_from_path_one(const char *path, const char *suffix, const char *expected, int ret) {
+ _cleanup_free_ char *t = NULL;
+ int r;
+
+ assert_se(unit_name_from_path(path, suffix, &t) == ret);
+ puts(strna(t));
+ assert_se(streq_ptr(t, expected));
+
+ if (t) {
+ _cleanup_free_ char *k = NULL;
+
+ /* We don't support converting hashed unit names back to paths */
+ r = unit_name_to_path(t, &k);
+ if (r == -ENAMETOOLONG)
+ return;
+ assert(r == 0);
+
+ puts(strna(k));
+ assert_se(path_equal(k, empty_to_root(path)));
+ }
+}
+
+TEST(unit_name_is_hashed) {
+ assert_se(!unit_name_is_hashed(""));
+ assert_se(!unit_name_is_hashed("foo@bar.service"));
+ assert_se(!unit_name_is_hashed("foo@.service"));
+ assert_se(unit_name_is_hashed("waldoaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_7736d9ed33c2ec55.mount"));
+ assert_se(!unit_name_is_hashed("waldoaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_7736D9ED33C2EC55.mount"));
+ assert_se(!unit_name_is_hashed("waldoaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!7736d9ed33c2ec55.mount"));
+ assert_se(!unit_name_is_hashed("waldoaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_7736d9gd33c2ec55.mount"));
+ assert_se(!unit_name_is_hashed("waldoaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_.mount"));
+ assert_se(!unit_name_is_hashed("waldoaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_2103e1466b87f7f7@waldo.mount"));
+ assert_se(!unit_name_is_hashed("waldoaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_2103e1466b87f7f7@.mount"));
+}
+
+TEST(unit_name_from_path) {
+ test_unit_name_from_path_one("/waldo", ".mount", "waldo.mount", 0);
+ test_unit_name_from_path_one("/waldo/quuix", ".mount", "waldo-quuix.mount", 0);
+ test_unit_name_from_path_one("/waldo/quuix/", ".mount", "waldo-quuix.mount", 0);
+ test_unit_name_from_path_one("", ".mount", "-.mount", 0);
+ test_unit_name_from_path_one("/", ".mount", "-.mount", 0);
+ test_unit_name_from_path_one("///", ".mount", "-.mount", 0);
+ test_unit_name_from_path_one("/foo/../bar", ".mount", NULL, -EINVAL);
+ test_unit_name_from_path_one("/foo/./bar", ".mount", "foo-bar.mount", 0);
+ test_unit_name_from_path_one("/waldoaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ".mount",
+ "waldoaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa_7736d9ed33c2ec55.mount", 0);
+}
+
+static void test_unit_name_from_path_instance_one(const char *pattern, const char *path, const char *suffix, const char *expected, int ret) {
+ _cleanup_free_ char *t = NULL;
+
+ assert_se(unit_name_from_path_instance(pattern, path, suffix, &t) == ret);
+ puts(strna(t));
+ assert_se(streq_ptr(t, expected));
+
+ if (t) {
+ _cleanup_free_ char *k = NULL, *v = NULL;
+
+ assert_se(unit_name_to_instance(t, &k) > 0);
+ assert_se(unit_name_path_unescape(k, &v) == 0);
+ assert_se(path_equal(v, empty_to_root(path)));
+ }
+}
+
+TEST(unit_name_from_path_instance) {
+ test_unit_name_from_path_instance_one("waldo", "/waldo", ".mount", "waldo@waldo.mount", 0);
+ test_unit_name_from_path_instance_one("waldo", "/waldo////quuix////", ".mount", "waldo@waldo-quuix.mount", 0);
+ test_unit_name_from_path_instance_one("waldo", "/", ".mount", "waldo@-.mount", 0);
+ test_unit_name_from_path_instance_one("waldo", "", ".mount", "waldo@-.mount", 0);
+ test_unit_name_from_path_instance_one("waldo", "///", ".mount", "waldo@-.mount", 0);
+ test_unit_name_from_path_instance_one("waldo", "..", ".mount", NULL, -EINVAL);
+ test_unit_name_from_path_instance_one("waldo", "/foo", ".waldi", NULL, -EINVAL);
+ test_unit_name_from_path_instance_one("wa--ldo", "/--", ".mount", "wa--ldo@\\x2d\\x2d.mount", 0);
+}
+
+static void test_unit_name_to_path_one(const char *unit, const char *path, int ret) {
+ _cleanup_free_ char *p = NULL;
+
+ assert_se(unit_name_to_path(unit, &p) == ret);
+ assert_se(streq_ptr(path, p));
+}
+
+TEST(unit_name_to_path) {
+ test_unit_name_to_path_one("home.mount", "/home", 0);
+ test_unit_name_to_path_one("home-lennart.mount", "/home/lennart", 0);
+ test_unit_name_to_path_one("home-lennart-.mount", NULL, -EINVAL);
+ test_unit_name_to_path_one("-home-lennart.mount", NULL, -EINVAL);
+ test_unit_name_to_path_one("-home--lennart.mount", NULL, -EINVAL);
+ test_unit_name_to_path_one("home-..-lennart.mount", NULL, -EINVAL);
+ test_unit_name_to_path_one("", NULL, -EINVAL);
+ test_unit_name_to_path_one("home/foo", NULL, -EINVAL);
+}
+
+static void test_unit_name_mangle_one(bool allow_globs, const char *pattern, const char *expect, int ret) {
+ _cleanup_free_ char *t = NULL;
+ int r;
+
+ r = unit_name_mangle(pattern, (allow_globs * UNIT_NAME_MANGLE_GLOB) | UNIT_NAME_MANGLE_WARN, &t);
+ log_debug("%s: %s -> %d, %s", __func__, pattern, r, strnull(t));
+
+ assert_se(r == ret);
+ puts(strna(t));
+ assert_se(streq_ptr(t, expect));
+
+ if (t) {
+ _cleanup_free_ char *k = NULL;
+
+ assert_se(unit_name_is_valid(t, UNIT_NAME_ANY) ||
+ (allow_globs && string_is_glob(t)));
+
+ assert_se(unit_name_mangle(t, (allow_globs * UNIT_NAME_MANGLE_GLOB) | UNIT_NAME_MANGLE_WARN, &k) == 0);
+ assert_se(streq_ptr(t, k));
+ }
+}
+
+TEST(unit_name_mangle) {
+ test_unit_name_mangle_one(false, "foo.service", "foo.service", 0);
+ test_unit_name_mangle_one(false, "/home", "home.mount", 1);
+ test_unit_name_mangle_one(false, "/dev/sda", "dev-sda.device", 1);
+ test_unit_name_mangle_one(false, "üxknürz.service", "\\xc3\\xbcxkn\\xc3\\xbcrz.service", 1);
+ test_unit_name_mangle_one(false, "foobar-meh...waldi.service", "foobar-meh...waldi.service", 0);
+ test_unit_name_mangle_one(false, "_____####----.....service", "_____\\x23\\x23\\x23\\x23----.....service", 1);
+ test_unit_name_mangle_one(false, "_____##@;;;,,,##----.....service", "_____\\x23\\x23@\\x3b\\x3b\\x3b\\x2c\\x2c\\x2c\\x23\\x23----.....service", 1);
+ test_unit_name_mangle_one(false, "xxx@@@@/////\\\\\\\\\\yyy.service", "xxx@@@@-----\\\\\\\\\\yyy.service", 1);
+ test_unit_name_mangle_one(false, "", NULL, -EINVAL);
+
+ test_unit_name_mangle_one(true, "foo.service", "foo.service", 0);
+ test_unit_name_mangle_one(true, "foo", "foo.service", 1);
+ test_unit_name_mangle_one(true, "foo*", "foo*", 0);
+ test_unit_name_mangle_one(true, "ü*", "\\xc3\\xbc*", 1);
+}
+
+static void test_unit_name_mangle_with_suffix_one(const char *arg, int expected, const char *expected_name) {
+ _cleanup_free_ char *s = NULL;
+ int r;
+
+ r = unit_name_mangle_with_suffix(arg, NULL, 0, ".service", &s);
+ log_debug("%s: %s -> %d, %s", __func__, arg, r, strnull(s));
+
+ assert_se(r == expected);
+ assert_se(streq_ptr(s, expected_name));
+}
+
+TEST(unit_name_mangle_with_suffix) {
+ test_unit_name_mangle_with_suffix_one("", -EINVAL, NULL);
+
+ test_unit_name_mangle_with_suffix_one("/dev", 1, "dev.mount");
+ test_unit_name_mangle_with_suffix_one("/../dev", 1, "dev.mount");
+ test_unit_name_mangle_with_suffix_one("/../dev/.", 1, "dev.mount");
+ /* We don't skip the last '..', and it makes this an invalid device or mount name */
+ test_unit_name_mangle_with_suffix_one("/.././dev/..", 1, "-..-.-dev-...service");
+ test_unit_name_mangle_with_suffix_one("/.././dev", 1, "dev.mount");
+ test_unit_name_mangle_with_suffix_one("/./.././../dev/", 1, "dev.mount");
+
+ test_unit_name_mangle_with_suffix_one("/dev/sda", 1, "dev-sda.device");
+ test_unit_name_mangle_with_suffix_one("/dev/sda5", 1, "dev-sda5.device");
+
+ test_unit_name_mangle_with_suffix_one("/sys", 1, "sys.mount");
+ test_unit_name_mangle_with_suffix_one("/../sys", 1, "sys.mount");
+ test_unit_name_mangle_with_suffix_one("/../sys/.", 1, "sys.mount");
+ /* We don't skip the last '..', and it makes this an invalid device or mount name */
+ test_unit_name_mangle_with_suffix_one("/.././sys/..", 1, "-..-.-sys-...service");
+ test_unit_name_mangle_with_suffix_one("/.././sys", 1, "sys.mount");
+ test_unit_name_mangle_with_suffix_one("/./.././../sys/", 1, "sys.mount");
+
+ test_unit_name_mangle_with_suffix_one("/proc", 1, "proc.mount");
+ test_unit_name_mangle_with_suffix_one("/../proc", 1, "proc.mount");
+ test_unit_name_mangle_with_suffix_one("/../proc/.", 1, "proc.mount");
+ /* We don't skip the last '..', and it makes this an invalid device or mount name */
+ test_unit_name_mangle_with_suffix_one("/.././proc/..", 1, "-..-.-proc-...service");
+ test_unit_name_mangle_with_suffix_one("/.././proc", 1, "proc.mount");
+ test_unit_name_mangle_with_suffix_one("/./.././../proc/", 1, "proc.mount");
+}
+
+TEST_RET(unit_printf, .sd_booted = true) {
+ _cleanup_free_ char
+ *architecture, *os_image_version, *boot_id = NULL, *os_build_id,
+ *hostname, *short_hostname, *pretty_hostname,
+ *machine_id = NULL, *os_image_id, *os_id, *os_version_id, *os_variant_id,
+ *user, *group, *uid, *gid, *home, *shell,
+ *tmp_dir, *var_tmp_dir;
+ _cleanup_(manager_freep) Manager *m = NULL;
+ _cleanup_close_ int fd = -EBADF;
+ Unit *u;
+ int r;
+
+ _cleanup_(unlink_tempfilep) char filename[] = "/tmp/test-unit_printf.XXXXXX";
+ fd = mkostemp_safe(filename);
+ assert_se(fd >= 0);
+
+ /* Using the specifier functions is admittedly a bit circular, but we don't want to reimplement the
+ * logic a second time. We're at least testing that the hookup works. */
+ assert_se(specifier_architecture('a', NULL, NULL, NULL, &architecture) >= 0);
+ assert_se(architecture);
+ assert_se(specifier_os_image_version('A', NULL, NULL, NULL, &os_image_version) >= 0);
+ if (sd_booted() > 0) {
+ assert_se(specifier_boot_id('b', NULL, NULL, NULL, &boot_id) >= 0);
+ assert_se(boot_id);
+ }
+ assert_se(specifier_os_build_id('B', NULL, NULL, NULL, &os_build_id) >= 0);
+ assert_se(hostname = gethostname_malloc());
+ assert_se(specifier_short_hostname('l', NULL, NULL, NULL, &short_hostname) == 0);
+ assert_se(short_hostname);
+ assert_se(specifier_pretty_hostname('q', NULL, NULL, NULL, &pretty_hostname) == 0);
+ assert_se(pretty_hostname);
+ if (sd_id128_get_machine(NULL) >= 0) {
+ assert_se(specifier_machine_id('m', NULL, NULL, NULL, &machine_id) >= 0);
+ assert_se(machine_id);
+ }
+ assert_se(specifier_os_image_id('M', NULL, NULL, NULL, &os_image_id) >= 0);
+ assert_se(specifier_os_id('o', NULL, NULL, NULL, &os_id) >= 0);
+ assert_se(specifier_os_version_id('w', NULL, NULL, NULL, &os_version_id) >= 0);
+ assert_se(specifier_os_variant_id('W', NULL, NULL, NULL, &os_variant_id) >= 0);
+ assert_se(user = uid_to_name(getuid()));
+ assert_se(group = gid_to_name(getgid()));
+ assert_se(asprintf(&uid, UID_FMT, getuid()));
+ assert_se(asprintf(&gid, UID_FMT, getgid()));
+ assert_se(get_home_dir(&home) >= 0);
+ assert_se(get_shell(&shell) >= 0);
+ assert_se(specifier_tmp_dir('T', NULL, NULL, NULL, &tmp_dir) >= 0);
+ assert_se(tmp_dir);
+ assert_se(specifier_var_tmp_dir('V', NULL, NULL, NULL, &var_tmp_dir) >= 0);
+ assert_se(var_tmp_dir);
+
+ r = manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_MINIMAL, &m);
+ if (manager_errno_skip_test(r))
+ return log_tests_skipped_errno(r, "manager_new");
+ assert_se(r == 0);
+
+ assert_se(free_and_strdup(&m->cgroup_root, "/cgroup-root") == 1);
+
+#define expect(unit, pattern, _expected) \
+ { \
+ _cleanup_free_ char *t = NULL; \
+ assert_se(unit_full_printf(unit, pattern, &t) >= 0); \
+ const char *expected = strempty(_expected); \
+ printf("%s: result: %s\n expect: %s\n", pattern, t, expected); \
+ assert_se(fnmatch(expected, t, FNM_NOESCAPE) == 0); \
+ }
+
+ assert_se(u = unit_new(m, sizeof(Service)));
+ assert_se(unit_add_name(u, "blah.service") == 0);
+ assert_se(unit_add_name(u, "blah.service") == 0);
+
+ /* We need *a* file that exists, but it doesn't even need to have the right suffix. */
+ assert_se(free_and_strdup(&u->fragment_path, filename) == 1);
+
+ /* This sets the slice to /app.slice. */
+ assert_se(unit_set_default_slice(u) == 1);
+
+ /* general tests */
+ expect(u, "%%", "%");
+ expect(u, "%%s", "%s");
+ expect(u, "%,", "%,");
+ expect(u, "%", "%");
+
+ /* normal unit */
+ expect(u, "%a", architecture);
+ expect(u, "%A", os_image_version);
+ if (boot_id)
+ expect(u, "%b", boot_id);
+ expect(u, "%B", os_build_id);
+ expect(u, "%H", hostname);
+ expect(u, "%l", short_hostname);
+ expect(u, "%q", pretty_hostname);
+ if (machine_id)
+ expect(u, "%m", machine_id);
+ expect(u, "%M", os_image_id);
+ expect(u, "%o", os_id);
+ expect(u, "%w", os_version_id);
+ expect(u, "%W", os_variant_id);
+ expect(u, "%g", group);
+ expect(u, "%G", gid);
+ expect(u, "%u", user);
+ expect(u, "%U", uid);
+ expect(u, "%T", tmp_dir);
+ expect(u, "%V", var_tmp_dir);
+
+ expect(u, "%i", "");
+ expect(u, "%I", "");
+ expect(u, "%j", "blah");
+ expect(u, "%J", "blah");
+ expect(u, "%n", "blah.service");
+ expect(u, "%N", "blah");
+ expect(u, "%p", "blah");
+ expect(u, "%P", "blah");
+ expect(u, "%f", "/blah");
+ expect(u, "%y", filename);
+ expect(u, "%Y", "/tmp");
+ expect(u, "%C", m->prefix[EXEC_DIRECTORY_CACHE]);
+ expect(u, "%d", "*/credentials/blah.service");
+ expect(u, "%E", m->prefix[EXEC_DIRECTORY_CONFIGURATION]);
+ expect(u, "%L", m->prefix[EXEC_DIRECTORY_LOGS]);
+ expect(u, "%S", m->prefix[EXEC_DIRECTORY_STATE]);
+ expect(u, "%t", m->prefix[EXEC_DIRECTORY_RUNTIME]);
+ expect(u, "%h", home);
+ expect(u, "%s", shell);
+
+ /* deprecated */
+ expect(u, "%c", "/cgroup-root/app.slice/blah.service");
+ expect(u, "%r", "/cgroup-root/app.slice");
+ expect(u, "%R", "/cgroup-root");
+
+ /* templated */
+ assert_se(u = unit_new(m, sizeof(Service)));
+ assert_se(unit_add_name(u, "blah@foo-foo.service") == 0);
+ assert_se(unit_add_name(u, "blah@foo-foo.service") == 0);
+
+ assert_se(free_and_strdup(&u->fragment_path, filename) == 1);
+
+ /* This sets the slice to /app.slice/app-blah.slice. */
+ assert_se(unit_set_default_slice(u) == 1);
+
+ expect(u, "%i", "foo-foo");
+ expect(u, "%I", "foo/foo");
+ expect(u, "%j", "blah");
+ expect(u, "%J", "blah");
+ expect(u, "%n", "blah@foo-foo.service");
+ expect(u, "%N", "blah@foo-foo");
+ expect(u, "%p", "blah");
+ expect(u, "%P", "blah");
+ expect(u, "%f", "/foo/foo");
+ expect(u, "%y", filename);
+ expect(u, "%Y", "/tmp");
+ expect(u, "%C", m->prefix[EXEC_DIRECTORY_CACHE]);
+ expect(u, "%d", "*/credentials/blah@foo-foo.service");
+ expect(u, "%E", m->prefix[EXEC_DIRECTORY_CONFIGURATION]);
+ expect(u, "%L", m->prefix[EXEC_DIRECTORY_LOGS]);
+ expect(u, "%S", m->prefix[EXEC_DIRECTORY_STATE]);
+ expect(u, "%t", m->prefix[EXEC_DIRECTORY_RUNTIME]);
+ expect(u, "%h", home);
+ expect(u, "%s", shell);
+
+ /* deprecated */
+ expect(u, "%c", "/cgroup-root/app.slice/app-blah.slice/blah@foo-foo.service");
+ expect(u, "%r", "/cgroup-root/app.slice/app-blah.slice");
+ expect(u, "%R", "/cgroup-root");
+
+ /* templated with components */
+ assert_se(u = unit_new(m, sizeof(Slice)));
+ assert_se(unit_add_name(u, "blah-blah\\x2d.slice") == 0);
+
+ expect(u, "%i", "");
+ expect(u, "%I", "");
+ expect(u, "%j", "blah\\x2d");
+ expect(u, "%J", "blah-");
+ expect(u, "%n", "blah-blah\\x2d.slice");
+ expect(u, "%N", "blah-blah\\x2d");
+ expect(u, "%p", "blah-blah\\x2d");
+ expect(u, "%P", "blah/blah-");
+ expect(u, "%f", "/blah/blah-");
+
+ /* deprecated */
+ expect(u, "%c", "/cgroup-root/blah-blah\\x2d.slice");
+ expect(u, "%r", "/cgroup-root");
+ expect(u, "%R", "/cgroup-root");
+
+#undef expect
+
+ return 0;
+}
+
+TEST(unit_instance_is_valid) {
+ assert_se(unit_instance_is_valid("fooBar"));
+ assert_se(unit_instance_is_valid("foo-bar"));
+ assert_se(unit_instance_is_valid("foo.stUff"));
+ assert_se(unit_instance_is_valid("fOo123.stuff"));
+ assert_se(unit_instance_is_valid("@f_oo123.Stuff"));
+
+ assert_se(!unit_instance_is_valid("$¢£"));
+ assert_se(!unit_instance_is_valid(""));
+ assert_se(!unit_instance_is_valid("foo bar"));
+ assert_se(!unit_instance_is_valid("foo/bar"));
+}
+
+TEST(unit_prefix_is_valid) {
+ assert_se(unit_prefix_is_valid("fooBar"));
+ assert_se(unit_prefix_is_valid("foo-bar"));
+ assert_se(unit_prefix_is_valid("foo.stUff"));
+ assert_se(unit_prefix_is_valid("fOo123.stuff"));
+ assert_se(unit_prefix_is_valid("foo123.Stuff"));
+
+ assert_se(!unit_prefix_is_valid("$¢£"));
+ assert_se(!unit_prefix_is_valid(""));
+ assert_se(!unit_prefix_is_valid("foo bar"));
+ assert_se(!unit_prefix_is_valid("foo/bar"));
+ assert_se(!unit_prefix_is_valid("@foo-bar"));
+}
+
+TEST(unit_name_change_suffix) {
+ char *t;
+
+ assert_se(unit_name_change_suffix("foo.mount", ".service", &t) == 0);
+ assert_se(streq(t, "foo.service"));
+ free(t);
+
+ assert_se(unit_name_change_suffix("foo@stuff.service", ".socket", &t) == 0);
+ assert_se(streq(t, "foo@stuff.socket"));
+ free(t);
+}
+
+TEST(unit_name_build) {
+ char *t;
+
+ assert_se(unit_name_build("foo", "bar", ".service", &t) == 0);
+ assert_se(streq(t, "foo@bar.service"));
+ free(t);
+
+ assert_se(unit_name_build("fo0-stUff_b", "bar", ".mount", &t) == 0);
+ assert_se(streq(t, "fo0-stUff_b@bar.mount"));
+ free(t);
+
+ assert_se(unit_name_build("foo", NULL, ".service", &t) == 0);
+ assert_se(streq(t, "foo.service"));
+ free(t);
+}
+
+TEST(slice_name_is_valid) {
+ assert_se( slice_name_is_valid(SPECIAL_ROOT_SLICE));
+ assert_se( slice_name_is_valid("foo.slice"));
+ assert_se( slice_name_is_valid("foo-bar.slice"));
+ assert_se( slice_name_is_valid("foo-bar-baz.slice"));
+ assert_se(!slice_name_is_valid("-foo-bar-baz.slice"));
+ assert_se(!slice_name_is_valid("foo-bar-baz-.slice"));
+ assert_se(!slice_name_is_valid("-foo-bar-baz-.slice"));
+ assert_se(!slice_name_is_valid("foo-bar--baz.slice"));
+ assert_se(!slice_name_is_valid("foo--bar--baz.slice"));
+ assert_se(!slice_name_is_valid(".slice"));
+ assert_se(!slice_name_is_valid(""));
+ assert_se(!slice_name_is_valid("foo.service"));
+
+ assert_se(!slice_name_is_valid("foo@.slice"));
+ assert_se(!slice_name_is_valid("foo@bar.slice"));
+ assert_se(!slice_name_is_valid("foo-bar@baz.slice"));
+ assert_se(!slice_name_is_valid("foo@bar@baz.slice"));
+ assert_se(!slice_name_is_valid("foo@bar-baz.slice"));
+ assert_se(!slice_name_is_valid("-foo-bar-baz@.slice"));
+ assert_se(!slice_name_is_valid("foo-bar-baz@-.slice"));
+ assert_se(!slice_name_is_valid("foo-bar-baz@a--b.slice"));
+ assert_se(!slice_name_is_valid("-foo-bar-baz@-.slice"));
+ assert_se(!slice_name_is_valid("foo-bar--baz@.slice"));
+ assert_se(!slice_name_is_valid("foo--bar--baz@.slice"));
+ assert_se(!slice_name_is_valid("@.slice"));
+ assert_se(!slice_name_is_valid("foo@bar.service"));
+}
+
+TEST(build_subslice) {
+ char *a;
+ char *b;
+
+ assert_se(slice_build_subslice(SPECIAL_ROOT_SLICE, "foo", &a) >= 0);
+ assert_se(slice_build_subslice(a, "bar", &b) >= 0);
+ free(a);
+ assert_se(slice_build_subslice(b, "barfoo", &a) >= 0);
+ free(b);
+ assert_se(slice_build_subslice(a, "foobar", &b) >= 0);
+ free(a);
+ assert_se(streq(b, "foo-bar-barfoo-foobar.slice"));
+ free(b);
+
+ assert_se(slice_build_subslice("foo.service", "bar", &a) < 0);
+ assert_se(slice_build_subslice("foo", "bar", &a) < 0);
+}
+
+static void test_build_parent_slice_one(const char *name, const char *expect, int ret) {
+ _cleanup_free_ char *s = NULL;
+
+ assert_se(slice_build_parent_slice(name, &s) == ret);
+ assert_se(streq_ptr(s, expect));
+}
+
+TEST(build_parent_slice) {
+ test_build_parent_slice_one(SPECIAL_ROOT_SLICE, NULL, 0);
+ test_build_parent_slice_one("foo.slice", SPECIAL_ROOT_SLICE, 1);
+ test_build_parent_slice_one("foo-bar.slice", "foo.slice", 1);
+ test_build_parent_slice_one("foo-bar-baz.slice", "foo-bar.slice", 1);
+ test_build_parent_slice_one("foo-bar--baz.slice", NULL, -EINVAL);
+ test_build_parent_slice_one("-foo-bar.slice", NULL, -EINVAL);
+ test_build_parent_slice_one("foo-bar-.slice", NULL, -EINVAL);
+ test_build_parent_slice_one("foo-bar.service", NULL, -EINVAL);
+ test_build_parent_slice_one(".slice", NULL, -EINVAL);
+ test_build_parent_slice_one("foo@bar.slice", NULL, -EINVAL);
+ test_build_parent_slice_one("foo-bar@baz.slice", NULL, -EINVAL);
+ test_build_parent_slice_one("foo-bar--@baz.slice", NULL, -EINVAL);
+ test_build_parent_slice_one("-foo-bar@bar.slice", NULL, -EINVAL);
+ test_build_parent_slice_one("foo-bar@-.slice", NULL, -EINVAL);
+ test_build_parent_slice_one("foo@bar.service", NULL, -EINVAL);
+ test_build_parent_slice_one("@.slice", NULL, -EINVAL);
+}
+
+TEST(unit_name_to_instance) {
+ UnitNameFlags r;
+ char *instance;
+
+ r = unit_name_to_instance("foo@bar.service", &instance);
+ assert_se(r == UNIT_NAME_INSTANCE);
+ assert_se(streq(instance, "bar"));
+ free(instance);
+
+ r = unit_name_to_instance("foo@.service", &instance);
+ assert_se(r == UNIT_NAME_TEMPLATE);
+ assert_se(streq(instance, ""));
+ free(instance);
+
+ r = unit_name_to_instance("fo0-stUff_b@b.service", &instance);
+ assert_se(r == UNIT_NAME_INSTANCE);
+ assert_se(streq(instance, "b"));
+ free(instance);
+
+ r = unit_name_to_instance("foo.service", &instance);
+ assert_se(r == UNIT_NAME_PLAIN);
+ assert_se(!instance);
+
+ r = unit_name_to_instance("fooj@unk", &instance);
+ assert_se(r < 0);
+ assert_se(!instance);
+
+ r = unit_name_to_instance("foo@", &instance);
+ assert_se(r < 0);
+ assert_se(!instance);
+}
+
+TEST(unit_name_escape) {
+ _cleanup_free_ char *r = NULL;
+
+ r = unit_name_escape("ab+-c.a/bc@foo.service");
+ assert_se(r);
+ assert_se(streq(r, "ab\\x2b\\x2dc.a-bc\\x40foo.service"));
+}
+
+static void test_u_n_t_one(const char *name, const char *expected, int ret) {
+ _cleanup_free_ char *f = NULL;
+
+ assert_se(unit_name_template(name, &f) == ret);
+ printf("got: %s, expected: %s\n", strna(f), strna(expected));
+ assert_se(streq_ptr(f, expected));
+}
+
+TEST(unit_name_template) {
+ test_u_n_t_one("foo@bar.service", "foo@.service", 0);
+ test_u_n_t_one("foo.mount", NULL, -EINVAL);
+}
+
+static void test_unit_name_path_unescape_one(const char *name, const char *path, int ret) {
+ _cleanup_free_ char *p = NULL;
+
+ assert_se(unit_name_path_unescape(name, &p) == ret);
+ assert_se(streq_ptr(path, p));
+}
+
+TEST(unit_name_path_unescape) {
+ test_unit_name_path_unescape_one("foo", "/foo", 0);
+ test_unit_name_path_unescape_one("foo-bar", "/foo/bar", 0);
+ test_unit_name_path_unescape_one("foo-.bar", "/foo/.bar", 0);
+ test_unit_name_path_unescape_one("foo-bar-baz", "/foo/bar/baz", 0);
+ test_unit_name_path_unescape_one("-", "/", 0);
+ test_unit_name_path_unescape_one("--", NULL, -EINVAL);
+ test_unit_name_path_unescape_one("-foo-bar", NULL, -EINVAL);
+ test_unit_name_path_unescape_one("foo--bar", NULL, -EINVAL);
+ test_unit_name_path_unescape_one("foo-bar-", NULL, -EINVAL);
+ test_unit_name_path_unescape_one(".-bar", NULL, -EINVAL);
+ test_unit_name_path_unescape_one("foo-..", NULL, -EINVAL);
+ test_unit_name_path_unescape_one("", NULL, -EINVAL);
+}
+
+static void test_unit_name_to_prefix_one(const char *input, int ret, const char *output) {
+ _cleanup_free_ char *k = NULL;
+
+ assert_se(unit_name_to_prefix(input, &k) == ret);
+ assert_se(streq_ptr(k, output));
+}
+
+TEST(unit_name_to_prefix) {
+ test_unit_name_to_prefix_one("foobar.service", 0, "foobar");
+ test_unit_name_to_prefix_one("", -EINVAL, NULL);
+ test_unit_name_to_prefix_one("foobar", -EINVAL, NULL);
+ test_unit_name_to_prefix_one(".service", -EINVAL, NULL);
+ test_unit_name_to_prefix_one("quux.quux", -EINVAL, NULL);
+ test_unit_name_to_prefix_one("quux.mount", 0, "quux");
+ test_unit_name_to_prefix_one("quux-quux.mount", 0, "quux-quux");
+ test_unit_name_to_prefix_one("quux@bar.mount", 0, "quux");
+ test_unit_name_to_prefix_one("quux-@.mount", 0, "quux-");
+ test_unit_name_to_prefix_one("@.mount", -EINVAL, NULL);
+}
+
+static void test_unit_name_from_dbus_path_one(const char *input, int ret, const char *output) {
+ _cleanup_free_ char *k = NULL;
+
+ assert_se(unit_name_from_dbus_path(input, &k) == ret);
+ assert_se(streq_ptr(k, output));
+}
+
+TEST(unit_name_from_dbus_path) {
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dbus_2esocket", 0, "dbus.socket");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/_2d_2emount", 0, "-.mount");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/_2d_2eslice", 0, "-.slice");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/accounts_2ddaemon_2eservice", 0, "accounts-daemon.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/auditd_2eservice", 0, "auditd.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/basic_2etarget", 0, "basic.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/bluetooth_2etarget", 0, "bluetooth.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/boot_2eautomount", 0, "boot.automount");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/boot_2emount", 0, "boot.mount");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/btrfs_2emount", 0, "btrfs.mount");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/cryptsetup_2dpre_2etarget", 0, "cryptsetup-pre.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/cryptsetup_2etarget", 0, "cryptsetup.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dbus_2eservice", 0, "dbus.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dbus_2esocket", 0, "dbus.socket");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dcdrom_2edevice", 0, "dev-cdrom.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2did_2data_5cx2dINTEL_5fSSDSA2M120G2GC_5fCVPO044405HH120QGN_2edevice", 0, "dev-disk-by\\x2did-ata\\x2dINTEL_SSDSA2M120G2GC_CVPO044405HH120QGN.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2did_2data_5cx2dINTEL_5fSSDSA2M120G2GC_5fCVPO044405HH120QGN_5cx2dpart1_2edevice", 0, "dev-disk-by\\x2did-ata\\x2dINTEL_SSDSA2M120G2GC_CVPO044405HH120QGN\\x2dpart1.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2did_2data_5cx2dINTEL_5fSSDSA2M160G2GC_5fCVPO951003RY160AGN_2edevice", 0, "dev-disk-by\\x2did-ata\\x2dINTEL_SSDSA2M160G2GC_CVPO951003RY160AGN.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2did_2data_5cx2dINTEL_5fSSDSA2M160G2GC_5fCVPO951003RY160AGN_5cx2dpart1_2edevice", 0, "dev-disk-by\\x2did-ata\\x2dINTEL_SSDSA2M160G2GC_CVPO951003RY160AGN\\x2dpart1.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2did_2data_5cx2dINTEL_5fSSDSA2M160G2GC_5fCVPO951003RY160AGN_5cx2dpart2_2edevice", 0, "dev-disk-by\\x2did-ata\\x2dINTEL_SSDSA2M160G2GC_CVPO951003RY160AGN\\x2dpart2.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2did_2data_5cx2dINTEL_5fSSDSA2M160G2GC_5fCVPO951003RY160AGN_5cx2dpart3_2edevice", 0, "dev-disk-by\\x2did-ata\\x2dINTEL_SSDSA2M160G2GC_CVPO951003RY160AGN\\x2dpart3.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2did_2data_5cx2dTSSTcorp_5fCDDVDW_5fTS_5cx2dL633C_5fR6176GLZB14646_2edevice", 0, "dev-disk-by\\x2did-ata\\x2dTSSTcorp_CDDVDW_TS\\x2dL633C_R6176GLZB14646.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2did_2dwwn_5cx2d0x50015179591245ae_2edevice", 0, "dev-disk-by\\x2did-wwn\\x2d0x50015179591245ae.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2did_2dwwn_5cx2d0x50015179591245ae_5cx2dpart1_2edevice", 0, "dev-disk-by\\x2did-wwn\\x2d0x50015179591245ae\\x2dpart1.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2did_2dwwn_5cx2d0x50015179591245ae_5cx2dpart2_2edevice", 0, "dev-disk-by\\x2did-wwn\\x2d0x50015179591245ae\\x2dpart2.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2did_2dwwn_5cx2d0x50015179591245ae_5cx2dpart3_2edevice", 0, "dev-disk-by\\x2did-wwn\\x2d0x50015179591245ae\\x2dpart3.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2did_2dwwn_5cx2d0x500151795946eab5_2edevice", 0, "dev-disk-by\\x2did-wwn\\x2d0x500151795946eab5.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2did_2dwwn_5cx2d0x500151795946eab5_5cx2dpart1_2edevice", 0, "dev-disk-by\\x2did-wwn\\x2d0x500151795946eab5\\x2dpart1.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2dlabel_2d_5cxe3_5cx82_5cxb7_5cxe3_5cx82_5cxb9_5cxe3_5cx83_5cx86_5cxe3_5cx83_5cxa0_5cxe3_5cx81_5cxa7_5cxe4_5cxba_5cx88_5cxe7_5cxb4_5cx84_5cxe6_5cxb8_5cx88_5cxe3_5cx81_5cxbf_2edevice", 0, "dev-disk-by\\x2dlabel-\\xe3\\x82\\xb7\\xe3\\x82\\xb9\\xe3\\x83\\x86\\xe3\\x83\\xa0\\xe3\\x81\\xa7\\xe4\\xba\\x88\\xe7\\xb4\\x84\\xe6\\xb8\\x88\\xe3\\x81\\xbf.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2dpartuuid_2d59834e50_5cx2d01_2edevice", 0, "dev-disk-by\\x2dpartuuid-59834e50\\x2d01.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2dpartuuid_2d63e2a7b3_5cx2d01_2edevice", 0, "dev-disk-by\\x2dpartuuid-63e2a7b3\\x2d01.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2dpartuuid_2d63e2a7b3_5cx2d02_2edevice", 0, "dev-disk-by\\x2dpartuuid-63e2a7b3\\x2d02.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2dpartuuid_2d63e2a7b3_5cx2d03_2edevice", 0, "dev-disk-by\\x2dpartuuid-63e2a7b3\\x2d03.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2dpath_2dpci_5cx2d0000_3a00_3a1f_2e2_5cx2data_5cx2d1_2edevice", 0, "dev-disk-by\\x2dpath-pci\\x2d0000:00:1f.2\\x2data\\x2d1.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2dpath_2dpci_5cx2d0000_3a00_3a1f_2e2_5cx2data_5cx2d1_5cx2dpart1_2edevice", 0, "dev-disk-by\\x2dpath-pci\\x2d0000:00:1f.2\\x2data\\x2d1\\x2dpart1.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2dpath_2dpci_5cx2d0000_3a00_3a1f_2e2_5cx2data_5cx2d1_5cx2dpart2_2edevice", 0, "dev-disk-by\\x2dpath-pci\\x2d0000:00:1f.2\\x2data\\x2d1\\x2dpart2.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2dpath_2dpci_5cx2d0000_3a00_3a1f_2e2_5cx2data_5cx2d1_5cx2dpart3_2edevice", 0, "dev-disk-by\\x2dpath-pci\\x2d0000:00:1f.2\\x2data\\x2d1\\x2dpart3.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2dpath_2dpci_5cx2d0000_3a00_3a1f_2e2_5cx2data_5cx2d2_2edevice", 0, "dev-disk-by\\x2dpath-pci\\x2d0000:00:1f.2\\x2data\\x2d2.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2dpath_2dpci_5cx2d0000_3a00_3a1f_2e2_5cx2data_5cx2d6_2edevice", 0, "dev-disk-by\\x2dpath-pci\\x2d0000:00:1f.2\\x2data\\x2d6.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2dpath_2dpci_5cx2d0000_3a00_3a1f_2e2_5cx2data_5cx2d6_5cx2dpart1_2edevice", 0, "dev-disk-by\\x2dpath-pci\\x2d0000:00:1f.2\\x2data\\x2d6\\x2dpart1.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2duuid_2d1A34E3F034E3CD37_2edevice", 0, "dev-disk-by\\x2duuid-1A34E3F034E3CD37.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2duuid_2dB670EBFE70EBC2EB_2edevice", 0, "dev-disk-by\\x2duuid-B670EBFE70EBC2EB.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2duuid_2dFCD4F509D4F4C6C4_2edevice", 0, "dev-disk-by\\x2duuid-FCD4F509D4F4C6C4.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2ddisk_2dby_5cx2duuid_2db49ead57_5cx2d907c_5cx2d446c_5cx2db405_5cx2d5ca6cd865f5e_2edevice", 0, "dev-disk-by\\x2duuid-b49ead57\\x2d907c\\x2d446c\\x2db405\\x2d5ca6cd865f5e.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dhugepages_2emount", 0, "dev-hugepages.mount");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dmqueue_2emount", 0, "dev-mqueue.mount");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2drfkill_2edevice", 0, "dev-rfkill.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dsda1_2edevice", 0, "dev-sda1.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dsda2_2edevice", 0, "dev-sda2.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dsda3_2edevice", 0, "dev-sda3.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dsda_2edevice", 0, "dev-sda.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dsdb1_2edevice", 0, "dev-sdb1.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dsdb_2edevice", 0, "dev-sdb.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dsr0_2edevice", 0, "dev-sr0.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS0_2edevice", 0, "dev-ttyS0.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS10_2edevice", 0, "dev-ttyS10.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS11_2edevice", 0, "dev-ttyS11.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS12_2edevice", 0, "dev-ttyS12.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS13_2edevice", 0, "dev-ttyS13.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS14_2edevice", 0, "dev-ttyS14.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS15_2edevice", 0, "dev-ttyS15.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS16_2edevice", 0, "dev-ttyS16.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS17_2edevice", 0, "dev-ttyS17.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS18_2edevice", 0, "dev-ttyS18.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS19_2edevice", 0, "dev-ttyS19.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS1_2edevice", 0, "dev-ttyS1.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS20_2edevice", 0, "dev-ttyS20.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS21_2edevice", 0, "dev-ttyS21.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS22_2edevice", 0, "dev-ttyS22.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS23_2edevice", 0, "dev-ttyS23.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS24_2edevice", 0, "dev-ttyS24.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS25_2edevice", 0, "dev-ttyS25.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS26_2edevice", 0, "dev-ttyS26.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS27_2edevice", 0, "dev-ttyS27.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS28_2edevice", 0, "dev-ttyS28.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS29_2edevice", 0, "dev-ttyS29.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS2_2edevice", 0, "dev-ttyS2.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS30_2edevice", 0, "dev-ttyS30.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS31_2edevice", 0, "dev-ttyS31.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS3_2edevice", 0, "dev-ttyS3.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS4_2edevice", 0, "dev-ttyS4.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS5_2edevice", 0, "dev-ttyS5.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS6_2edevice", 0, "dev-ttyS6.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS7_2edevice", 0, "dev-ttyS7.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS8_2edevice", 0, "dev-ttyS8.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dev_2dttyS9_2edevice", 0, "dev-ttyS9.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dracut_2dcmdline_2eservice", 0, "dracut-cmdline.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dracut_2dinitqueue_2eservice", 0, "dracut-initqueue.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dracut_2dmount_2eservice", 0, "dracut-mount.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dracut_2dpre_2dmount_2eservice", 0, "dracut-pre-mount.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dracut_2dpre_2dpivot_2eservice", 0, "dracut-pre-pivot.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dracut_2dpre_2dtrigger_2eservice", 0, "dracut-pre-trigger.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dracut_2dpre_2dudev_2eservice", 0, "dracut-pre-udev.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/dracut_2dshutdown_2eservice", 0, "dracut-shutdown.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/ebtables_2eservice", 0, "ebtables.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/emergency_2eservice", 0, "emergency.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/emergency_2etarget", 0, "emergency.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/fedora_2dimport_2dstate_2eservice", 0, "fedora-import-state.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/fedora_2dreadonly_2eservice", 0, "fedora-readonly.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/firewalld_2eservice", 0, "firewalld.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/getty_2dpre_2etarget", 0, "getty-pre.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/getty_2etarget", 0, "getty.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/getty_40tty1_2eservice", 0, "getty@tty1.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/graphical_2etarget", 0, "graphical.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/home_2emount", 0, "home.mount");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/init_2escope", 0, "init.scope");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/initrd_2dcleanup_2eservice", 0, "initrd-cleanup.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/initrd_2dfs_2etarget", 0, "initrd-fs.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/initrd_2dparse_2detc_2eservice", 0, "initrd-parse-etc.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/initrd_2droot_2ddevice_2etarget", 0, "initrd-root-device.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/initrd_2droot_2dfs_2etarget", 0, "initrd-root-fs.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/initrd_2dswitch_2droot_2eservice", 0, "initrd-switch-root.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/initrd_2dswitch_2droot_2etarget", 0, "initrd-switch-root.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/initrd_2dudevadm_2dcleanup_2ddb_2eservice", 0, "initrd-udevadm-cleanup-db.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/initrd_2etarget", 0, "initrd.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/ip6tables_2eservice", 0, "ip6tables.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/ipset_2eservice", 0, "ipset.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/iptables_2eservice", 0, "iptables.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/irqbalance_2eservice", 0, "irqbalance.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/kmod_2dstatic_2dnodes_2eservice", 0, "kmod-static-nodes.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/ldconfig_2eservice", 0, "ldconfig.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/lightdm_2eservice", 0, "lightdm.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/livesys_2dlate_2eservice", 0, "livesys-late.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/lm_5fsensors_2eservice", 0, "lm_sensors.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/local_2dfs_2dpre_2etarget", 0, "local-fs-pre.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/local_2dfs_2etarget", 0, "local-fs.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/machines_2etarget", 0, "machines.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/mcelog_2eservice", 0, "mcelog.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/multi_2duser_2etarget", 0, "multi-user.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/network_2dpre_2etarget", 0, "network-pre.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/network_2etarget", 0, "network.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/nss_2dlookup_2etarget", 0, "nss-lookup.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/nss_2duser_2dlookup_2etarget", 0, "nss-user-lookup.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/paths_2etarget", 0, "paths.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/plymouth_2dquit_2dwait_2eservice", 0, "plymouth-quit-wait.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/plymouth_2dquit_2eservice", 0, "plymouth-quit.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/plymouth_2dstart_2eservice", 0, "plymouth-start.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/polkit_2eservice", 0, "polkit.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/proc_2dsys_2dfs_2dbinfmt_5fmisc_2eautomount", 0, "proc-sys-fs-binfmt_misc.automount");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/proc_2dsys_2dfs_2dbinfmt_5fmisc_2emount", 0, "proc-sys-fs-binfmt_misc.mount");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/rc_2dlocal_2eservice", 0, "rc-local.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/remote_2dcryptsetup_2etarget", 0, "remote-cryptsetup.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/remote_2dfs_2dpre_2etarget", 0, "remote-fs-pre.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/remote_2dfs_2etarget", 0, "remote-fs.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/rescue_2eservice", 0, "rescue.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/rescue_2etarget", 0, "rescue.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/run_2duser_2d1000_2emount", 0, "run-user-1000.mount");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/session_2d2_2escope", 0, "session-2.scope");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/shutdown_2etarget", 0, "shutdown.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/slices_2etarget", 0, "slices.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/smartd_2eservice", 0, "smartd.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sockets_2etarget", 0, "sockets.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sound_2etarget", 0, "sound.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sshd_2dkeygen_2etarget", 0, "sshd-keygen.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sshd_2dkeygen_40ecdsa_2eservice", 0, "sshd-keygen@ecdsa.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sshd_2dkeygen_40ed25519_2eservice", 0, "sshd-keygen@ed25519.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sshd_2dkeygen_40rsa_2eservice", 0, "sshd-keygen@rsa.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sshd_2eservice", 0, "sshd.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/swap_2etarget", 0, "swap.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dpci0000_3a00_2d0000_3a00_3a02_2e0_2dbacklight_2dacpi_5fvideo0_2edevice", 0, "sys-devices-pci0000:00-0000:00:02.0-backlight-acpi_video0.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dpci0000_3a00_2d0000_3a00_3a02_2e0_2ddrm_2dcard0_2dcard0_5cx2dLVDS_5cx2d1_2dintel_5fbacklight_2edevice", 0, "sys-devices-pci0000:00-0000:00:02.0-drm-card0-card0\\x2dLVDS\\x2d1-intel_backlight.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dpci0000_3a00_2d0000_3a00_3a1a_2e0_2dusb1_2d1_5cx2d1_2d1_5cx2d1_2e6_2d1_5cx2d1_2e6_3a1_2e0_2dbluetooth_2dhci0_2edevice", 0, "sys-devices-pci0000:00-0000:00:1a.0-usb1-1\\x2d1-1\\x2d1.6-1\\x2d1.6:1.0-bluetooth-hci0.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dpci0000_3a00_2d0000_3a00_3a1b_2e0_2dsound_2dcard0_2edevice", 0, "sys-devices-pci0000:00-0000:00:1b.0-sound-card0.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dpci0000_3a00_2d0000_3a00_3a1c_2e0_2d0000_3a02_3a00_2e0_2dnet_2dwlp2s0_2edevice", 0, "sys-devices-pci0000:00-0000:00:1c.0-0000:02:00.0-net-wlp2s0.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dpci0000_3a00_2d0000_3a00_3a1c_2e2_2d0000_3a04_3a00_2e0_2dnet_2denp4s0_2edevice", 0, "sys-devices-pci0000:00-0000:00:1c.2-0000:04:00.0-net-enp4s0.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dpci0000_3a00_2d0000_3a00_3a1f_2e2_2data1_2dhost0_2dtarget0_3a0_3a0_2d0_3a0_3a0_3a0_2dblock_2dsda_2dsda1_2edevice", 0, "sys-devices-pci0000:00-0000:00:1f.2-ata1-host0-target0:0:0-0:0:0:0-block-sda-sda1.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dpci0000_3a00_2d0000_3a00_3a1f_2e2_2data1_2dhost0_2dtarget0_3a0_3a0_2d0_3a0_3a0_3a0_2dblock_2dsda_2dsda2_2edevice", 0, "sys-devices-pci0000:00-0000:00:1f.2-ata1-host0-target0:0:0-0:0:0:0-block-sda-sda2.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dpci0000_3a00_2d0000_3a00_3a1f_2e2_2data1_2dhost0_2dtarget0_3a0_3a0_2d0_3a0_3a0_3a0_2dblock_2dsda_2dsda3_2edevice", 0, "sys-devices-pci0000:00-0000:00:1f.2-ata1-host0-target0:0:0-0:0:0:0-block-sda-sda3.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dpci0000_3a00_2d0000_3a00_3a1f_2e2_2data1_2dhost0_2dtarget0_3a0_3a0_2d0_3a0_3a0_3a0_2dblock_2dsda_2edevice", 0, "sys-devices-pci0000:00-0000:00:1f.2-ata1-host0-target0:0:0-0:0:0:0-block-sda.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dpci0000_3a00_2d0000_3a00_3a1f_2e2_2data2_2dhost1_2dtarget1_3a0_3a0_2d1_3a0_3a0_3a0_2dblock_2dsr0_2edevice", 0, "sys-devices-pci0000:00-0000:00:1f.2-ata2-host1-target1:0:0-1:0:0:0-block-sr0.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dpci0000_3a00_2d0000_3a00_3a1f_2e2_2data6_2dhost5_2dtarget5_3a0_3a0_2d5_3a0_3a0_3a0_2dblock_2dsdb_2dsdb1_2edevice", 0, "sys-devices-pci0000:00-0000:00:1f.2-ata6-host5-target5:0:0-5:0:0:0-block-sdb-sdb1.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dpci0000_3a00_2d0000_3a00_3a1f_2e2_2data6_2dhost5_2dtarget5_3a0_3a0_2d5_3a0_3a0_3a0_2dblock_2dsdb_2edevice", 0, "sys-devices-pci0000:00-0000:00:1f.2-ata6-host5-target5:0:0-5:0:0:0-block-sdb.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS0_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS0.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS10_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS10.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS11_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS11.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS12_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS12.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS13_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS13.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS14_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS14.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS15_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS15.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS16_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS16.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS17_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS17.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS18_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS18.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS19_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS19.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS1_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS1.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS20_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS20.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS21_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS21.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS22_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS22.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS23_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS23.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS24_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS24.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS25_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS25.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS26_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS26.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS27_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS27.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS28_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS28.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS29_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS29.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS2_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS2.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS30_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS30.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS31_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS31.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS3_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS3.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS4_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS4.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS5_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS5.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS6_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS6.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS7_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS7.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS8_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS8.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dplatform_2dserial8250_2dtty_2dttyS9_2edevice", 0, "sys-devices-platform-serial8250-tty-ttyS9.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2ddevices_2dvirtual_2dmisc_2drfkill_2edevice", 0, "sys-devices-virtual-misc-rfkill.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2dfs_2dfuse_2dconnections_2emount", 0, "sys-fs-fuse-connections.mount");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2dkernel_2dconfig_2emount", 0, "sys-kernel-config.mount");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2dkernel_2ddebug_2emount", 0, "sys-kernel-debug.mount");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2dmodule_2dconfigfs_2edevice", 0, "sys-module-configfs.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2dsubsystem_2dbluetooth_2ddevices_2dhci0_2edevice", 0, "sys-subsystem-bluetooth-devices-hci0.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2dsubsystem_2dnet_2ddevices_2denp4s0_2edevice", 0, "sys-subsystem-net-devices-enp4s0.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sys_2dsubsystem_2dnet_2ddevices_2dwlp2s0_2edevice", 0, "sys-subsystem-net-devices-wlp2s0.device");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sysinit_2etarget", 0, "sysinit.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/syslog_2eservice", 0, "syslog.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/syslog_2esocket", 0, "syslog.socket");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/syslog_2etarget", 0, "syslog.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/sysroot_2emount", 0, "sysroot.mount");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/system_2dgetty_2eslice", 0, "system-getty.slice");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/system_2dsshd_5cx2dkeygen_2eslice", 0, "system-sshd\\x2dkeygen.slice");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/system_2dsystemd_5cx2dbacklight_2eslice", 0, "system-systemd\\x2dbacklight.slice");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/system_2dsystemd_5cx2dcoredump_2eslice", 0, "system-systemd\\x2dcoredump.slice");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/system_2duser_5cx2druntime_5cx2ddir_2eslice", 0, "system-user\\x2druntime\\x2ddir.slice");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/system_2eslice", 0, "system.slice");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dask_2dpassword_2dconsole_2epath", 0, "systemd-ask-password-console.path");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dask_2dpassword_2dconsole_2eservice", 0, "systemd-ask-password-console.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dask_2dpassword_2dwall_2epath", 0, "systemd-ask-password-wall.path");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dask_2dpassword_2dwall_2eservice", 0, "systemd-ask-password-wall.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dbacklight_40backlight_3aacpi_5fvideo0_2eservice", 0, "systemd-backlight@backlight:acpi_video0.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dbacklight_40backlight_3aintel_5fbacklight_2eservice", 0, "systemd-backlight@backlight:intel_backlight.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dbinfmt_2eservice", 0, "systemd-binfmt.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dcoredump_2esocket", 0, "systemd-coredump.socket");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dcoredump_400_2eservice", 0, "systemd-coredump@0.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dfirstboot_2eservice", 0, "systemd-firstboot.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dfsck_2droot_2eservice", 0, SPECIAL_FSCK_ROOT_SERVICE);
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dhwdb_2dupdate_2eservice", 0, "systemd-hwdb-update.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dinitctl_2eservice", 0, "systemd-initctl.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dinitctl_2esocket", 0, "systemd-initctl.socket");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2djournal_2dcatalog_2dupdate_2eservice", 0, "systemd-journal-catalog-update.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2djournal_2dflush_2eservice", 0, "systemd-journal-flush.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2djournald_2daudit_2esocket", 0, "systemd-journald-audit.socket");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2djournald_2ddev_2dlog_2esocket", 0, "systemd-journald-dev-log.socket");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2djournald_2eservice", 0, "systemd-journald.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2djournald_2esocket", 0, "systemd-journald.socket");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dlogind_2eservice", 0, "systemd-logind.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dmachine_2did_2dcommit_2eservice", 0, "systemd-machine-id-commit.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dmodules_2dload_2eservice", 0, "systemd-modules-load.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dnetworkd_2eservice", 0, "systemd-networkd.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dnetworkd_2esocket", 0, "systemd-networkd.socket");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2drandom_2dseed_2eservice", 0, "systemd-random-seed.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dremount_2dfs_2eservice", 0, "systemd-remount-fs.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dresolved_2eservice", 0, "systemd-resolved.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2drfkill_2eservice", 0, "systemd-rfkill.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2drfkill_2esocket", 0, "systemd-rfkill.socket");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dsysctl_2eservice", 0, "systemd-sysctl.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dsysusers_2eservice", 0, "systemd-sysusers.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dtimesyncd_2eservice", 0, "systemd-timesyncd.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dtmpfiles_2dclean_2eservice", 0, "systemd-tmpfiles-clean.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dtmpfiles_2dclean_2etimer", 0, "systemd-tmpfiles-clean.timer");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dtmpfiles_2dsetup_2ddev_2eservice", 0, "systemd-tmpfiles-setup-dev.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dtmpfiles_2dsetup_2eservice", 0, "systemd-tmpfiles-setup.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dudev_2dtrigger_2eservice", 0, "systemd-udev-trigger.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dudevd_2dcontrol_2esocket", 0, "systemd-udevd-control.socket");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dudevd_2dkernel_2esocket", 0, "systemd-udevd-kernel.socket");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dudevd_2eservice", 0, "systemd-udevd.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dupdate_2ddone_2eservice", 0, "systemd-update-done.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dupdate_2dutmp_2drunlevel_2eservice", 0, "systemd-update-utmp-runlevel.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dupdate_2dutmp_2eservice", 0, "systemd-update-utmp.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2duser_2dsessions_2eservice", 0, "systemd-user-sessions.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/systemd_2dvconsole_2dsetup_2eservice", 0, "systemd-vconsole-setup.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/time_2dsync_2etarget", 0, "time-sync.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/timers_2etarget", 0, "timers.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/tmp_2emount", 0, "tmp.mount");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/umount_2etarget", 0, "umount.target");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/unbound_2danchor_2eservice", 0, "unbound-anchor.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/unbound_2danchor_2etimer", 0, "unbound-anchor.timer");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/upower_2eservice", 0, "upower.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/user_2d1000_2eslice", 0, "user-1000.slice");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/user_2druntime_2ddir_401000_2eservice", 0, "user-runtime-dir@1000.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/user_2eslice", 0, "user.slice");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/user_401000_2eservice", 0, "user@1000.service");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/usr_2dlocal_2dtexlive_2emount", 0, "usr-local-texlive.mount");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/var_2dlib_2dmachines_2emount", 0, "var-lib-machines.mount");
+ test_unit_name_from_dbus_path_one("/org/freedesktop/systemd1/unit/wpa_5fsupplicant_2eservice", 0, "wpa_supplicant.service");
+}
+
+TEST(unit_name_prefix_equal) {
+ assert_se(unit_name_prefix_equal("a.service", "a.service"));
+ assert_se(unit_name_prefix_equal("a.service", "a.mount"));
+ assert_se(unit_name_prefix_equal("a@b.service", "a.service"));
+ assert_se(unit_name_prefix_equal("a@b.service", "a@c.service"));
+
+ assert_se(!unit_name_prefix_equal("a.service", "b.service"));
+ assert_se(!unit_name_prefix_equal("a.service", "b.mount"));
+ assert_se(!unit_name_prefix_equal("a@a.service", "b.service"));
+ assert_se(!unit_name_prefix_equal("a@a.service", "b@a.service"));
+ assert_se(!unit_name_prefix_equal("a", "b"));
+ assert_se(!unit_name_prefix_equal("a", "a"));
+}
+
+static int intro(void) {
+ if (enter_cgroup_subroot(NULL) == -ENOMEDIUM)
+ return log_tests_skipped("cgroupfs not available");
+
+ assert_se(runtime_dir = setup_fake_runtime_dir());
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_INFO, intro);
diff --git a/src/test/test-unit-serialize.c b/src/test/test-unit-serialize.c
new file mode 100644
index 0000000..7a1e8a0
--- /dev/null
+++ b/src/test/test-unit-serialize.c
@@ -0,0 +1,63 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "rm-rf.h"
+#include "service.h"
+#include "tests.h"
+
+static char *runtime_dir = NULL;
+
+STATIC_DESTRUCTOR_REGISTER(runtime_dir, rm_rf_physical_and_freep);
+
+#define EXEC_START_ABSOLUTE \
+ "ExecStart 0 /bin/sh \"sh\" \"-e\" \"-x\" \"-c\" \"systemctl --state=failed --no-legend --no-pager >/failed ; systemctl daemon-reload ; echo OK >/testok\""
+#define EXEC_START_RELATIVE \
+ "ExecStart 0 sh \"sh\" \"-e\" \"-x\" \"-c\" \"systemctl --state=failed --no-legend --no-pager >/failed ; systemctl daemon-reload ; echo OK >/testok\""
+
+static void test_deserialize_exec_command_one(Manager *m, const char *key, const char *line, int expected) {
+ _cleanup_(unit_freep) Unit *u = NULL;
+ int r;
+
+ assert_se(unit_new_for_name(m, sizeof(Service), "test.service", &u) >= 0);
+
+ r = service_deserialize_exec_command(u, key, line);
+ log_debug("[%s] → %d (expected: %d)", line, r, expected);
+ assert_se(r == expected);
+
+ /* Note that the command doesn't match any command in the empty list of commands in 's', so it is
+ * always rejected with "Current command vanished from the unit file", and we don't leak anything. */
+}
+
+TEST(deserialize_exec_command) {
+ _cleanup_(manager_freep) Manager *m = NULL;
+ int r;
+
+ r = manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_MINIMAL, &m);
+ if (manager_errno_skip_test(r)) {
+ log_notice_errno(r, "Skipping test: manager_new: %m");
+ return;
+ }
+
+ assert_se(r >= 0);
+
+ test_deserialize_exec_command_one(m, "main-command", EXEC_START_ABSOLUTE, 0);
+ test_deserialize_exec_command_one(m, "main-command", EXEC_START_RELATIVE, 0);
+ test_deserialize_exec_command_one(m, "control-command", EXEC_START_ABSOLUTE, 0);
+ test_deserialize_exec_command_one(m, "control-command", EXEC_START_RELATIVE, 0);
+
+ test_deserialize_exec_command_one(m, "control-command", "ExecStart 0 /bin/sh \"sh\"", 0);
+ test_deserialize_exec_command_one(m, "control-command", "ExecStart 0 /no/command ", -EINVAL);
+ test_deserialize_exec_command_one(m, "control-command", "ExecStart 0 /bad/quote \"", -EINVAL);
+ test_deserialize_exec_command_one(m, "control-command", "ExecStart s /bad/id x y z", -EINVAL);
+ test_deserialize_exec_command_one(m, "control-command", "ExecStart 11", -EINVAL);
+ test_deserialize_exec_command_one(m, "control-command", "ExecWhat 11 /a/b c d e", -EINVAL);
+}
+
+static int intro(void) {
+ if (enter_cgroup_subroot(NULL) == -ENOMEDIUM)
+ return log_tests_skipped("cgroupfs not available");
+
+ assert_se(runtime_dir = setup_fake_runtime_dir());
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_DEBUG, intro);
diff --git a/src/test/test-user-util.c b/src/test/test-user-util.c
new file mode 100644
index 0000000..db76cde
--- /dev/null
+++ b/src/test/test-user-util.c
@@ -0,0 +1,484 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "format-util.h"
+#include "libcrypt-util.h"
+#include "log.h"
+#include "macro.h"
+#include "memory-util.h"
+#include "path-util.h"
+#include "string-util.h"
+#include "tests.h"
+#include "user-util.h"
+
+static void test_uid_to_name_one(uid_t uid, const char *name) {
+ _cleanup_free_ char *t = NULL;
+
+ log_info("/* %s("UID_FMT", \"%s\") */", __func__, uid, name);
+
+ assert_se(t = uid_to_name(uid));
+ if (!synthesize_nobody() && streq(name, NOBODY_USER_NAME)) {
+ log_info("(skipping detailed tests because nobody is not synthesized)");
+ return;
+ }
+ assert_se(streq_ptr(t, name));
+}
+
+TEST(uid_to_name) {
+ test_uid_to_name_one(0, "root");
+ test_uid_to_name_one(UID_NOBODY, NOBODY_USER_NAME);
+ test_uid_to_name_one(0xFFFF, "65535");
+ test_uid_to_name_one(0xFFFFFFFF, "4294967295");
+}
+
+static void test_gid_to_name_one(gid_t gid, const char *name) {
+ _cleanup_free_ char *t = NULL;
+
+ log_info("/* %s("GID_FMT", \"%s\") */", __func__, gid, name);
+
+ assert_se(t = gid_to_name(gid));
+ if (!synthesize_nobody() && streq(name, NOBODY_GROUP_NAME)) {
+ log_info("(skipping detailed tests because nobody is not synthesized)");
+ return;
+ }
+ assert_se(streq_ptr(t, name));
+}
+
+TEST(gid_to_name) {
+ test_gid_to_name_one(0, "root");
+ test_gid_to_name_one(GID_NOBODY, NOBODY_GROUP_NAME);
+ test_gid_to_name_one(0xFFFF, "65535");
+ test_gid_to_name_one(0xFFFFFFFF, "4294967295");
+}
+
+TEST(parse_uid) {
+ int r;
+ uid_t uid;
+
+ r = parse_uid("0", &uid);
+ assert_se(r == 0);
+ assert_se(uid == 0);
+
+ r = parse_uid("1", &uid);
+ assert_se(r == 0);
+ assert_se(uid == 1);
+
+ r = parse_uid("01", &uid);
+ assert_se(r == -EINVAL);
+ assert_se(uid == 1);
+
+ r = parse_uid("001", &uid);
+ assert_se(r == -EINVAL);
+ assert_se(uid == 1);
+
+ r = parse_uid("100", &uid);
+ assert_se(r == 0);
+ assert_se(uid == 100);
+
+ r = parse_uid("65535", &uid);
+ assert_se(r == -ENXIO);
+ assert_se(uid == 100);
+
+ r = parse_uid("0x1234", &uid);
+ assert_se(r == -EINVAL);
+ assert_se(uid == 100);
+
+ r = parse_uid("0o1234", &uid);
+ assert_se(r == -EINVAL);
+ assert_se(uid == 100);
+
+ r = parse_uid("0b1234", &uid);
+ assert_se(r == -EINVAL);
+ assert_se(uid == 100);
+
+ r = parse_uid("+1234", &uid);
+ assert_se(r == -EINVAL);
+ assert_se(uid == 100);
+
+ r = parse_uid("-1234", &uid);
+ assert_se(r == -EINVAL);
+ assert_se(uid == 100);
+
+ r = parse_uid(" 1234", &uid);
+ assert_se(r == -EINVAL);
+ assert_se(uid == 100);
+
+ r = parse_uid("01234", &uid);
+ assert_se(r == -EINVAL);
+ assert_se(uid == 100);
+
+ r = parse_uid("001234", &uid);
+ assert_se(r == -EINVAL);
+ assert_se(uid == 100);
+
+ r = parse_uid("0001234", &uid);
+ assert_se(r == -EINVAL);
+ assert_se(uid == 100);
+
+ r = parse_uid("-0", &uid);
+ assert_se(r == -EINVAL);
+ assert_se(uid == 100);
+
+ r = parse_uid("+0", &uid);
+ assert_se(r == -EINVAL);
+ assert_se(uid == 100);
+
+ r = parse_uid("00", &uid);
+ assert_se(r == -EINVAL);
+ assert_se(uid == 100);
+
+ r = parse_uid("000", &uid);
+ assert_se(r == -EINVAL);
+ assert_se(uid == 100);
+
+ r = parse_uid("asdsdas", &uid);
+ assert_se(r == -EINVAL);
+ assert_se(uid == 100);
+}
+
+TEST(uid_ptr) {
+ assert_se(UID_TO_PTR(0) != NULL);
+ assert_se(UID_TO_PTR(1000) != NULL);
+
+ assert_se(PTR_TO_UID(UID_TO_PTR(0)) == 0);
+ assert_se(PTR_TO_UID(UID_TO_PTR(1000)) == 1000);
+}
+
+TEST(valid_user_group_name_relaxed) {
+ assert_se(!valid_user_group_name(NULL, VALID_USER_RELAX));
+ assert_se(!valid_user_group_name("", VALID_USER_RELAX));
+ assert_se(!valid_user_group_name("1", VALID_USER_RELAX));
+ assert_se(!valid_user_group_name("65535", VALID_USER_RELAX));
+ assert_se(!valid_user_group_name("-1", VALID_USER_RELAX));
+ assert_se(!valid_user_group_name("foo\nbar", VALID_USER_RELAX));
+ assert_se(!valid_user_group_name("0123456789012345678901234567890123456789", VALID_USER_RELAX));
+ assert_se(!valid_user_group_name("aaa:bbb", VALID_USER_RELAX|VALID_USER_ALLOW_NUMERIC));
+ assert_se(!valid_user_group_name(".aaa:bbb", VALID_USER_RELAX|VALID_USER_ALLOW_NUMERIC));
+ assert_se(!valid_user_group_name(".", VALID_USER_RELAX));
+ assert_se(!valid_user_group_name("..", VALID_USER_RELAX));
+
+ assert_se(valid_user_group_name("root", VALID_USER_RELAX));
+ assert_se(valid_user_group_name("lennart", VALID_USER_RELAX));
+ assert_se(valid_user_group_name("LENNART", VALID_USER_RELAX));
+ assert_se(valid_user_group_name("_kkk", VALID_USER_RELAX));
+ assert_se(valid_user_group_name("kkk-", VALID_USER_RELAX));
+ assert_se(valid_user_group_name("kk-k", VALID_USER_RELAX));
+ assert_se(valid_user_group_name("eff.eff", VALID_USER_RELAX));
+ assert_se(valid_user_group_name("eff.", VALID_USER_RELAX));
+ assert_se(valid_user_group_name("-kkk", VALID_USER_RELAX));
+ assert_se(valid_user_group_name("rööt", VALID_USER_RELAX));
+ assert_se(valid_user_group_name(".eff", VALID_USER_RELAX));
+ assert_se(valid_user_group_name(".1", VALID_USER_RELAX));
+ assert_se(valid_user_group_name(".65535", VALID_USER_RELAX));
+ assert_se(valid_user_group_name(".-1", VALID_USER_RELAX));
+ assert_se(valid_user_group_name(".-kkk", VALID_USER_RELAX));
+ assert_se(valid_user_group_name(".rööt", VALID_USER_RELAX));
+ assert_se(valid_user_group_name("...", VALID_USER_RELAX));
+
+ assert_se(valid_user_group_name("some5", VALID_USER_RELAX));
+ assert_se(valid_user_group_name("5some", VALID_USER_RELAX));
+ assert_se(valid_user_group_name("INNER5NUMBER", VALID_USER_RELAX));
+
+ assert_se(valid_user_group_name("piff.paff@ad.domain.example", VALID_USER_RELAX));
+ assert_se(valid_user_group_name("Dāvis", VALID_USER_RELAX));
+}
+
+TEST(valid_user_group_name) {
+ assert_se(!valid_user_group_name(NULL, 0));
+ assert_se(!valid_user_group_name("", 0));
+ assert_se(!valid_user_group_name("1", 0));
+ assert_se(!valid_user_group_name("65535", 0));
+ assert_se(!valid_user_group_name("-1", 0));
+ assert_se(!valid_user_group_name("-kkk", 0));
+ assert_se(!valid_user_group_name("rööt", 0));
+ assert_se(!valid_user_group_name(".", 0));
+ assert_se(!valid_user_group_name(".eff", 0));
+ assert_se(!valid_user_group_name("foo\nbar", 0));
+ assert_se(!valid_user_group_name("0123456789012345678901234567890123456789", 0));
+ assert_se(!valid_user_group_name("aaa:bbb", VALID_USER_ALLOW_NUMERIC));
+ assert_se(!valid_user_group_name(".", 0));
+ assert_se(!valid_user_group_name("..", 0));
+ assert_se(!valid_user_group_name("...", 0));
+ assert_se(!valid_user_group_name(".1", 0));
+ assert_se(!valid_user_group_name(".65535", 0));
+ assert_se(!valid_user_group_name(".-1", 0));
+ assert_se(!valid_user_group_name(".-kkk", 0));
+ assert_se(!valid_user_group_name(".rööt", 0));
+ assert_se(!valid_user_group_name(".aaa:bbb", VALID_USER_ALLOW_NUMERIC));
+
+ assert_se(valid_user_group_name("root", 0));
+ assert_se(valid_user_group_name("lennart", 0));
+ assert_se(valid_user_group_name("LENNART", 0));
+ assert_se(valid_user_group_name("_kkk", 0));
+ assert_se(valid_user_group_name("kkk-", 0));
+ assert_se(valid_user_group_name("kk-k", 0));
+ assert_se(!valid_user_group_name("eff.eff", 0));
+ assert_se(!valid_user_group_name("eff.", 0));
+
+ assert_se(valid_user_group_name("some5", 0));
+ assert_se(!valid_user_group_name("5some", 0));
+ assert_se(valid_user_group_name("INNER5NUMBER", 0));
+
+ assert_se(!valid_user_group_name("piff.paff@ad.domain.example", 0));
+ assert_se(!valid_user_group_name("Dāvis", 0));
+}
+
+TEST(valid_user_group_name_or_numeric_relaxed) {
+ assert_se(!valid_user_group_name(NULL, VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(!valid_user_group_name("", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(valid_user_group_name("0", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(valid_user_group_name("1", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(valid_user_group_name("65534", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(!valid_user_group_name("65535", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(valid_user_group_name("65536", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(!valid_user_group_name("-1", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(!valid_user_group_name("foo\nbar", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(!valid_user_group_name("0123456789012345678901234567890123456789", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(!valid_user_group_name("aaa:bbb", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(!valid_user_group_name(".", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(!valid_user_group_name("..", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+
+ assert_se(valid_user_group_name("root", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(valid_user_group_name("lennart", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(valid_user_group_name("LENNART", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(valid_user_group_name("_kkk", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(valid_user_group_name("kkk-", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(valid_user_group_name("kk-k", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(valid_user_group_name("-kkk", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(valid_user_group_name("rööt", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(valid_user_group_name(".eff", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(valid_user_group_name("eff.eff", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(valid_user_group_name("eff.", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(valid_user_group_name("...", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+
+ assert_se(valid_user_group_name("some5", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(valid_user_group_name("5some", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(valid_user_group_name("INNER5NUMBER", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+
+ assert_se(valid_user_group_name("piff.paff@ad.domain.example", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+ assert_se(valid_user_group_name("Dāvis", VALID_USER_ALLOW_NUMERIC|VALID_USER_RELAX));
+}
+
+TEST(valid_user_group_name_or_numeric) {
+ assert_se(!valid_user_group_name(NULL, VALID_USER_ALLOW_NUMERIC));
+ assert_se(!valid_user_group_name("", VALID_USER_ALLOW_NUMERIC));
+ assert_se(valid_user_group_name("0", VALID_USER_ALLOW_NUMERIC));
+ assert_se(valid_user_group_name("1", VALID_USER_ALLOW_NUMERIC));
+ assert_se(valid_user_group_name("65534", VALID_USER_ALLOW_NUMERIC));
+ assert_se(!valid_user_group_name("65535", VALID_USER_ALLOW_NUMERIC));
+ assert_se(valid_user_group_name("65536", VALID_USER_ALLOW_NUMERIC));
+ assert_se(!valid_user_group_name("-1", VALID_USER_ALLOW_NUMERIC));
+ assert_se(!valid_user_group_name("-kkk", VALID_USER_ALLOW_NUMERIC));
+ assert_se(!valid_user_group_name("rööt", VALID_USER_ALLOW_NUMERIC));
+ assert_se(!valid_user_group_name(".", VALID_USER_ALLOW_NUMERIC));
+ assert_se(!valid_user_group_name("..", VALID_USER_ALLOW_NUMERIC));
+ assert_se(!valid_user_group_name("...", VALID_USER_ALLOW_NUMERIC));
+ assert_se(!valid_user_group_name(".eff", VALID_USER_ALLOW_NUMERIC));
+ assert_se(!valid_user_group_name("eff.eff", VALID_USER_ALLOW_NUMERIC));
+ assert_se(!valid_user_group_name("eff.", VALID_USER_ALLOW_NUMERIC));
+ assert_se(!valid_user_group_name("foo\nbar", VALID_USER_ALLOW_NUMERIC));
+ assert_se(!valid_user_group_name("0123456789012345678901234567890123456789", VALID_USER_ALLOW_NUMERIC));
+ assert_se(!valid_user_group_name("aaa:bbb", VALID_USER_ALLOW_NUMERIC));
+
+ assert_se(valid_user_group_name("root", VALID_USER_ALLOW_NUMERIC));
+ assert_se(valid_user_group_name("lennart", VALID_USER_ALLOW_NUMERIC));
+ assert_se(valid_user_group_name("LENNART", VALID_USER_ALLOW_NUMERIC));
+ assert_se(valid_user_group_name("_kkk", VALID_USER_ALLOW_NUMERIC));
+ assert_se(valid_user_group_name("kkk-", VALID_USER_ALLOW_NUMERIC));
+ assert_se(valid_user_group_name("kk-k", VALID_USER_ALLOW_NUMERIC));
+
+ assert_se(valid_user_group_name("some5", VALID_USER_ALLOW_NUMERIC));
+ assert_se(!valid_user_group_name("5some", VALID_USER_ALLOW_NUMERIC));
+ assert_se(valid_user_group_name("INNER5NUMBER", VALID_USER_ALLOW_NUMERIC));
+
+ assert_se(!valid_user_group_name("piff.paff@ad.domain.example", VALID_USER_ALLOW_NUMERIC));
+ assert_se(!valid_user_group_name("Dāvis", VALID_USER_ALLOW_NUMERIC));
+}
+
+TEST(valid_gecos) {
+ assert_se(!valid_gecos(NULL));
+ assert_se(valid_gecos(""));
+ assert_se(valid_gecos("test"));
+ assert_se(valid_gecos("Ümläüt"));
+ assert_se(!valid_gecos("In\nvalid"));
+ assert_se(!valid_gecos("In:valid"));
+}
+
+TEST(valid_home) {
+ assert_se(!valid_home(NULL));
+ assert_se(!valid_home(""));
+ assert_se(!valid_home("."));
+ assert_se(!valid_home("/home/.."));
+ assert_se(!valid_home("/home/../"));
+ assert_se(!valid_home("/home\n/foo"));
+ assert_se(!valid_home("./piep"));
+ assert_se(!valid_home("piep"));
+ assert_se(!valid_home("/home/user:lennart"));
+
+ assert_se(valid_home("/"));
+ assert_se(valid_home("/home"));
+ assert_se(valid_home("/home/foo"));
+}
+
+static void test_get_user_creds_one(const char *id, const char *name, uid_t uid, gid_t gid, const char *home, const char *shell) {
+ const char *rhome = NULL;
+ const char *rshell = NULL;
+ uid_t ruid = UID_INVALID;
+ gid_t rgid = GID_INVALID;
+ int r;
+
+ log_info("/* %s(\"%s\", \"%s\", "UID_FMT", "GID_FMT", \"%s\", \"%s\") */",
+ __func__, id, name, uid, gid, home, shell);
+
+ r = get_user_creds(&id, &ruid, &rgid, &rhome, &rshell, 0);
+ log_info_errno(r, "got \"%s\", "UID_FMT", "GID_FMT", \"%s\", \"%s\": %m",
+ id, ruid, rgid, strnull(rhome), strnull(rshell));
+ if (!synthesize_nobody() && streq(name, NOBODY_USER_NAME)) {
+ log_info("(skipping detailed tests because nobody is not synthesized)");
+ return;
+ }
+ assert_se(r == 0);
+ assert_se(streq_ptr(id, name));
+ assert_se(ruid == uid);
+ assert_se(rgid == gid);
+ assert_se(path_equal(rhome, home));
+}
+
+TEST(get_user_creds) {
+ test_get_user_creds_one("root", "root", 0, 0, "/root", DEFAULT_USER_SHELL);
+ test_get_user_creds_one("0", "root", 0, 0, "/root", DEFAULT_USER_SHELL);
+ test_get_user_creds_one(NOBODY_USER_NAME, NOBODY_USER_NAME, UID_NOBODY, GID_NOBODY, "/", NOLOGIN);
+ test_get_user_creds_one("65534", NOBODY_USER_NAME, UID_NOBODY, GID_NOBODY, "/", NOLOGIN);
+}
+
+static void test_get_group_creds_one(const char *id, const char *name, gid_t gid) {
+ gid_t rgid = GID_INVALID;
+ int r;
+
+ log_info("/* %s(\"%s\", \"%s\", "GID_FMT") */", __func__, id, name, gid);
+
+ r = get_group_creds(&id, &rgid, 0);
+ log_info_errno(r, "got \"%s\", "GID_FMT": %m", id, rgid);
+ if (!synthesize_nobody() && streq(name, NOBODY_GROUP_NAME)) {
+ log_info("(skipping detailed tests because nobody is not synthesized)");
+ return;
+ }
+ assert_se(r == 0);
+ assert_se(streq_ptr(id, name));
+ assert_se(rgid == gid);
+}
+
+TEST(get_group_creds) {
+ test_get_group_creds_one("root", "root", 0);
+ test_get_group_creds_one("0", "root", 0);
+ test_get_group_creds_one(NOBODY_GROUP_NAME, NOBODY_GROUP_NAME, GID_NOBODY);
+ test_get_group_creds_one("65534", NOBODY_GROUP_NAME, GID_NOBODY);
+}
+
+TEST(make_salt) {
+ _cleanup_free_ char *s, *t;
+
+ assert_se(make_salt(&s) == 0);
+ log_info("got %s", s);
+
+ assert_se(make_salt(&t) == 0);
+ log_info("got %s", t);
+
+ assert_se(!streq(s, t));
+}
+
+TEST(in_gid) {
+ assert_se(in_gid(getgid()) >= 0);
+ assert_se(in_gid(getegid()) >= 0);
+ assert_se(in_gid(GID_INVALID) < 0);
+ assert_se(in_gid(TTY_GID) == 0); /* The TTY gid is for owning ttys, it would be really really weird if we were in it. */
+}
+
+TEST(gid_lists_ops) {
+ static const gid_t l1[] = { 5, 10, 15, 20, 25};
+ static const gid_t l2[] = { 1, 2, 3, 15, 20, 25};
+ static const gid_t l3[] = { 5, 10, 15, 20, 25, 26, 27};
+ static const gid_t l4[] = { 25, 26, 20, 15, 5, 27, 10};
+
+ static const gid_t result1[] = {1, 2, 3, 5, 10, 15, 20, 25, 26, 27};
+ static const gid_t result2[] = {5, 10, 15, 20, 25, 26, 27};
+
+ _cleanup_free_ gid_t *gids = NULL;
+ _cleanup_free_ gid_t *res1 = NULL;
+ _cleanup_free_ gid_t *res2 = NULL;
+ _cleanup_free_ gid_t *res3 = NULL;
+ _cleanup_free_ gid_t *res4 = NULL;
+ int nresult;
+
+ nresult = merge_gid_lists(l2, ELEMENTSOF(l2), l3, ELEMENTSOF(l3), &res1);
+ assert_se(nresult >= 0);
+ assert_se(memcmp_nn(res1, nresult, result1, ELEMENTSOF(result1)) == 0);
+
+ nresult = merge_gid_lists(NULL, 0, l2, ELEMENTSOF(l2), &res2);
+ assert_se(nresult >= 0);
+ assert_se(memcmp_nn(res2, nresult, l2, ELEMENTSOF(l2)) == 0);
+
+ nresult = merge_gid_lists(l1, ELEMENTSOF(l1), l1, ELEMENTSOF(l1), &res3);
+ assert_se(nresult >= 0);
+ assert_se(memcmp_nn(l1, ELEMENTSOF(l1), res3, nresult) == 0);
+
+ nresult = merge_gid_lists(l1, ELEMENTSOF(l1), l4, ELEMENTSOF(l4), &res4);
+ assert_se(nresult >= 0);
+ assert_se(memcmp_nn(result2, ELEMENTSOF(result2), res4, nresult) == 0);
+
+ nresult = getgroups_alloc(&gids);
+ assert_se(nresult >= 0 || nresult == -EINVAL || nresult == -ENOMEM);
+ assert_se(gids);
+}
+
+TEST(parse_uid_range) {
+ uid_t a = 4711, b = 4711;
+
+ assert_se(parse_uid_range("", &a, &b) == -EINVAL && a == 4711 && b == 4711);
+ assert_se(parse_uid_range(" ", &a, &b) == -EINVAL && a == 4711 && b == 4711);
+ assert_se(parse_uid_range("x", &a, &b) == -EINVAL && a == 4711 && b == 4711);
+
+ assert_se(parse_uid_range("0", &a, &b) >= 0 && a == 0 && b == 0);
+ assert_se(parse_uid_range("1", &a, &b) >= 0 && a == 1 && b == 1);
+ assert_se(parse_uid_range("2-2", &a, &b) >= 0 && a == 2 && b == 2);
+ assert_se(parse_uid_range("3-3", &a, &b) >= 0 && a == 3 && b == 3);
+ assert_se(parse_uid_range("4-5", &a, &b) >= 0 && a == 4 && b == 5);
+
+ assert_se(parse_uid_range("7-6", &a, &b) == -EINVAL && a == 4 && b == 5);
+ assert_se(parse_uid_range("-1", &a, &b) == -EINVAL && a == 4 && b == 5);
+ assert_se(parse_uid_range("01", &a, &b) == -EINVAL && a == 4 && b == 5);
+ assert_se(parse_uid_range("001", &a, &b) == -EINVAL && a == 4 && b == 5);
+ assert_se(parse_uid_range("+1", &a, &b) == -EINVAL && a == 4 && b == 5);
+ assert_se(parse_uid_range("1--1", &a, &b) == -EINVAL && a == 4 && b == 5);
+ assert_se(parse_uid_range(" 1", &a, &b) == -EINVAL && a == 4 && b == 5);
+ assert_se(parse_uid_range(" 1-2", &a, &b) == -EINVAL && a == 4 && b == 5);
+ assert_se(parse_uid_range("1 -2", &a, &b) == -EINVAL && a == 4 && b == 5);
+ assert_se(parse_uid_range("1- 2", &a, &b) == -EINVAL && a == 4 && b == 5);
+ assert_se(parse_uid_range("1-2 ", &a, &b) == -EINVAL && a == 4 && b == 5);
+ assert_se(parse_uid_range("01-2", &a, &b) == -EINVAL && a == 4 && b == 5);
+ assert_se(parse_uid_range("1-02", &a, &b) == -EINVAL && a == 4 && b == 5);
+ assert_se(parse_uid_range("001-2", &a, &b) == -EINVAL && a == 4 && b == 5);
+ assert_se(parse_uid_range("1-002", &a, &b) == -EINVAL && a == 4 && b == 5);
+ assert_se(parse_uid_range(" 01", &a, &b) == -EINVAL && a == 4 && b == 5);
+}
+
+static void test_mangle_gecos_one(const char *input, const char *expected) {
+ _cleanup_free_ char *p = NULL;
+
+ assert_se(p = mangle_gecos(input));
+ assert_se(streq(p, expected));
+ assert_se(valid_gecos(p));
+}
+
+TEST(mangle_gecos) {
+ test_mangle_gecos_one("", "");
+ test_mangle_gecos_one("root", "root");
+ test_mangle_gecos_one("wuff\nwuff", "wuff wuff");
+ test_mangle_gecos_one("wuff:wuff", "wuff wuff");
+ test_mangle_gecos_one("wuff\r\n:wuff", "wuff wuff");
+ test_mangle_gecos_one("\n--wüff-wäff-wöff::", " --wüff-wäff-wöff ");
+ test_mangle_gecos_one("\xc3\x28", " (");
+ test_mangle_gecos_one("\xe2\x28\xa1", " ( ");
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-utf8.c b/src/test/test-utf8.c
new file mode 100644
index 0000000..a0d7dc1
--- /dev/null
+++ b/src/test/test-utf8.c
@@ -0,0 +1,235 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "utf8.h"
+
+TEST(utf8_is_printable) {
+ assert_se(utf8_is_printable("ascii is valid\tunicode", 22));
+ assert_se(utf8_is_printable("\342\204\242", 3));
+ assert_se(!utf8_is_printable("\341\204", 2));
+ assert_se(utf8_is_printable("ąę", 4));
+ assert_se(!utf8_is_printable("\r", 1));
+ assert_se(utf8_is_printable("\n", 1));
+ assert_se(utf8_is_printable("\t", 1));
+}
+
+TEST(utf8_n_is_valid) {
+ assert_se( utf8_is_valid_n("ascii is valid unicode", 21));
+ assert_se( utf8_is_valid_n("ascii is valid unicode", 22));
+ assert_se(!utf8_is_valid_n("ascii is valid unicode", 23));
+ assert_se( utf8_is_valid_n("\342\204\242", 0));
+ assert_se(!utf8_is_valid_n("\342\204\242", 1));
+ assert_se(!utf8_is_valid_n("\342\204\242", 2));
+ assert_se( utf8_is_valid_n("\342\204\242", 3));
+ assert_se(!utf8_is_valid_n("\342\204\242", 4));
+ assert_se( utf8_is_valid_n("<ZZ>", 0));
+ assert_se( utf8_is_valid_n("<ZZ>", 1));
+ assert_se( utf8_is_valid_n("<ZZ>", 2));
+ assert_se( utf8_is_valid_n("<ZZ>", 3));
+ assert_se( utf8_is_valid_n("<ZZ>", 4));
+ assert_se(!utf8_is_valid_n("<ZZ>", 5));
+}
+
+TEST(utf8_is_valid) {
+ assert_se(utf8_is_valid("ascii is valid unicode"));
+ assert_se(utf8_is_valid("\342\204\242"));
+ assert_se(!utf8_is_valid("\341\204"));
+}
+
+TEST(ascii_is_valid) {
+ assert_se( ascii_is_valid("alsdjf\t\vbarr\nba z"));
+ assert_se(!ascii_is_valid("\342\204\242"));
+ assert_se(!ascii_is_valid("\341\204"));
+}
+
+TEST(ascii_is_valid_n) {
+ assert_se( ascii_is_valid_n("alsdjf\t\vbarr\nba z", 17));
+ assert_se( ascii_is_valid_n("alsdjf\t\vbarr\nba z", 16));
+ assert_se(!ascii_is_valid_n("alsdjf\t\vbarr\nba z", 18));
+ assert_se(!ascii_is_valid_n("\342\204\242", 3));
+ assert_se(!ascii_is_valid_n("\342\204\242", 2));
+ assert_se(!ascii_is_valid_n("\342\204\242", 1));
+ assert_se( ascii_is_valid_n("\342\204\242", 0));
+}
+
+static void test_utf8_to_ascii_one(const char *s, int r_expected, const char *expected) {
+ _cleanup_free_ char *ans = NULL;
+ int r;
+
+ r = utf8_to_ascii(s, '*', &ans);
+ log_debug("\"%s\" → %d/\"%s\" (expected %d/\"%s\")", s, r, strnull(ans), r_expected, strnull(expected));
+ assert_se(r == r_expected);
+ assert_se(streq_ptr(ans, expected));
+}
+
+TEST(utf8_to_ascii) {
+ test_utf8_to_ascii_one("asdf", 0, "asdf");
+ test_utf8_to_ascii_one("dąb", 0, "d*b");
+ test_utf8_to_ascii_one("żęśłą óźń", 0, "***** ***");
+ test_utf8_to_ascii_one("\342\204\242", 0, "*");
+ test_utf8_to_ascii_one("\342\204", -EINVAL, NULL); /* truncated */
+ test_utf8_to_ascii_one("\342", -EINVAL, NULL); /* truncated */
+ test_utf8_to_ascii_one("\302\256", 0, "*");
+ test_utf8_to_ascii_one("", 0, "");
+ test_utf8_to_ascii_one(" ", 0, " ");
+ test_utf8_to_ascii_one("\t", 0, "\t");
+ test_utf8_to_ascii_one("串", 0, "*");
+ test_utf8_to_ascii_one("…👊🔪💐…", 0, "*****");
+}
+
+TEST(utf8_encoded_valid_unichar) {
+ assert_se(utf8_encoded_valid_unichar("\342\204\242", 1) == -EINVAL); /* truncated */
+ assert_se(utf8_encoded_valid_unichar("\342\204\242", 2) == -EINVAL); /* truncated */
+ assert_se(utf8_encoded_valid_unichar("\342\204\242", 3) == 3);
+ assert_se(utf8_encoded_valid_unichar("\342\204\242", 4) == 3);
+ assert_se(utf8_encoded_valid_unichar("\302\256", 1) == -EINVAL); /* truncated */
+ assert_se(utf8_encoded_valid_unichar("\302\256", 2) == 2);
+ assert_se(utf8_encoded_valid_unichar("\302\256", 3) == 2);
+ assert_se(utf8_encoded_valid_unichar("\302\256", SIZE_MAX) == 2);
+ assert_se(utf8_encoded_valid_unichar("a", 1) == 1);
+ assert_se(utf8_encoded_valid_unichar("a", 2) == 1);
+ assert_se(utf8_encoded_valid_unichar("\341\204", 1) == -EINVAL); /* truncated, potentially valid */
+ assert_se(utf8_encoded_valid_unichar("\341\204", 2) == -EINVAL); /* truncated, potentially valid */
+ assert_se(utf8_encoded_valid_unichar("\341\204", 3) == -EINVAL);
+ assert_se(utf8_encoded_valid_unichar("\341\204\341\204", 4) == -EINVAL);
+ assert_se(utf8_encoded_valid_unichar("\341\204\341\204", 5) == -EINVAL);
+}
+
+TEST(utf8_escape_invalid) {
+ _cleanup_free_ char *p1 = NULL, *p2 = NULL, *p3 = NULL;
+
+ p1 = utf8_escape_invalid("goo goo goo");
+ log_debug("\"%s\"", p1);
+ assert_se(utf8_is_valid(p1));
+
+ p2 = utf8_escape_invalid("\341\204\341\204");
+ log_debug("\"%s\"", p2);
+ assert_se(utf8_is_valid(p2));
+
+ p3 = utf8_escape_invalid("\341\204");
+ log_debug("\"%s\"", p3);
+ assert_se(utf8_is_valid(p3));
+}
+
+TEST(utf8_escape_non_printable) {
+ _cleanup_free_ char *p1 = NULL, *p2 = NULL, *p3 = NULL, *p4 = NULL, *p5 = NULL, *p6 = NULL;
+
+ p1 = utf8_escape_non_printable("goo goo goo");
+ log_debug("\"%s\"", p1);
+ assert_se(utf8_is_valid(p1));
+
+ p2 = utf8_escape_non_printable("\341\204\341\204");
+ log_debug("\"%s\"", p2);
+ assert_se(utf8_is_valid(p2));
+
+ p3 = utf8_escape_non_printable("\341\204");
+ log_debug("\"%s\"", p3);
+ assert_se(utf8_is_valid(p3));
+
+ p4 = utf8_escape_non_printable("ąę\n가너도루\n1234\n\341\204\341\204\n\001 \019\20\a");
+ log_debug("\"%s\"", p4);
+ assert_se(utf8_is_valid(p4));
+
+ p5 = utf8_escape_non_printable("\001 \019\20\a");
+ log_debug("\"%s\"", p5);
+ assert_se(utf8_is_valid(p5));
+
+ p6 = utf8_escape_non_printable("\xef\xbf\x30\x13");
+ log_debug("\"%s\"", p6);
+ assert_se(utf8_is_valid(p6));
+}
+
+TEST(utf8_escape_non_printable_full) {
+ FOREACH_STRING(s,
+ "goo goo goo", /* ASCII */
+ "\001 \019\20\a", /* control characters */
+ "\xef\xbf\x30\x13") /* misplaced continuation bytes followed by a digit and cc */
+ for (size_t cw = 0; cw < 22; cw++) {
+ _cleanup_free_ char *p = NULL, *q = NULL;
+ size_t ew;
+
+ p = utf8_escape_non_printable_full(s, cw, false);
+ ew = utf8_console_width(p);
+ log_debug("%02zu \"%s\" (%zu wasted)", cw, p, cw - ew);
+ assert_se(utf8_is_valid(p));
+ assert_se(ew <= cw);
+
+ q = utf8_escape_non_printable_full(s, cw, true);
+ ew = utf8_console_width(q);
+ log_debug(" \"%s\" (%zu wasted)", q, cw - ew);
+ assert_se(utf8_is_valid(q));
+ assert_se(ew <= cw);
+ if (cw > 0)
+ assert_se(endswith(q, "…"));
+ }
+}
+
+TEST(utf16_to_utf8) {
+ const char16_t utf16[] = { htole16('a'), htole16(0xd800), htole16('b'), htole16(0xdc00), htole16('c'), htole16(0xd801), htole16(0xdc37) };
+ static const char utf8[] = { 'a', 'b', 'c', 0xf0, 0x90, 0x90, 0xb7 };
+ _cleanup_free_ char16_t *b = NULL;
+ _cleanup_free_ char *a = NULL;
+
+ /* Convert UTF-16 to UTF-8, filtering embedded bad chars */
+ a = utf16_to_utf8(utf16, sizeof(utf16));
+ assert_se(a);
+ assert_se(memcmp(a, utf8, sizeof(utf8)) == 0);
+
+ /* Convert UTF-8 to UTF-16, and back */
+ b = utf8_to_utf16(utf8, sizeof(utf8));
+ assert_se(b);
+
+ free(a);
+ a = utf16_to_utf8(b, SIZE_MAX);
+ assert_se(a);
+ assert_se(strlen(a) == sizeof(utf8));
+ assert_se(memcmp(a, utf8, sizeof(utf8)) == 0);
+}
+
+TEST(utf8_n_codepoints) {
+ assert_se(utf8_n_codepoints("abc") == 3);
+ assert_se(utf8_n_codepoints("zażółcić gęślą jaźń") == 19);
+ assert_se(utf8_n_codepoints("串") == 1);
+ assert_se(utf8_n_codepoints("") == 0);
+ assert_se(utf8_n_codepoints("…👊🔪💐…") == 5);
+ assert_se(utf8_n_codepoints("\xF1") == SIZE_MAX);
+}
+
+TEST(utf8_console_width) {
+ assert_se(utf8_console_width("abc") == 3);
+ assert_se(utf8_console_width("zażółcić gęślą jaźń") == 19);
+ assert_se(utf8_console_width("串") == 2);
+ assert_se(utf8_console_width("") == 0);
+ assert_se(utf8_console_width("…👊🔪💐…") == 8);
+ assert_se(utf8_console_width("\xF1") == SIZE_MAX);
+}
+
+TEST(utf8_to_utf16) {
+ FOREACH_STRING(p,
+ "abc",
+ "zażółcić gęślą jaźń",
+ "串",
+ "",
+ "…👊🔪💐…") {
+
+ _cleanup_free_ char16_t *a = NULL;
+ _cleanup_free_ char *b = NULL;
+
+ a = utf8_to_utf16(p, SIZE_MAX);
+ assert_se(a);
+
+ b = utf16_to_utf8(a, SIZE_MAX);
+ assert_se(b);
+ assert_se(streq(p, b));
+ }
+}
+
+static int intro(void) {
+ log_show_color(true);
+ return EXIT_SUCCESS;
+}
+
+DEFINE_TEST_MAIN_WITH_INTRO(LOG_INFO, intro);
diff --git a/src/test/test-utmp.c b/src/test/test-utmp.c
new file mode 100644
index 0000000..06a0fce
--- /dev/null
+++ b/src/test/test-utmp.c
@@ -0,0 +1,58 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "format-util.h"
+#include "socket-util.h"
+#include "stdio-util.h"
+#include "string-util.h"
+#include "utmp-wtmp.h"
+#include "tests.h"
+
+#ifndef UT_LINESIZE
+# define UT_LINESIZE 32
+#endif
+#ifndef UT_NAMESIZE
+# define UT_NAMESIZE 32
+#endif
+#ifndef UT_HOSTSIZE
+# define UT_HOSTSIZE 256
+#endif
+
+TEST(dump_run_utmp) {
+ _unused_ _cleanup_(utxent_cleanup) bool utmpx = false;
+
+ utmpx = utxent_start();
+
+ for (struct utmpx *u; (u = getutxent()); ) {
+ char _type_buf[DECIMAL_STR_MAX(short)];
+ const char *type =
+ u->ut_type == EMPTY ? "EMPTY" :
+ u->ut_type == RUN_LVL ? "RUN_LVL" :
+ u->ut_type == BOOT_TIME ? "BOOT_TIME" :
+ u->ut_type == NEW_TIME ? "NEW_TIME" :
+ u->ut_type == OLD_TIME ? "OLD_TIME" :
+ u->ut_type == INIT_PROCESS ? "INIT_PROCESS" :
+ u->ut_type == LOGIN_PROCESS ? "LOGIN_PROCESS" :
+ u->ut_type == USER_PROCESS ? "USER_PROCESS" :
+ u->ut_type == DEAD_PROCESS ? "DEAD_PROCESS" :
+ u->ut_type == ACCOUNTING ? "ACCOUNTING" :
+ _type_buf;
+ if (type == _type_buf)
+ xsprintf(_type_buf, "%hd", u->ut_type);
+
+ union in_addr_union addr = {};
+ memcpy(&addr, u->ut_addr_v6, MIN(sizeof(addr), sizeof(u->ut_addr_v6)));
+ bool is_ipv4 = memeqzero((const uint8_t*) &addr + 4, sizeof(addr) - 4);
+
+ log_info("%14s %10"PID_PRI" line=%-7.*s id=%-4.4s name=%-8.*s session=%lu host=%.*s addr=%s",
+ type,
+ u->ut_pid,
+ UT_LINESIZE, u->ut_line,
+ u->ut_id,
+ UT_NAMESIZE, u->ut_user,
+ (long unsigned) u->ut_session,
+ UT_HOSTSIZE, u->ut_host,
+ IN_ADDR_TO_STRING(is_ipv4 ? AF_INET : AF_INET6, &addr));
+ }
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-varlink-idl.c b/src/test/test-varlink-idl.c
new file mode 100644
index 0000000..cbdb9c6
--- /dev/null
+++ b/src/test/test-varlink-idl.c
@@ -0,0 +1,385 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <pthread.h>
+
+#include "fd-util.h"
+#include "pretty-print.h"
+#include "tests.h"
+#include "varlink.h"
+#include "varlink-idl.h"
+#include "varlink-io.systemd.h"
+#include "varlink-io.systemd.Journal.h"
+#include "varlink-io.systemd.ManagedOOM.h"
+#include "varlink-io.systemd.PCRExtend.h"
+#include "varlink-io.systemd.Resolve.Monitor.h"
+#include "varlink-io.systemd.Resolve.h"
+#include "varlink-io.systemd.UserDatabase.h"
+#include "varlink-io.systemd.oom.h"
+#include "varlink-io.systemd.service.h"
+#include "varlink-io.systemd.sysext.h"
+#include "varlink-org.varlink.service.h"
+
+static VARLINK_DEFINE_ENUM_TYPE(
+ EnumTest,
+ VARLINK_DEFINE_ENUM_VALUE(foo),
+ VARLINK_DEFINE_ENUM_VALUE(bar),
+ VARLINK_DEFINE_ENUM_VALUE(baz));
+
+static VARLINK_DEFINE_STRUCT_TYPE(
+ NestedStructTest,
+ VARLINK_DEFINE_FIELD(x, VARLINK_INT, 0));
+
+static VARLINK_DEFINE_STRUCT_TYPE(
+ StructTest,
+
+ VARLINK_DEFINE_FIELD(bbb, VARLINK_BOOL, 0),
+ VARLINK_DEFINE_FIELD(bbbn, VARLINK_BOOL, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(bbba, VARLINK_BOOL, VARLINK_ARRAY),
+ VARLINK_DEFINE_FIELD(bbbna, VARLINK_BOOL, VARLINK_NULLABLE|VARLINK_ARRAY),
+ VARLINK_DEFINE_FIELD(bbbm, VARLINK_BOOL, VARLINK_MAP),
+ VARLINK_DEFINE_FIELD(bbbnm, VARLINK_BOOL, VARLINK_NULLABLE|VARLINK_MAP),
+
+ VARLINK_DEFINE_FIELD(iii, VARLINK_INT, 0),
+ VARLINK_DEFINE_FIELD(iiin, VARLINK_INT, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(iiia, VARLINK_INT, VARLINK_ARRAY),
+ VARLINK_DEFINE_FIELD(iiina, VARLINK_INT, VARLINK_NULLABLE|VARLINK_ARRAY),
+ VARLINK_DEFINE_FIELD(iiim, VARLINK_INT, VARLINK_MAP),
+ VARLINK_DEFINE_FIELD(iiinm, VARLINK_INT, VARLINK_NULLABLE|VARLINK_MAP),
+
+ VARLINK_DEFINE_FIELD(fff, VARLINK_FLOAT, 0),
+ VARLINK_DEFINE_FIELD(fffn, VARLINK_FLOAT, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(fffa, VARLINK_FLOAT, VARLINK_ARRAY),
+ VARLINK_DEFINE_FIELD(fffna, VARLINK_FLOAT, VARLINK_NULLABLE|VARLINK_ARRAY),
+ VARLINK_DEFINE_FIELD(fffm, VARLINK_FLOAT, VARLINK_MAP),
+ VARLINK_DEFINE_FIELD(fffnm, VARLINK_FLOAT, VARLINK_NULLABLE|VARLINK_MAP),
+
+ VARLINK_DEFINE_FIELD(sss, VARLINK_STRING, 0),
+ VARLINK_DEFINE_FIELD(sssn, VARLINK_STRING, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(sssa, VARLINK_STRING, VARLINK_ARRAY),
+ VARLINK_DEFINE_FIELD(sssna, VARLINK_STRING, VARLINK_NULLABLE|VARLINK_ARRAY),
+ VARLINK_DEFINE_FIELD(sssm, VARLINK_STRING, VARLINK_MAP),
+ VARLINK_DEFINE_FIELD(sssnm, VARLINK_STRING, VARLINK_NULLABLE|VARLINK_MAP),
+
+ VARLINK_DEFINE_FIELD(ooo, VARLINK_OBJECT, 0),
+ VARLINK_DEFINE_FIELD(ooon, VARLINK_OBJECT, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD(oooa, VARLINK_OBJECT, VARLINK_ARRAY),
+ VARLINK_DEFINE_FIELD(ooona, VARLINK_OBJECT, VARLINK_NULLABLE|VARLINK_ARRAY),
+ VARLINK_DEFINE_FIELD(ooom, VARLINK_OBJECT, VARLINK_MAP),
+ VARLINK_DEFINE_FIELD(ooonm, VARLINK_OBJECT, VARLINK_NULLABLE|VARLINK_MAP),
+
+ VARLINK_DEFINE_FIELD_BY_TYPE(eee, EnumTest, 0),
+ VARLINK_DEFINE_FIELD_BY_TYPE(eeen, EnumTest, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD_BY_TYPE(eeea, EnumTest, VARLINK_ARRAY),
+ VARLINK_DEFINE_FIELD_BY_TYPE(eeena, EnumTest, VARLINK_NULLABLE|VARLINK_ARRAY),
+ VARLINK_DEFINE_FIELD_BY_TYPE(eeem, EnumTest, VARLINK_MAP),
+ VARLINK_DEFINE_FIELD_BY_TYPE(eeenm, EnumTest, VARLINK_NULLABLE|VARLINK_MAP),
+
+ VARLINK_DEFINE_FIELD_BY_TYPE(nnn, NestedStructTest, 0),
+ VARLINK_DEFINE_FIELD_BY_TYPE(nnnn, NestedStructTest, VARLINK_NULLABLE),
+ VARLINK_DEFINE_FIELD_BY_TYPE(nnna, NestedStructTest, VARLINK_ARRAY),
+ VARLINK_DEFINE_FIELD_BY_TYPE(nnnna, NestedStructTest, VARLINK_NULLABLE|VARLINK_ARRAY),
+ VARLINK_DEFINE_FIELD_BY_TYPE(nnnm, NestedStructTest, VARLINK_MAP),
+ VARLINK_DEFINE_FIELD_BY_TYPE(nnnnm, NestedStructTest, VARLINK_NULLABLE|VARLINK_MAP));
+
+static VARLINK_DEFINE_METHOD(
+ MethodTest,
+ VARLINK_DEFINE_INPUT(x, VARLINK_BOOL, 0),
+ VARLINK_DEFINE_INPUT_BY_TYPE(y, EnumTest, 0),
+ VARLINK_DEFINE_INPUT_BY_TYPE(z, StructTest, 0),
+ VARLINK_DEFINE_OUTPUT(x, VARLINK_BOOL, 0),
+ VARLINK_DEFINE_OUTPUT_BY_TYPE(y, EnumTest, 0),
+ VARLINK_DEFINE_OUTPUT_BY_TYPE(z, StructTest, 0));
+
+static VARLINK_DEFINE_ERROR(
+ ErrorTest,
+ VARLINK_DEFINE_FIELD(x, VARLINK_BOOL, 0),
+ VARLINK_DEFINE_FIELD_BY_TYPE(y, EnumTest, 0),
+ VARLINK_DEFINE_FIELD_BY_TYPE(z, StructTest, 0));
+
+static VARLINK_DEFINE_INTERFACE(
+ xyz_test,
+ "xyz.test",
+ &vl_type_EnumTest,
+ &vl_type_NestedStructTest,
+ &vl_type_StructTest,
+ &vl_method_MethodTest,
+ &vl_error_ErrorTest);
+
+static void test_parse_format_one(const VarlinkInterface *iface) {
+ _cleanup_(varlink_interface_freep) VarlinkInterface *parsed = NULL;
+ _cleanup_free_ char *text = NULL, *text2 = NULL;
+
+ assert_se(iface);
+
+ assert_se(varlink_idl_dump(stdout, /* use_colors=*/ true, iface) >= 0);
+ assert_se(varlink_idl_consistent(iface, LOG_ERR) >= 0);
+ assert_se(varlink_idl_format(iface, &text) >= 0);
+ assert_se(varlink_idl_parse(text, NULL, NULL, &parsed) >= 0);
+ assert_se(varlink_idl_consistent(parsed, LOG_ERR) >= 0);
+ assert_se(varlink_idl_format(parsed, &text2) >= 0);
+ assert_se(streq(text, text2));
+}
+
+TEST(parse_format) {
+ test_parse_format_one(&vl_interface_org_varlink_service);
+ print_separator();
+ test_parse_format_one(&vl_interface_io_systemd_UserDatabase);
+ print_separator();
+ test_parse_format_one(&vl_interface_io_systemd_Journal);
+ print_separator();
+ test_parse_format_one(&vl_interface_io_systemd_Resolve);
+ print_separator();
+ test_parse_format_one(&vl_interface_io_systemd_Resolve_Monitor);
+ print_separator();
+ test_parse_format_one(&vl_interface_io_systemd_ManagedOOM);
+ print_separator();
+ test_parse_format_one(&vl_interface_io_systemd_oom);
+ print_separator();
+ test_parse_format_one(&vl_interface_io_systemd);
+ print_separator();
+ test_parse_format_one(&vl_interface_io_systemd_PCRExtend);
+ print_separator();
+ test_parse_format_one(&vl_interface_io_systemd_service);
+ print_separator();
+ test_parse_format_one(&vl_interface_io_systemd_sysext);
+ print_separator();
+ test_parse_format_one(&vl_interface_xyz_test);
+}
+
+TEST(parse) {
+ _cleanup_(varlink_interface_freep) VarlinkInterface *parsed = NULL;
+
+ /* This one has (nested) enonymous enums and structs */
+ static const char text[] =
+ "interface quu.waa\n"
+ "type Fooenum ( a, b, c )\n"
+ "type Barstruct ( a : (x, y, z), b : (x : int), c: (f, ff, fff), d: object, e : (sub : (subsub: (subsubsub: string, subsubsub2: (iii, ooo)))))"
+ ;
+
+ assert_se(varlink_idl_parse(text, NULL, NULL, &parsed) >= 0);
+ test_parse_format_one(parsed);
+
+ assert_se(varlink_idl_parse("interface org.freedesktop.Foo\n"
+ "type Foo (b: bool, c: foo, c: int)", NULL, NULL, NULL) == -ENETUNREACH); /* unresolved type */
+ assert_se(varlink_idl_parse("interface org.freedesktop.Foo\n"
+ "type Foo ()", NULL, NULL, NULL) == -EBADMSG); /* empty struct/enum */
+
+}
+
+TEST(interface_name_is_valid) {
+ assert_se(!varlink_idl_interface_name_is_valid(NULL));
+ assert_se(!varlink_idl_interface_name_is_valid(""));
+ assert_se(!varlink_idl_interface_name_is_valid(","));
+ assert_se(!varlink_idl_interface_name_is_valid("."));
+ assert_se(!varlink_idl_interface_name_is_valid("-"));
+ assert_se(varlink_idl_interface_name_is_valid("a"));
+ assert_se(varlink_idl_interface_name_is_valid("a.a"));
+ assert_se(!varlink_idl_interface_name_is_valid("-.a"));
+ assert_se(!varlink_idl_interface_name_is_valid("-a.a"));
+ assert_se(!varlink_idl_interface_name_is_valid("a-.a"));
+ assert_se(varlink_idl_interface_name_is_valid("a-a.a"));
+ assert_se(!varlink_idl_interface_name_is_valid("a-a.a-"));
+ assert_se(!varlink_idl_interface_name_is_valid("a-a.-a"));
+ assert_se(!varlink_idl_interface_name_is_valid("a-a.-"));
+ assert_se(varlink_idl_interface_name_is_valid("a-a.a-a"));
+ assert_se(varlink_idl_interface_name_is_valid("io.systemd.Foobar"));
+}
+
+TEST(symbol_name_is_valid) {
+ assert_se(!varlink_idl_symbol_name_is_valid(NULL));
+ assert_se(!varlink_idl_symbol_name_is_valid(""));
+ assert_se(!varlink_idl_symbol_name_is_valid("_"));
+ assert_se(!varlink_idl_symbol_name_is_valid("_foo"));
+ assert_se(varlink_idl_symbol_name_is_valid("Foofoo"));
+ assert_se(varlink_idl_symbol_name_is_valid("Foo"));
+ assert_se(varlink_idl_symbol_name_is_valid("Foo0"));
+ assert_se(!varlink_idl_symbol_name_is_valid("0Foo"));
+ assert_se(!varlink_idl_symbol_name_is_valid("foo"));
+ assert_se(varlink_idl_symbol_name_is_valid("Foo0foo"));
+ assert_se(!varlink_idl_symbol_name_is_valid("bool"));
+ assert_se(!varlink_idl_symbol_name_is_valid("int"));
+ assert_se(!varlink_idl_symbol_name_is_valid("float"));
+ assert_se(!varlink_idl_symbol_name_is_valid("string"));
+ assert_se(!varlink_idl_symbol_name_is_valid("object"));
+}
+
+TEST(field_name_is_valid) {
+ assert_se(!varlink_idl_field_name_is_valid(NULL));
+ assert_se(!varlink_idl_field_name_is_valid(""));
+ assert_se(!varlink_idl_field_name_is_valid("_"));
+ assert_se(!varlink_idl_field_name_is_valid("_foo"));
+ assert_se(!varlink_idl_field_name_is_valid("_foo_"));
+ assert_se(!varlink_idl_field_name_is_valid("foo_"));
+ assert_se(varlink_idl_field_name_is_valid("foo_foo"));
+ assert_se(varlink_idl_field_name_is_valid("f_o_o_f_o_o"));
+ assert_se(!varlink_idl_field_name_is_valid("foo__foo"));
+ assert_se(varlink_idl_field_name_is_valid("Foofoo"));
+ assert_se(varlink_idl_field_name_is_valid("Foo"));
+ assert_se(varlink_idl_field_name_is_valid("Foo0"));
+ assert_se(!varlink_idl_field_name_is_valid("0Foo"));
+ assert_se(varlink_idl_field_name_is_valid("foo"));
+ assert_se(varlink_idl_field_name_is_valid("Foo0foo"));
+ assert_se(varlink_idl_field_name_is_valid("foo0foo"));
+}
+
+TEST(validate_json) {
+
+ _cleanup_(varlink_interface_freep) VarlinkInterface *parsed = NULL;
+
+ /* This one has (nested) enonymous enums and structs */
+ static const char text[] =
+ "interface validate.test\n"
+ "method Mymethod ( a:string, b:int, c:?bool, d:[]int, e:?[string]bool, f:?(piff, paff), g:(f:float) ) -> ()\n";
+
+ assert_se(varlink_idl_parse(text, NULL, NULL, &parsed) >= 0);
+ test_parse_format_one(parsed);
+
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+
+ assert_se(json_build(&v, JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR("a", JSON_BUILD_STRING("x")),
+ JSON_BUILD_PAIR("b", JSON_BUILD_UNSIGNED(44)),
+ JSON_BUILD_PAIR("d", JSON_BUILD_ARRAY(JSON_BUILD_UNSIGNED(5), JSON_BUILD_UNSIGNED(7), JSON_BUILD_UNSIGNED(107))),
+ JSON_BUILD_PAIR("g", JSON_BUILD_OBJECT(JSON_BUILD_PAIR("f", JSON_BUILD_REAL(0.5f)))))) >= 0);
+
+ json_variant_dump(v, JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO, stdout, NULL);
+
+ const VarlinkSymbol* symbol = ASSERT_PTR(varlink_idl_find_symbol(parsed, VARLINK_METHOD, "Mymethod"));
+
+ assert_se(varlink_idl_validate_method_call(symbol, v, NULL) >= 0);
+}
+
+static int test_recursive_one(unsigned depth) {
+ _cleanup_(varlink_interface_freep) VarlinkInterface *parsed = NULL;
+ _cleanup_free_ char *pre = NULL, *post = NULL, *text = NULL;
+ static const char header[] =
+ "interface recursive.test\n"
+ "type Foo (\n";
+
+ /* Generate a chain of nested structures, i.e. a: (a: (... (int))...) */
+ pre = strrep("a:(", depth);
+ post = strrep(")", depth);
+ if (!pre || !post)
+ return log_oom();
+
+ text = strjoin(header, pre, "int", post, ")");
+ if (!text)
+ return log_oom();
+
+ return varlink_idl_parse(text, NULL, NULL, &parsed);
+}
+
+TEST(recursive) {
+ assert_se(test_recursive_one(32) >= 0);
+ assert_se(test_recursive_one(64) >= 0);
+
+ /* We should handle this gracefully without a stack overflow */
+ assert_se(test_recursive_one(65) < 0);
+ assert_se(test_recursive_one(20000) < 0 );
+}
+
+static int test_method(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+ JsonVariant *foo = json_variant_by_key(parameters, "foo"), *bar = json_variant_by_key(parameters, "bar");
+
+ return varlink_replyb(link,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_UNSIGNED("waldo", json_variant_unsigned(foo) * json_variant_unsigned(bar)),
+ JSON_BUILD_PAIR_UNSIGNED("quux", json_variant_unsigned(foo) + json_variant_unsigned(bar))));
+}
+
+static int done_method(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+ assert_se(sd_event_exit(varlink_get_event(link), 0) >= 0);
+ return 0;
+}
+
+static VARLINK_DEFINE_METHOD(
+ TestMethod,
+ VARLINK_DEFINE_INPUT(foo, VARLINK_INT, 0),
+ VARLINK_DEFINE_INPUT(bar, VARLINK_INT, 0),
+ VARLINK_DEFINE_INPUT(optional, VARLINK_STRING, VARLINK_NULLABLE),
+ VARLINK_DEFINE_OUTPUT(waldo, VARLINK_INT, 0),
+ VARLINK_DEFINE_OUTPUT(quux, VARLINK_INT, 0));
+
+static VARLINK_DEFINE_METHOD(Done);
+
+static VARLINK_DEFINE_INTERFACE(
+ xyz,
+ "xyz",
+ &vl_method_TestMethod,
+ &vl_method_Done);
+
+
+static void* server_thread(void *userdata) {
+ _cleanup_(varlink_server_unrefp) VarlinkServer *server = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+
+ assert_se(varlink_server_new(&server, 0) >= 0);
+ assert_se(varlink_server_add_interface(server, &vl_interface_xyz) >= 0);
+ assert_se(varlink_server_bind_method(server, "xyz.TestMethod", test_method) >= 0);
+ assert_se(varlink_server_bind_method(server, "xyz.Done", done_method) >= 0);
+
+ assert_se(sd_event_new(&event) >= 0);
+ assert_se(varlink_server_attach_event(server, event, 0) >= 0);
+
+ assert_se(varlink_server_add_connection(server, PTR_TO_FD(userdata), NULL) >= 0);
+
+ assert_se(sd_event_loop(event) >= 0);
+ return NULL;
+}
+
+TEST(validate_method_call) {
+ _cleanup_close_pair_ int fd[2] = EBADF_PAIR;
+ _cleanup_(varlink_unrefp) Varlink *v = NULL;
+ pthread_t t;
+
+ assert_se(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0, fd) >= 0);
+ assert_se(pthread_create(&t, NULL, server_thread, FD_TO_PTR(TAKE_FD(fd[1]))) == 0);
+ assert_se(varlink_connect_fd(&v, TAKE_FD(fd[0])) >= 0);
+
+ JsonVariant *reply = NULL;
+ const char *error_id = NULL;
+ assert_se(varlink_callb(v, "xyz.TestMethod", &reply, &error_id, NULL,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_UNSIGNED("foo", 8),
+ JSON_BUILD_PAIR_UNSIGNED("bar", 9))) >= 0);
+
+ _cleanup_(json_variant_unrefp) JsonVariant *expected_reply = NULL;
+ assert_se(json_build(&expected_reply,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_UNSIGNED("waldo", 8*9),
+ JSON_BUILD_PAIR_UNSIGNED("quux", 8+9))) >= 0);
+
+ assert_se(!error_id);
+
+ json_variant_dump(reply, JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO, NULL, NULL);
+ json_variant_dump(expected_reply, JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO, NULL, NULL);
+ assert_se(json_variant_equal(reply, expected_reply));
+
+ assert_se(varlink_callb(v, "xyz.TestMethod", &reply, &error_id, NULL,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_UNSIGNED("foo", 9),
+ JSON_BUILD_PAIR_UNSIGNED("bar", 8),
+ JSON_BUILD_PAIR_STRING("optional", "pfft"))) >= 0);
+
+ assert_se(!error_id);
+ assert_se(json_variant_equal(reply, expected_reply));
+
+ assert_se(varlink_callb(v, "xyz.TestMethod", &reply, &error_id, NULL,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_UNSIGNED("foo", 8),
+ JSON_BUILD_PAIR_UNSIGNED("bar", 9),
+ JSON_BUILD_PAIR_STRING("zzz", "pfft"))) >= 0);
+ assert_se(streq_ptr(error_id, VARLINK_ERROR_INVALID_PARAMETER));
+
+ assert_se(varlink_callb(v, "xyz.TestMethod", &reply, &error_id, NULL,
+ JSON_BUILD_OBJECT(
+ JSON_BUILD_PAIR_STRING("foo", "wuff"),
+ JSON_BUILD_PAIR_UNSIGNED("bar", 9))) >= 0);
+ assert_se(streq_ptr(error_id, VARLINK_ERROR_INVALID_PARAMETER));
+
+ assert_se(varlink_send(v, "xyz.Done", NULL) >= 0);
+ assert_se(varlink_flush(v) >= 0);
+ assert_se(pthread_join(t, NULL) == 0);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-varlink.c b/src/test/test-varlink.c
new file mode 100644
index 0000000..2617ed0
--- /dev/null
+++ b/src/test/test-varlink.c
@@ -0,0 +1,376 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <poll.h>
+#include <pthread.h>
+
+#include "sd-event.h"
+
+#include "data-fd-util.h"
+#include "fd-util.h"
+#include "json.h"
+#include "rm-rf.h"
+#include "strv.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+#include "user-util.h"
+#include "varlink.h"
+
+/* Let's pick some high value, that is higher than the largest listen() backlog, but leaves enough room below
+ the typical RLIMIT_NOFILE value of 1024 so that we can process both sides of each socket in our
+ process. Or in other words: "OVERLOAD_CONNECTIONS * 2 + x < 1024" should hold, for some small x that
+ should cover any auxiliary fds, the listener server fds, stdin/stdout/stderr and whatever else. */
+#define OVERLOAD_CONNECTIONS 333
+
+static int n_done = 0;
+static int block_write_fd = -EBADF;
+
+static int method_something(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+ _cleanup_(json_variant_unrefp) JsonVariant *ret = NULL;
+ JsonVariant *a, *b;
+ int64_t x, y;
+ int r;
+
+ a = json_variant_by_key(parameters, "a");
+ if (!a)
+ return varlink_error(link, "io.test.BadParameters", NULL);
+
+ x = json_variant_integer(a);
+
+ b = json_variant_by_key(parameters, "b");
+ if (!b)
+ return varlink_error(link, "io.test.BadParameters", NULL);
+
+ y = json_variant_integer(b);
+
+ r = json_build(&ret, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("sum", JSON_BUILD_INTEGER(x + y))));
+ if (r < 0)
+ return r;
+
+ return varlink_reply(link, ret);
+}
+
+static int method_something_more(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+ _cleanup_(json_variant_unrefp) JsonVariant *ret = NULL;
+ int r;
+
+ struct Something {
+ int x;
+ int y;
+ };
+
+ static const JsonDispatch dispatch_table[] = {
+ { "a", JSON_VARIANT_INTEGER, json_dispatch_int, offsetof(struct Something, x), JSON_MANDATORY },
+ { "b", JSON_VARIANT_INTEGER, json_dispatch_int, offsetof(struct Something, y), JSON_MANDATORY},
+ {}
+ };
+ struct Something s = {};
+
+ r = varlink_dispatch(link, parameters, dispatch_table, &s);
+ if (r != 0)
+ return r;
+
+ for (int i = 0; i < 5; i++) {
+ _cleanup_(json_variant_unrefp) JsonVariant *w = NULL;
+
+ r = json_build(&w, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("sum", JSON_BUILD_INTEGER(s.x + (s.y * i)))));
+ if (r < 0)
+ return r;
+
+ r = varlink_notify(link, w);
+ if (r < 0)
+ return r;
+ }
+
+ r = json_build(&ret, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("sum", JSON_BUILD_INTEGER(s.x + (s.y * 5)))));
+ if (r < 0)
+ return r;
+
+ return varlink_reply(link, ret);
+}
+
+static void test_fd(int fd, const void *buf, size_t n) {
+ char rbuf[n + 1];
+ ssize_t m;
+
+ m = read(fd, rbuf, n + 1);
+ assert_se(m >= 0);
+ assert_se(memcmp_nn(buf, n, rbuf, m) == 0);
+}
+
+static int method_passfd(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+ _cleanup_(json_variant_unrefp) JsonVariant *ret = NULL;
+ JsonVariant *a;
+ int r;
+
+ a = json_variant_by_key(parameters, "fd");
+ if (!a)
+ return varlink_error(link, "io.test.BadParameters", NULL);
+
+ assert_se(streq_ptr(json_variant_string(a), "whoop"));
+
+ int xx = varlink_peek_fd(link, 0),
+ yy = varlink_peek_fd(link, 1),
+ zz = varlink_peek_fd(link, 2);
+
+ log_info("%i %i %i", xx, yy, zz);
+
+ assert_se(xx >= 0);
+ assert_se(yy >= 0);
+ assert_se(zz >= 0);
+
+ test_fd(xx, "foo", 3);
+ test_fd(yy, "bar", 3);
+ test_fd(zz, "quux", 4);
+
+ _cleanup_close_ int vv = acquire_data_fd("miau", 4, 0);
+ _cleanup_close_ int ww = acquire_data_fd("wuff", 4, 0);
+
+ assert_se(vv >= 0);
+ assert_se(ww >= 0);
+
+ r = json_build(&ret, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("yo", JSON_BUILD_INTEGER(88))));
+ if (r < 0)
+ return r;
+
+ assert_se(varlink_push_fd(link, vv) == 0);
+ assert_se(varlink_push_fd(link, ww) == 1);
+
+ TAKE_FD(vv);
+ TAKE_FD(ww);
+
+ return varlink_reply(link, ret);
+}
+
+static int method_done(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+
+ if (++n_done == 2)
+ sd_event_exit(varlink_get_event(link), EXIT_FAILURE);
+
+ return 0;
+}
+
+static int reply(Varlink *link, JsonVariant *parameters, const char *error_id, VarlinkReplyFlags flags, void *userdata) {
+ JsonVariant *sum;
+
+ sum = json_variant_by_key(parameters, "sum");
+
+ assert_se(json_variant_integer(sum) == 7+22);
+
+ if (++n_done == 2)
+ sd_event_exit(varlink_get_event(link), EXIT_FAILURE);
+
+ return 0;
+}
+
+static int on_connect(VarlinkServer *s, Varlink *link, void *userdata) {
+ uid_t uid = UID_INVALID;
+
+ assert_se(s);
+ assert_se(link);
+
+ assert_se(varlink_get_peer_uid(link, &uid) >= 0);
+ assert_se(getuid() == uid);
+ assert_se(varlink_set_allow_fd_passing_input(link, true) >= 0);
+ assert_se(varlink_set_allow_fd_passing_output(link, true) >= 0);
+
+ return 0;
+}
+
+static int overload_reply(Varlink *link, JsonVariant *parameters, const char *error_id, VarlinkReplyFlags flags, void *userdata) {
+
+ /* This method call reply should always be called with a disconnection, since the method call should
+ * be talking to an overloaded server */
+
+ log_debug("Over reply triggered with error: %s", strna(error_id));
+ assert_se(streq(error_id, VARLINK_ERROR_DISCONNECTED));
+ sd_event_exit(varlink_get_event(link), 0);
+
+ return 0;
+}
+
+static void flood_test(const char *address) {
+ _cleanup_(varlink_flush_close_unrefp) Varlink *c = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ _cleanup_free_ Varlink **connections = NULL;
+ size_t k;
+ char x = 'x';
+
+ log_debug("Flooding server...");
+
+ /* Block the main event loop while we flood */
+ assert_se(write(block_write_fd, &x, sizeof(x)) == sizeof(x));
+
+ assert_se(sd_event_default(&e) >= 0);
+
+ /* Flood the server with connections */
+ assert_se(connections = new0(Varlink*, OVERLOAD_CONNECTIONS));
+ for (k = 0; k < OVERLOAD_CONNECTIONS; k++) {
+ _cleanup_free_ char *t = NULL;
+ log_debug("connection %zu", k);
+ assert_se(varlink_connect_address(connections + k, address) >= 0);
+
+ assert_se(asprintf(&t, "flood-%zu", k) >= 0);
+ assert_se(varlink_set_description(connections[k], t) >= 0);
+ assert_se(varlink_attach_event(connections[k], e, k) >= 0);
+ assert_se(varlink_sendb(connections[k], "io.test.Rubbish", JSON_BUILD_OBJECT(JSON_BUILD_PAIR("id", JSON_BUILD_INTEGER(k)))) >= 0);
+ }
+
+ /* Then, create one more, which should fail */
+ log_debug("Creating overload connection...");
+ assert_se(varlink_connect_address(&c, address) >= 0);
+ assert_se(varlink_set_description(c, "overload-client") >= 0);
+ assert_se(varlink_attach_event(c, e, k) >= 0);
+ assert_se(varlink_bind_reply(c, overload_reply) >= 0);
+ assert_se(varlink_invokeb(c, "io.test.Overload", JSON_BUILD_OBJECT(JSON_BUILD_PAIR("foo", JSON_BUILD_CONST_STRING("bar")))) >= 0);
+
+ /* Unblock it */
+ log_debug("Unblocking server...");
+ block_write_fd = safe_close(block_write_fd);
+
+ /* This loop will terminate as soon as the overload reply callback is called */
+ assert_se(sd_event_loop(e) >= 0);
+
+ /* And close all connections again */
+ for (k = 0; k < OVERLOAD_CONNECTIONS; k++)
+ connections[k] = varlink_unref(connections[k]);
+}
+
+static void *thread(void *arg) {
+ _cleanup_(varlink_flush_close_unrefp) Varlink *c = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *i = NULL, *j = NULL;
+ JsonVariant *o = NULL, *k = NULL;
+ const char *error_id;
+ VarlinkReplyFlags flags = 0;
+ const char *e;
+ int x = 0;
+
+ assert_se(json_build(&i, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("a", JSON_BUILD_INTEGER(88)),
+ JSON_BUILD_PAIR("b", JSON_BUILD_INTEGER(99)))) >= 0);
+
+ assert_se(varlink_connect_address(&c, arg) >= 0);
+ assert_se(varlink_set_description(c, "thread-client") >= 0);
+ assert_se(varlink_set_allow_fd_passing_input(c, true) >= 0);
+ assert_se(varlink_set_allow_fd_passing_output(c, true) >= 0);
+
+ assert_se(varlink_collect(c, "io.test.DoSomethingMore", i, &j, &error_id, &flags) >= 0);
+
+ assert_se(!error_id);
+ assert_se(!flags);
+ assert_se(json_variant_is_array(j) && !json_variant_is_blank_array(j));
+
+ JSON_VARIANT_ARRAY_FOREACH(k, j) {
+ assert_se(json_variant_integer(json_variant_by_key(k, "sum")) == 88 + (99 * x));
+ x++;
+ }
+ assert_se(x == 6);
+
+ assert_se(varlink_call(c, "io.test.DoSomething", i, &o, &e, NULL) >= 0);
+ assert_se(json_variant_integer(json_variant_by_key(o, "sum")) == 88 + 99);
+ assert_se(!e);
+
+ int fd1 = acquire_data_fd("foo", 3, 0);
+ int fd2 = acquire_data_fd("bar", 3, 0);
+ int fd3 = acquire_data_fd("quux", 4, 0);
+
+ assert_se(fd1 >= 0);
+ assert_se(fd2 >= 0);
+ assert_se(fd3 >= 0);
+
+ assert_se(varlink_push_fd(c, fd1) == 0);
+ assert_se(varlink_push_fd(c, fd2) == 1);
+ assert_se(varlink_push_fd(c, fd3) == 2);
+
+ assert_se(varlink_callb(c, "io.test.PassFD", &o, &e, NULL, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("fd", JSON_BUILD_STRING("whoop")))) >= 0);
+
+ int fd4 = varlink_peek_fd(c, 0);
+ int fd5 = varlink_peek_fd(c, 1);
+
+ assert_se(fd4 >= 0);
+ assert_se(fd5 >= 0);
+
+ test_fd(fd4, "miau", 4);
+ test_fd(fd5, "wuff", 4);
+
+ assert_se(varlink_callb(c, "io.test.IDontExist", &o, &e, NULL, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("x", JSON_BUILD_REAL(5.5)))) >= 0);
+ assert_se(streq_ptr(json_variant_string(json_variant_by_key(o, "method")), "io.test.IDontExist"));
+ assert_se(streq(e, VARLINK_ERROR_METHOD_NOT_FOUND));
+
+ flood_test(arg);
+
+ assert_se(varlink_send(c, "io.test.Done", NULL) >= 0);
+
+ return NULL;
+}
+
+static int block_fd_handler(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ char c;
+
+ assert_se(fd_nonblock(fd, false) >= 0);
+
+ assert_se(read(fd, &c, sizeof(c)) == sizeof(c));
+ /* When a character is written to this pipe we'll block until the pipe is closed. */
+
+ assert_se(read(fd, &c, sizeof(c)) == 0);
+
+ assert_se(fd_nonblock(fd, true) >= 0);
+
+ assert_se(sd_event_source_set_enabled(s, SD_EVENT_OFF) >= 0);
+
+ return 0;
+}
+
+int main(int argc, char *argv[]) {
+ _cleanup_(sd_event_source_unrefp) sd_event_source *block_event = NULL;
+ _cleanup_(varlink_server_unrefp) VarlinkServer *s = NULL;
+ _cleanup_(varlink_flush_close_unrefp) Varlink *c = NULL;
+ _cleanup_(rm_rf_physical_and_freep) char *tmpdir = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *v = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ _cleanup_close_pair_ int block_fds[2] = EBADF_PAIR;
+ pthread_t t;
+ const char *sp;
+
+ test_setup_logging(LOG_DEBUG);
+
+ assert_se(mkdtemp_malloc("/tmp/varlink-test-XXXXXX", &tmpdir) >= 0);
+ sp = strjoina(tmpdir, "/socket");
+
+ assert_se(sd_event_default(&e) >= 0);
+
+ assert_se(pipe2(block_fds, O_NONBLOCK|O_CLOEXEC) >= 0);
+ assert_se(sd_event_add_io(e, &block_event, block_fds[0], EPOLLIN, block_fd_handler, NULL) >= 0);
+ assert_se(sd_event_source_set_priority(block_event, SD_EVENT_PRIORITY_IMPORTANT) >= 0);
+ block_write_fd = TAKE_FD(block_fds[1]);
+
+ assert_se(varlink_server_new(&s, VARLINK_SERVER_ACCOUNT_UID) >= 0);
+ assert_se(varlink_server_set_description(s, "our-server") >= 0);
+
+ assert_se(varlink_server_bind_method(s, "io.test.PassFD", method_passfd) >= 0);
+ assert_se(varlink_server_bind_method(s, "io.test.DoSomething", method_something) >= 0);
+ assert_se(varlink_server_bind_method(s, "io.test.DoSomethingMore", method_something_more) >= 0);
+ assert_se(varlink_server_bind_method(s, "io.test.Done", method_done) >= 0);
+ assert_se(varlink_server_bind_connect(s, on_connect) >= 0);
+ assert_se(varlink_server_listen_address(s, sp, 0600) >= 0);
+ assert_se(varlink_server_attach_event(s, e, 0) >= 0);
+ assert_se(varlink_server_set_connections_max(s, OVERLOAD_CONNECTIONS) >= 0);
+
+ assert_se(json_build(&v, JSON_BUILD_OBJECT(JSON_BUILD_PAIR("a", JSON_BUILD_INTEGER(7)),
+ JSON_BUILD_PAIR("b", JSON_BUILD_INTEGER(22)))) >= 0);
+
+ assert_se(varlink_connect_address(&c, sp) >= 0);
+ assert_se(varlink_set_description(c, "main-client") >= 0);
+ assert_se(varlink_bind_reply(c, reply) >= 0);
+
+ assert_se(varlink_invoke(c, "io.test.DoSomething", v) >= 0);
+
+ assert_se(varlink_attach_event(c, e, 0) >= 0);
+
+ assert_se(pthread_create(&t, NULL, thread, (void*) sp) == 0);
+
+ assert_se(sd_event_loop(e) >= 0);
+
+ assert_se(pthread_join(t, NULL) == 0);
+
+ return 0;
+}
diff --git a/src/test/test-verbs.c b/src/test/test-verbs.c
new file mode 100644
index 0000000..6e30794
--- /dev/null
+++ b/src/test/test-verbs.c
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+
+#include "macro.h"
+#include "strv.h"
+#include "tests.h"
+#include "verbs.h"
+
+static int noop_dispatcher(int argc, char *argv[], void *userdata) {
+ return 0;
+}
+
+#define test_dispatch_one(argv, verbs, expected) \
+ optind = 0; \
+ assert_se(dispatch_verb(strv_length(argv), argv, verbs, NULL) == expected);
+
+TEST(verbs) {
+ static const Verb verbs[] = {
+ { "help", VERB_ANY, VERB_ANY, 0, noop_dispatcher },
+ { "list-images", VERB_ANY, 1, 0, noop_dispatcher },
+ { "list", VERB_ANY, 2, VERB_DEFAULT, noop_dispatcher },
+ { "status", 2, VERB_ANY, 0, noop_dispatcher },
+ { "show", VERB_ANY, VERB_ANY, 0, noop_dispatcher },
+ { "terminate", 2, VERB_ANY, 0, noop_dispatcher },
+ { "login", 2, 2, 0, noop_dispatcher },
+ { "copy-to", 3, 4, 0, noop_dispatcher },
+ {}
+ };
+
+ /* not found */
+ test_dispatch_one(STRV_MAKE("command-not-found"), verbs, -EINVAL);
+
+ /* found */
+ test_dispatch_one(STRV_MAKE("show"), verbs, 0);
+
+ /* found, too few args */
+ test_dispatch_one(STRV_MAKE("copy-to", "foo"), verbs, -EINVAL);
+
+ /* found, meets min args */
+ test_dispatch_one(STRV_MAKE("status", "foo", "bar"), verbs, 0);
+
+ /* found, too many args */
+ test_dispatch_one(STRV_MAKE("copy-to", "foo", "bar", "baz", "quux", "qaax"), verbs, -EINVAL);
+
+ /* no verb, but a default is set */
+ test_dispatch_one(STRV_MAKE_EMPTY, verbs, 0);
+}
+
+TEST(verbs_no_default) {
+ static const Verb verbs[] = {
+ { "help", VERB_ANY, VERB_ANY, 0, noop_dispatcher },
+ {},
+ };
+
+ test_dispatch_one(STRV_MAKE(NULL), verbs, -EINVAL);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-watch-pid.c b/src/test/test-watch-pid.c
new file mode 100644
index 0000000..b0c2c06
--- /dev/null
+++ b/src/test/test-watch-pid.c
@@ -0,0 +1,102 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "log.h"
+#include "manager.h"
+#include "process-util.h"
+#include "rm-rf.h"
+#include "service.h"
+#include "tests.h"
+
+int main(int argc, char *argv[]) {
+ _cleanup_(rm_rf_physical_and_freep) char *runtime_dir = NULL;
+ _cleanup_(manager_freep) Manager *m = NULL;
+ Unit *a, *b, *c, *u;
+ int r;
+
+ test_setup_logging(LOG_DEBUG);
+
+ if (getuid() != 0)
+ return log_tests_skipped("not root");
+ r = enter_cgroup_subroot(NULL);
+ if (r == -ENOMEDIUM)
+ return log_tests_skipped("cgroupfs not available");
+
+ _cleanup_free_ char *unit_dir = NULL;
+ assert_se(get_testdata_dir("units/", &unit_dir) >= 0);
+ assert_se(set_unit_path(unit_dir) >= 0);
+
+ assert_se(runtime_dir = setup_fake_runtime_dir());
+
+ assert_se(manager_new(RUNTIME_SCOPE_USER, MANAGER_TEST_RUN_BASIC, &m) >= 0);
+ assert_se(manager_startup(m, NULL, NULL, NULL) >= 0);
+
+ assert_se(a = unit_new(m, sizeof(Service)));
+ assert_se(unit_add_name(a, "a.service") >= 0);
+ assert_se(set_isempty(a->pids));
+
+ assert_se(b = unit_new(m, sizeof(Service)));
+ assert_se(unit_add_name(b, "b.service") >= 0);
+ assert_se(set_isempty(b->pids));
+
+ assert_se(c = unit_new(m, sizeof(Service)));
+ assert_se(unit_add_name(c, "c.service") >= 0);
+ assert_se(set_isempty(c->pids));
+
+ /* Fork off a child so that we have a PID to watch */
+ _cleanup_(sigkill_waitp) pid_t pid = 0;
+ pid = fork();
+ if (pid == 0) {
+ /* Child */
+ pause();
+ _exit(EXIT_SUCCESS);
+ }
+
+ assert_se(pid >= 0);
+
+ assert_se(hashmap_isempty(m->watch_pids));
+ assert_se(manager_get_unit_by_pid(m, pid) == NULL);
+
+ assert_se(unit_watch_pid(a, pid, false) >= 0);
+ assert_se(manager_get_unit_by_pid(m, pid) == a);
+
+ assert_se(unit_watch_pid(a, pid, false) >= 0);
+ assert_se(manager_get_unit_by_pid(m, pid) == a);
+
+ assert_se(unit_watch_pid(b, pid, false) >= 0);
+ u = manager_get_unit_by_pid(m, pid);
+ assert_se(u == a || u == b);
+
+ assert_se(unit_watch_pid(b, pid, false) >= 0);
+ u = manager_get_unit_by_pid(m, pid);
+ assert_se(u == a || u == b);
+
+ assert_se(unit_watch_pid(c, pid, false) >= 0);
+ u = manager_get_unit_by_pid(m, pid);
+ assert_se(u == a || u == b || u == c);
+
+ assert_se(unit_watch_pid(c, pid, false) >= 0);
+ u = manager_get_unit_by_pid(m, pid);
+ assert_se(u == a || u == b || u == c);
+
+ unit_unwatch_pid(b, pid);
+ u = manager_get_unit_by_pid(m, pid);
+ assert_se(u == a || u == c);
+
+ unit_unwatch_pid(b, pid);
+ u = manager_get_unit_by_pid(m, pid);
+ assert_se(u == a || u == c);
+
+ unit_unwatch_pid(a, pid);
+ assert_se(manager_get_unit_by_pid(m, pid) == c);
+
+ unit_unwatch_pid(a, pid);
+ assert_se(manager_get_unit_by_pid(m, pid) == c);
+
+ unit_unwatch_pid(c, pid);
+ assert_se(manager_get_unit_by_pid(m, pid) == NULL);
+
+ unit_unwatch_pid(c, pid);
+ assert_se(manager_get_unit_by_pid(m, pid) == NULL);
+
+ return 0;
+}
diff --git a/src/test/test-watchdog.c b/src/test/test-watchdog.c
new file mode 100644
index 0000000..70d069c
--- /dev/null
+++ b/src/test/test-watchdog.c
@@ -0,0 +1,39 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <string.h>
+#include <unistd.h>
+
+#include "log.h"
+#include "tests.h"
+#include "watchdog.h"
+
+int main(int argc, char *argv[]) {
+ usec_t t;
+ unsigned i, count;
+ int r;
+ bool slow;
+
+ test_setup_logging(LOG_DEBUG);
+
+ slow = slow_tests_enabled();
+
+ t = slow ? 10 * USEC_PER_SEC : 2 * USEC_PER_SEC;
+ count = slow ? 5 : 3;
+
+ r = watchdog_setup(t);
+ if (r < 0)
+ log_warning_errno(r, "Failed to open watchdog: %m");
+
+ for (i = 0; i < count; i++) {
+ t = watchdog_runtime_wait();
+ log_info("Sleeping " USEC_FMT " microseconds...", t);
+ usleep_safe(t);
+ log_info("Pinging...");
+ r = watchdog_ping();
+ if (r < 0)
+ log_warning_errno(r, "Failed to ping watchdog: %m");
+ }
+
+ watchdog_close(true);
+ return 0;
+}
diff --git a/src/test/test-web-util.c b/src/test/test-web-util.c
new file mode 100644
index 0000000..d376d4a
--- /dev/null
+++ b/src/test/test-web-util.c
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "macro.h"
+#include "tests.h"
+#include "web-util.h"
+
+TEST(is_valid_documentation_url) {
+ assert_se(documentation_url_is_valid("https://www.freedesktop.org/wiki/Software/systemd"));
+ assert_se(documentation_url_is_valid("https://www.kernel.org/doc/Documentation/binfmt_misc.txt")); /* dead */
+ assert_se(documentation_url_is_valid("https://www.kernel.org/doc/Documentation/admin-guide/binfmt-misc.rst"));
+ assert_se(documentation_url_is_valid("https://docs.kernel.org/admin-guide/binfmt-misc.html"));
+ assert_se(documentation_url_is_valid("file:/foo/foo"));
+ assert_se(documentation_url_is_valid("man:systemd.special(7)"));
+ assert_se(documentation_url_is_valid("info:bar"));
+
+ assert_se(!documentation_url_is_valid("foo:"));
+ assert_se(!documentation_url_is_valid("info:"));
+ assert_se(!documentation_url_is_valid(""));
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/test/test-xattr-util.c b/src/test/test-xattr-util.c
new file mode 100644
index 0000000..85901c9
--- /dev/null
+++ b/src/test/test-xattr-util.c
@@ -0,0 +1,129 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/xattr.h>
+#include <unistd.h>
+
+#include "alloc-util.h"
+#include "fd-util.h"
+#include "fs-util.h"
+#include "macro.h"
+#include "rm-rf.h"
+#include "string-util.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+#include "xattr-util.h"
+
+TEST(getxattr_at_malloc) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ _cleanup_free_ char *value = NULL;
+ _cleanup_close_ int fd = -EBADF;
+ const char *x;
+ int r;
+
+ fd = mkdtemp_open("/var/tmp/test-xattrtestXXXXXX", O_RDONLY|O_NOCTTY, &t);
+ assert_se(fd >= 0);
+ x = strjoina(t, "/test");
+ assert_se(touch(x) >= 0);
+
+ r = setxattr(x, "user.foo", "bar", 3, 0);
+ if (r < 0 && ERRNO_IS_NOT_SUPPORTED(errno))
+ return (void) log_tests_skipped_errno(errno, "no xattrs supported on /var/tmp");
+ assert_se(r >= 0);
+
+ assert_se(getxattr_at_malloc(fd, "test", "user.foo", 0, &value) == 3);
+ assert_se(memcmp(value, "bar", 3) == 0);
+ value = mfree(value);
+
+ assert_se(getxattr_at_malloc(AT_FDCWD, x, "user.foo", 0, &value) == 3);
+ assert_se(memcmp(value, "bar", 3) == 0);
+ value = mfree(value);
+
+ safe_close(fd);
+ fd = open("/", O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOCTTY);
+ assert_se(fd >= 0);
+ r = getxattr_at_malloc(fd, "usr", "user.idontexist", 0, &value);
+ assert_se(r < 0 && ERRNO_IS_XATTR_ABSENT(r));
+
+ safe_close(fd);
+ fd = open(x, O_PATH|O_CLOEXEC);
+ assert_se(fd >= 0);
+ assert_se(getxattr_at_malloc(fd, NULL, "user.foo", 0, &value) == 3);
+ assert_se(streq(value, "bar"));
+}
+
+TEST(getcrtime) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ _cleanup_close_ int fd = -EBADF;
+ usec_t usec, k;
+ int r;
+
+ fd = mkdtemp_open("/var/tmp/test-xattrtestXXXXXX", 0, &t);
+ assert_se(fd >= 0);
+
+ r = fd_getcrtime(fd, &usec);
+ if (r < 0)
+ log_debug_errno(r, "btime: %m");
+ else
+ log_debug("btime: %s", FORMAT_TIMESTAMP(usec));
+
+ k = now(CLOCK_REALTIME);
+
+ r = fd_setcrtime(fd, 1519126446UL * USEC_PER_SEC);
+ if (!IN_SET(r, -EOPNOTSUPP, -ENOTTY)) {
+ assert_se(fd_getcrtime(fd, &usec) >= 0);
+ assert_se(k < 1519126446UL * USEC_PER_SEC ||
+ usec == 1519126446UL * USEC_PER_SEC);
+ }
+}
+
+static void verify_xattr(int dfd, const char *expected) {
+ _cleanup_free_ char *value = NULL;
+
+ assert_se(getxattr_at_malloc(dfd, "test", "user.foo", 0, &value) == (int) strlen(expected));
+ assert_se(streq(value, expected));
+}
+
+TEST(xsetxattr) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ _cleanup_close_ int dfd = -EBADF, fd = -EBADF;
+ const char *x;
+ int r;
+
+ dfd = mkdtemp_open("/var/tmp/test-xattrtestXXXXXX", O_PATH, &t);
+ assert_se(dfd >= 0);
+ x = strjoina(t, "/test");
+ assert_se(touch(x) >= 0);
+
+ /* by full path */
+ r = xsetxattr(AT_FDCWD, x, "user.foo", "fullpath", SIZE_MAX, 0);
+ if (r < 0 && ERRNO_IS_NOT_SUPPORTED(r))
+ return (void) log_tests_skipped_errno(r, "no xattrs supported on /var/tmp");
+ assert_se(r >= 0);
+ verify_xattr(dfd, "fullpath");
+
+ /* by dirfd */
+ assert_se(xsetxattr(dfd, "test", "user.foo", "dirfd", SIZE_MAX, 0) >= 0);
+ verify_xattr(dfd, "dirfd");
+
+ /* by fd (O_PATH) */
+ fd = openat(dfd, "test", O_PATH|O_CLOEXEC);
+ assert_se(fd >= 0);
+ assert_se(xsetxattr(fd, NULL, "user.foo", "fd_opath", SIZE_MAX, 0) >= 0);
+ verify_xattr(dfd, "fd_opath");
+ assert_se(xsetxattr(fd, "", "user.foo", "fd_opath", SIZE_MAX, 0) == -EINVAL);
+ assert_se(xsetxattr(fd, "", "user.foo", "fd_opath_empty", SIZE_MAX, AT_EMPTY_PATH) >= 0);
+ verify_xattr(dfd, "fd_opath_empty");
+ fd = safe_close(fd);
+
+ fd = openat(dfd, "test", O_RDONLY|O_CLOEXEC);
+ assert_se(xsetxattr(fd, NULL, "user.foo", "fd_regular", SIZE_MAX, 0) >= 0);
+ verify_xattr(dfd, "fd_regular");
+ assert_se(xsetxattr(fd, "", "user.foo", "fd_regular_empty", SIZE_MAX, 0) == -EINVAL);
+ assert_se(xsetxattr(fd, "", "user.foo", "fd_regular_empty", SIZE_MAX, AT_EMPTY_PATH) >= 0);
+ verify_xattr(dfd, "fd_regular_empty");
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-xml.c b/src/test/test-xml.c
new file mode 100644
index 0000000..a8cb635
--- /dev/null
+++ b/src/test/test-xml.c
@@ -0,0 +1,68 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdarg.h>
+
+#include "alloc-util.h"
+#include "string-util.h"
+#include "tests.h"
+#include "xml.h"
+
+static void test_one(const char *data, ...) {
+ void *state = NULL;
+ va_list ap;
+
+ va_start(ap, data);
+
+ for (;;) {
+ _cleanup_free_ char *name = NULL;
+ int t, tt;
+ const char *nn;
+
+ t = xml_tokenize(&data, &name, &state, NULL);
+ assert_se(t >= 0);
+
+ tt = va_arg(ap, int);
+ assert_se(tt >= 0);
+
+ assert_se(t == tt);
+ if (t == XML_END)
+ break;
+
+ nn = va_arg(ap, const char *);
+ assert_se(streq_ptr(nn, name));
+ }
+
+ va_end(ap);
+}
+
+int main(int argc, char *argv[]) {
+
+ test_setup_logging(LOG_DEBUG);
+
+ test_one("", XML_END);
+
+ test_one("<foo></foo>",
+ XML_TAG_OPEN, "foo",
+ XML_TAG_CLOSE, "foo",
+ XML_END);
+
+ test_one("<foo waldo=piep meh=\"huhu\"/>",
+ XML_TAG_OPEN, "foo",
+ XML_ATTRIBUTE_NAME, "waldo",
+ XML_ATTRIBUTE_VALUE, "piep",
+ XML_ATTRIBUTE_NAME, "meh",
+ XML_ATTRIBUTE_VALUE, "huhu",
+ XML_TAG_CLOSE_EMPTY, NULL,
+ XML_END);
+
+ test_one("xxxx\n"
+ "<foo><?xml foo?> <!-- zzzz --> </foo>",
+ XML_TEXT, "xxxx\n",
+ XML_TAG_OPEN, "foo",
+ XML_TEXT, " ",
+ XML_TEXT, " ",
+ XML_TAG_CLOSE, "foo",
+ XML_END);
+
+ return 0;
+}