1 // SPDX-License-Identifier: CDDL-1.0
2 /*
3 * This file and its contents are supplied under the terms of the
4 * Common Development and Distribution License ("CDDL"), version 1.0.
5 * You may only use this file in accordance with the terms of version
6 * 1.0 of the CDDL.
7 *
8 * A full copy of the text of the CDDL should have accompanied this
9 * source. A copy of the CDDL is also available via the Internet at
10 * http://www.illumos.org/license/CDDL.
11 */
12
13 /*
14 * Copyright (c) 2026, Christos Longros.
15 */
16
17 #include <string.h>
18
19 #include <sys/fs/zfs.h>
20 #include "zfs_namecheck.h"
21
22 #include "unit.h"
23
24 /* ========== */
25
26 /*
27 * The namecheck routines validate a name and report, via namecheck_err_t,
28 * exactly why it failed. We test them in two directions:
29 *
30 * - Validity path: randomly generated names from unit_rand_str(), which
31 * give only 'a'-'z' characters.
32 *
33 * - Invalidity path: explicit names, each tested against their specific
34 * error code since the rejection reason depends on the exact characters
35 * used.
36 */
37 typedef int (*namecheck_f)(const char *, namecheck_err_t *, char *);
38
39 /* Confirm 'name' is accepted by 'fn'. */
40 static void
check_valid(namecheck_f fn,const char * name)41 check_valid(namecheck_f fn, const char *name)
42 {
43 namecheck_err_t why = (namecheck_err_t)-1;
44 char what = '\0';
45 unit_ok(fn(name, &why, &what));
46 }
47
48 /* Confirm 'name' is rejected by 'fn' with the 'why' we expected. */
49 static void
check_invalid(namecheck_f fn,const char * name,namecheck_err_t expected)50 check_invalid(namecheck_f fn, const char *name, namecheck_err_t expected)
51 {
52 namecheck_err_t why = (namecheck_err_t)-1;
53 char what = '\0';
54 unit_err(fn(name, &why, &what), -1);
55 unit_eq(why, expected);
56 }
57
58 /* Confirm 'fn' rejects a lengthy name and returns NAME_ERR_TOOLONG. */
59 static void
check_longname_invalid(namecheck_f fn)60 check_longname_invalid(namecheck_f fn)
61 {
62 char buf[ZFS_MAX_DATASET_NAME_LEN + 16];
63 check_invalid(fn, unit_rand_str(buf, sizeof (buf)), NAME_ERR_TOOLONG);
64 }
65
66 /* ========== */
67
68 /* pool_namecheck: dataset character set that must begin with a letter. */
69 static MunitResult
test_pool_namecheck(const MunitParameter params[],void * data)70 test_pool_namecheck(const MunitParameter params[], void *data)
71 {
72 (void) params, (void) data;
73
74 /* A random array of letters is always a valid pool name. */
75 char pool[16];
76 check_valid(pool_namecheck, unit_rand_str(pool, sizeof (pool)));
77
78 /* Fixed names cover the rest of the allowed character set. */
79 check_valid(pool_namecheck, "tank_01");
80 check_valid(pool_namecheck, "Pool-2.0:label");
81
82 /* A pool name has to start with a letter. */
83 check_invalid(pool_namecheck, "0tank", NAME_ERR_NOLETTER);
84 check_invalid(pool_namecheck, "_tank", NAME_ERR_NOLETTER);
85
86 /* These pool names are reserved. */
87 check_invalid(pool_namecheck, "mirror", NAME_ERR_RESERVED);
88 check_invalid(pool_namecheck, "raidz", NAME_ERR_RESERVED);
89 check_invalid(pool_namecheck, "draid", NAME_ERR_RESERVED);
90
91 /* A pool name carries no hierarchy or snapshot delimiter. */
92 check_invalid(pool_namecheck, "tank/fs", NAME_ERR_INVALCHAR);
93 check_invalid(pool_namecheck, "tank@snap", NAME_ERR_INVALCHAR);
94
95 check_longname_invalid(pool_namecheck);
96
97 return (MUNIT_OK);
98 }
99
100 /* dataset_namecheck: any entity except a bookmark. */
101 static MunitResult
test_dataset_namecheck(const MunitParameter params[],void * data)102 test_dataset_namecheck(const MunitParameter params[], void *data)
103 {
104 (void) params, (void) data;
105
106 /* A path of random, independently-valid components is accepted. */
107 char path[64];
108 unit_rand_str(path, sizeof (path));
109 path[20] = path[40] = '/';
110 check_valid(dataset_namecheck, path);
111
112 /* A trailing snapshot is still a valid dataset name. */
113 check_valid(dataset_namecheck, "tank/home@snap");
114
115 /* '%' is allowed, for temporary clone names (online recv). */
116 check_valid(dataset_namecheck, "tank/%recv");
117
118 check_invalid(dataset_namecheck, "/tank", NAME_ERR_LEADING_SLASH);
119 check_invalid(dataset_namecheck, "tank/", NAME_ERR_TRAILING_SLASH);
120 check_invalid(dataset_namecheck, "tank//home",
121 NAME_ERR_EMPTY_COMPONENT);
122 check_invalid(dataset_namecheck, "", NAME_ERR_EMPTY_COMPONENT);
123 check_invalid(dataset_namecheck, "tank/./home", NAME_ERR_SELF_REF);
124 check_invalid(dataset_namecheck, "tank/../home", NAME_ERR_PARENT_REF);
125 check_invalid(dataset_namecheck, "tank/fs!", NAME_ERR_INVALCHAR);
126
127 /* A bookmark delimiter does not belong in a dataset name. */
128 check_invalid(dataset_namecheck, "tank/fs#bm", NAME_ERR_INVALCHAR);
129
130 check_longname_invalid(dataset_namecheck);
131
132 return (MUNIT_OK);
133 }
134
135 /* snapshot_namecheck: a valid snapshot name (an entity with '@'). */
136 static MunitResult
test_snapshot_namecheck(const MunitParameter params[],void * data)137 test_snapshot_namecheck(const MunitParameter params[], void *data)
138 {
139 (void) params, (void) data;
140
141 /* A random "filesystem@snapshot" pair is valid. */
142 char path[64];
143 unit_rand_str(path, sizeof (path));
144 path[40] = '@';
145 check_valid(snapshot_namecheck, path);
146
147 /* Without an '@' it is not a snapshot. */
148 check_invalid(snapshot_namecheck, "tank/home", NAME_ERR_NO_AT);
149
150 /* Only one delimiter is allowed. */
151 check_invalid(snapshot_namecheck, "tank@a@b",
152 NAME_ERR_MULTIPLE_DELIMITERS);
153
154 /* Nothing may follow the snapshot name with a '/'. */
155 check_invalid(snapshot_namecheck, "tank@snap/x",
156 NAME_ERR_TRAILING_SLASH);
157
158 return (MUNIT_OK);
159 }
160
161 /* bookmark_namecheck: a valid bookmark name (an entity with '#'). */
162 static MunitResult
test_bookmark_namecheck(const MunitParameter params[],void * data)163 test_bookmark_namecheck(const MunitParameter params[], void *data)
164 {
165 (void) params, (void) data;
166
167 /* A random "filesystem#bookmark" pair is valid. */
168 char path[64];
169 unit_rand_str(path, sizeof (path));
170 path[40] = '#';
171 check_valid(bookmark_namecheck, path);
172
173 /* Without a '#' it is not a bookmark. */
174 check_invalid(bookmark_namecheck, "tank/home", NAME_ERR_NO_POUND);
175
176 return (MUNIT_OK);
177 }
178
179 /* zfs_component_namecheck: one component; alphanumeric plus [-_.: ]. */
180 static MunitResult
test_component_namecheck(const MunitParameter params[],void * data)181 test_component_namecheck(const MunitParameter params[], void *data)
182 {
183 (void) params, (void) data;
184
185 /* A bare random component is valid. */
186 char comp[16];
187 unit_rand_str(comp, sizeof (comp));
188 check_valid(zfs_component_namecheck, comp);
189
190 /* An empty component is not valid. */
191 check_invalid(zfs_component_namecheck, "", NAME_ERR_EMPTY_COMPONENT);
192
193 /* A single component cannot contain a path separator. */
194 check_invalid(zfs_component_namecheck, "a/b", NAME_ERR_INVALCHAR);
195
196 check_longname_invalid(zfs_component_namecheck);
197
198 return (MUNIT_OK);
199 }
200
201 /* permset_namecheck: a permission set name, starting with '@'. */
202 static MunitResult
test_permset_namecheck(const MunitParameter params[],void * data)203 test_permset_namecheck(const MunitParameter params[], void *data)
204 {
205 (void) params, (void) data;
206
207 /* A random name behind a leading '@' is a valid permission set. */
208 char set[16];
209 set[0] = '@';
210 unit_rand_str(set + 1, sizeof (set) - 1);
211 check_valid(permset_namecheck, set);
212
213 /* It has to start with '@'. */
214 check_invalid(permset_namecheck, "backup", NAME_ERR_NO_AT);
215
216 /* The text after '@' follows the component rules. */
217 check_invalid(permset_namecheck, "@bad/name", NAME_ERR_INVALCHAR);
218
219 /* The length upper limit is checked ahead of everything else. */
220 check_longname_invalid(permset_namecheck);
221
222 return (MUNIT_OK);
223 }
224
225 /* mountpoint_namecheck: a mountpoint path, /[component][/]*. */
226 static MunitResult
test_mountpoint_namecheck(const MunitParameter params[],void * data)227 test_mountpoint_namecheck(const MunitParameter params[], void *data)
228 {
229 (void) params, (void) data;
230
231 namecheck_err_t why = (namecheck_err_t)-1;
232
233 /* An absolute path with a random component is accepted. */
234 char path[64];
235 unit_rand_str(path, sizeof (path));
236 path[0] = '/';
237 unit_ok(mountpoint_namecheck(path, &why));
238
239 /* The root mountpoint is valid. */
240 unit_ok(mountpoint_namecheck("/", &why));
241
242 /* A mountpoint must be absolute. */
243 unit_err(mountpoint_namecheck("relative/path", &why), -1);
244 unit_eq(why, NAME_ERR_LEADING_SLASH);
245
246 /* A NULL path counts as missing the leading slash. */
247 unit_err(mountpoint_namecheck(NULL, &why), -1);
248 unit_eq(why, NAME_ERR_LEADING_SLASH);
249
250 /* A long path component is rejected. */
251 char buf[ZFS_MAX_DATASET_NAME_LEN + 4];
252 buf[0] = '/';
253 (void) memset(buf + 1, 'a', sizeof (buf) - 2);
254 buf[sizeof (buf) - 1] = '\0';
255 unit_err(mountpoint_namecheck(buf, &why), -1);
256 unit_eq(why, NAME_ERR_TOOLONG);
257
258 return (MUNIT_OK);
259 }
260
261 /* get_dataset_depth: a path's level of nesting (depth). */
262 static MunitResult
test_dataset_depth(const MunitParameter params[],void * data)263 test_dataset_depth(const MunitParameter params[], void *data)
264 {
265 (void) params, (void) data;
266
267 /* Depth is the number of '/' separators in the path. */
268 unit_eq(get_dataset_depth("tank"), 0);
269 unit_eq(get_dataset_depth("tank/home"), 1);
270 unit_eq(get_dataset_depth("tank/home/user"), 2);
271
272 /* Counting stops at the snapshot or bookmark delimiter. */
273 unit_eq(get_dataset_depth("tank/home@snap"), 1);
274 unit_eq(get_dataset_depth("tank/home#bm"), 1);
275
276 /*
277 * dataset_nestcheck() passes while the depth is under the limit and
278 * fails once it reaches it. zfs_max_dataset_nesting is a tunable that
279 * can be adjusted to the desired nesting.
280 */
281 zfs_max_dataset_nesting = 2;
282 unit_ok(dataset_nestcheck("a/b")); /* depth 1, under 2 */
283 unit_err(dataset_nestcheck("a/b/c"), -1); /* depth 2, at 2 */
284
285 return (MUNIT_OK);
286 }
287
288 /* ========== */
289
290 static const MunitTest namecheck_tests[] = {
291 UNIT_TEST("pool", test_pool_namecheck),
292 UNIT_TEST("dataset", test_dataset_namecheck),
293 UNIT_TEST("snapshot", test_snapshot_namecheck),
294 UNIT_TEST("bookmark", test_bookmark_namecheck),
295 UNIT_TEST("component", test_component_namecheck),
296 UNIT_TEST("permset", test_permset_namecheck),
297 UNIT_TEST("mountpoint", test_mountpoint_namecheck),
298 UNIT_TEST("depth", test_dataset_depth),
299 { 0 },
300 };
301
302 static const MunitSuite namecheck_test_suite = {
303 "namecheck.",
304 namecheck_tests,
305 NULL,
306 1,
307 MUNIT_SUITE_OPTION_NONE,
308 };
309
310 int
main(int argc,char ** argv)311 main(int argc, char **argv)
312 {
313 return (munit_suite_main(&namecheck_test_suite, NULL, argc, argv));
314 }
315