1
+/* SPDX-License-Identifier: GPL-2.0-or-later */
2
+/*
3
+ * envlist tests
4
+ *
5
+ * Copyright 2026 Virtuozzo International GmbH
6
+ *
7
+ * Authors:
8
+ * Denis V. Lunev <den@openvz.org>
9
+ */
10
+
11
+#include "qemu/osdep.h"
12
+#include "qemu/envlist.h"
13
+
14
+static void free_environ(char **env)
15
+{
16
+ char **p;
17
+
18
+ for (p = env; *p != NULL; p++) {
19
+ g_free(*p);
20
+ }
21
+ g_free(env);
22
+}
23
+
24
+static const char *find_env(char **env, const char *name)
25
+{
26
+ size_t name_len = strlen(name);
27
+ char **p;
28
+
29
+ for (p = env; *p != NULL; p++) {
30
+ if (strncmp(*p, name, name_len) == 0 && (*p)[name_len] == '=') {
31
+ return *p + name_len + 1;
32
+ }
33
+ }
34
+ return NULL;
35
+}
36
+
37
+static void test_envlist_basic(void)
38
+{
39
+ envlist_t *el = envlist_create();
40
+ char **env;
41
+ size_t count;
42
+
43
+ /* empty list */
44
+ env = envlist_to_environ(el, &count);
45
+ g_assert_cmpuint(count, ==, 0);
46
+ g_assert_null(env[0]);
47
+ free_environ(env);
48
+
49
+ /* add */
50
+ g_assert_cmpint(envlist_setenv(el, "A=1"), ==, 0);
51
+ g_assert_cmpint(envlist_setenv(el, "B=2"), ==, 0);
52
+
53
+ env = envlist_to_environ(el, &count);
54
+ g_assert_cmpuint(count, ==, 2);
55
+ g_assert_cmpstr(find_env(env, "A"), ==, "1");
56
+ g_assert_cmpstr(find_env(env, "B"), ==, "2");
57
+ free_environ(env);
58
+
59
+ /* replace */
60
+ g_assert_cmpint(envlist_setenv(el, "A=42"), ==, 0);
61
+ env = envlist_to_environ(el, &count);
62
+ g_assert_cmpuint(count, ==, 2);
63
+ g_assert_cmpstr(find_env(env, "A"), ==, "42");
64
+ g_assert_cmpstr(find_env(env, "B"), ==, "2");
65
+ free_environ(env);
66
+
67
+ /* unset existing */
68
+ g_assert_cmpint(envlist_unsetenv(el, "A"), ==, 0);
69
+ env = envlist_to_environ(el, &count);
70
+ g_assert_cmpuint(count, ==, 1);
71
+ g_assert_null(find_env(env, "A"));
72
+ g_assert_cmpstr(find_env(env, "B"), ==, "2");
73
+ free_environ(env);
74
+
75
+ /* unset non-existing is a no-op success */
76
+ g_assert_cmpint(envlist_unsetenv(el, "NOPE"), ==, 0);
77
+ env = envlist_to_environ(el, &count);
78
+ g_assert_cmpuint(count, ==, 1);
79
+ free_environ(env);
80
+
81
+ envlist_free(el);
82
+}
83
+
84
+/*
85
+ * envlist_setenv() inserts at the head; envlist_to_environ() walks
86
+ * head-to-tail, so the last setenv comes out first.
87
+ */
88
+static void test_envlist_head_insertion_order(void)
89
+{
90
+ envlist_t *el = envlist_create();
91
+ char **env;
92
+ size_t count;
93
+
94
+ g_assert_cmpint(envlist_setenv(el, "A=1"), ==, 0);
95
+ g_assert_cmpint(envlist_setenv(el, "B=2"), ==, 0);
96
+ g_assert_cmpint(envlist_setenv(el, "C=3"), ==, 0);
97
+
98
+ env = envlist_to_environ(el, &count);
99
+ g_assert_cmpuint(count, ==, 3);
100
+ g_assert_cmpstr(env[0], ==, "C=3");
101
+ g_assert_cmpstr(env[1], ==, "B=2");
102
+ g_assert_cmpstr(env[2], ==, "A=1");
103
+ g_assert_null(env[3]);
104
+
105
+ free_environ(env);
106
+ envlist_free(el);
107
+}
108
+
109
+static void test_envlist_einval(void)
110
+{
111
+ envlist_t *el = envlist_create();
112
+
113
+ /* NULL list */
114
+ g_assert_cmpint(envlist_setenv(NULL, "A=1"), ==, EINVAL);
115
+ g_assert_cmpint(envlist_unsetenv(NULL, "A"), ==, EINVAL);
116
+
117
+ /* NULL string */
118
+ g_assert_cmpint(envlist_setenv(el, NULL), ==, EINVAL);
119
+ g_assert_cmpint(envlist_unsetenv(el, NULL), ==, EINVAL);
120
+
121
+ /* setenv: missing '=' */
122
+ g_assert_cmpint(envlist_setenv(el, "NOEQ"), ==, EINVAL);
123
+
124
+ /* unsetenv: name must not contain '=' */
125
+ g_assert_cmpint(envlist_unsetenv(el, "A=B"), ==, EINVAL);
126
+
127
+ envlist_free(el);
128
+}
129
+
130
+/*
131
+ * Regression: envlist_unsetenv("FOO") must not remove an entry named
132
+ * "FOOBAR" -- the previous strncmp(entry, name, strlen(name)) lookup
133
+ * prefix-matched. To trigger the bug, the longer-named entry has to
134
+ * be ahead of the target in the list: envlist_setenv() inserts at
135
+ * the head, so we add FOO first and FOOBAR last.
136
+ */
137
+static void test_envlist_unsetenv_no_prefix_match(void)
138
+{
139
+ envlist_t *el = envlist_create();
140
+ char **env;
141
+ size_t count;
142
+
143
+ g_assert_cmpint(envlist_setenv(el, "FOO=y"), ==, 0);
144
+ g_assert_cmpint(envlist_setenv(el, "FOOBAR=x"), ==, 0);
145
+
146
+ g_assert_cmpint(envlist_unsetenv(el, "FOO"), ==, 0);
147
+
148
+ env = envlist_to_environ(el, &count);
149
+ g_assert_cmpuint(count, ==, 1);
150
+ g_assert_cmpstr(find_env(env, "FOOBAR"), ==, "x");
151
+ g_assert_null(find_env(env, "FOO"));
152
+
153
+ free_environ(env);
154
+ envlist_free(el);
155
+}
156
+
157
+/*
158
+ * envlist_setenv() must not replace a prior FOOBAR=... entry when
159
+ * setting FOO=... The pre-fix code happened to be safe here only
160
+ * because it included the trailing '=' byte in its strncmp length;
161
+ * this test pins down the post-fix contract that the name boundary
162
+ * is a property of the entry, not of the encoded form.
163
+ */
164
+static void test_envlist_setenv_no_prefix_match(void)
165
+{
166
+ envlist_t *el = envlist_create();
167
+ char **env;
168
+ size_t count;
169
+
170
+ g_assert_cmpint(envlist_setenv(el, "FOOBAR=x"), ==, 0);
171
+ g_assert_cmpint(envlist_setenv(el, "FOO=y"), ==, 0);
172
+
173
+ env = envlist_to_environ(el, &count);
174
+ g_assert_cmpuint(count, ==, 2);
175
+ g_assert_cmpstr(find_env(env, "FOOBAR"), ==, "x");
176
+ g_assert_cmpstr(find_env(env, "FOO"), ==, "y");
177
+
178
+ free_environ(env);
179
+ envlist_free(el);
180
+}
181
+
182
+int main(int argc, char *argv[])
183
+{
184
+ g_test_init(&argc, &argv, NULL);
185
+
186
+ g_test_add_func("/envlist/basic", test_envlist_basic);
187
+ g_test_add_func("/envlist/head_insertion_order",
188
+ test_envlist_head_insertion_order);
189
+ g_test_add_func("/envlist/einval", test_envlist_einval);
190
+ g_test_add_func("/envlist/unsetenv_no_prefix_match",
191
+ test_envlist_unsetenv_no_prefix_match);
192
+ g_test_add_func("/envlist/setenv_no_prefix_match",
193
+ test_envlist_setenv_no_prefix_match);
194
+
195
+ return g_test_run();
196
+}