xref: /freebsd/sys/contrib/openzfs/tests/unit/test_namecheck.c (revision d0b3ecdc274930e190ea233b6b69ff03782eaf8d)
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