xref: /illumos-gate/usr/src/tools/manlink/manlink.c (revision 78a75454a34d2b5e9b2c2967ecdaf9c5d3e6b030)
1 /*
2  * This file and its contents are supplied under the terms of the
3  * Common Development and Distribution License ("CDDL"), version 1.0.
4  * You may only use this file in accordance with the terms of version
5  * 1.0 of the CDDL.
6  *
7  * A full copy of the text of the CDDL should have accompanied this
8  * source.  A copy of the CDDL is also available via the Internet at
9  * http://www.illumos.org/license/CDDL.
10  */
11 
12 /*
13  * Copyright 2025 Oxide Computer Company
14  */
15 
16 /*
17  * Parse a "Manlink" file and create the symlinks described within under a
18  * specified destination directory.
19  */
20 
21 
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <unistd.h>
25 #include <fcntl.h>
26 #include <ctype.h>
27 #include <err.h>
28 #include <errno.h>
29 #include <string.h>
30 #include <stdbool.h>
31 #include <libgen.h>
32 #include <sys/debug.h>
33 #include <sys/sysmacros.h>
34 #include <sys/stat.h>
35 
36 static const char *progname = NULL;
37 
38 static void
usage(const char * fmt,...)39 usage(const char *fmt, ...)
40 {
41 	if (fmt != NULL) {
42 		va_list ap;
43 
44 		va_start(ap, fmt);
45 		vwarnx(fmt, ap);
46 		va_end(ap);
47 	}
48 
49 	(void) fprintf(stderr,
50 	    "Usage: %s [opts] -d <destdir> <input(s)>\n\n"
51 	    "Options:\n"
52 	    "\t-n\tdry run\n",
53 	    progname);
54 }
55 
56 typedef struct manlink_iter {
57 	FILE	*mi_fp;
58 	size_t	mi_cap;
59 	char	*mi_line;
60 	char	*mi_tok_saveptr;
61 	char	*mi_target;
62 } manlink_iter_t;
63 
64 typedef struct manlink_iter_result {
65 	const char	*mir_name;
66 	const char	*mir_target;
67 } manlink_iter_res_t;
68 
69 static bool
valid_name(const char * name)70 valid_name(const char *name)
71 {
72 	for (char c; (c = *name) != '\0'; name++) {
73 		if (c == '/') {
74 			/* Link names expected to be in base directory */
75 			return (false);
76 		}
77 		if (c == '#') {
78 			/* Should not contain comment character */
79 			return (false);
80 		}
81 		if (!isalnum(c) && !ispunct(c)) {
82 			/* Expect "normal" man page names */
83 			return (false);
84 		}
85 	}
86 	return (true);
87 }
88 
89 static bool
valid_target(const char * target)90 valid_target(const char *target)
91 {
92 	for (char c; (c = *target) != '\0'; target++) {
93 		if (isalnum(c)) {
94 			continue;
95 		}
96 		switch (c) {
97 		case '.':
98 		case '_':
99 		case '-':
100 		case '/':
101 			break;
102 		default:
103 			return (false);
104 		}
105 	}
106 	return (true);
107 }
108 
109 static void
link_iter_init(FILE * ifp,manlink_iter_t * itr)110 link_iter_init(FILE *ifp, manlink_iter_t *itr)
111 {
112 	itr->mi_fp = ifp;
113 	itr->mi_cap = 0;
114 	itr->mi_line = NULL;
115 	itr->mi_target = NULL;
116 }
117 
118 static void
link_iter_fini(manlink_iter_t * itr)119 link_iter_fini(manlink_iter_t *itr)
120 {
121 	(void) fclose(itr->mi_fp);
122 	free(itr->mi_line);
123 	free(itr->mi_target);
124 }
125 
126 static bool
link_iter_next(manlink_iter_t * itr,const char ** namep,const char ** targetp)127 link_iter_next(manlink_iter_t *itr, const char **namep, const char **targetp)
128 {
129 	ssize_t len;
130 
131 	while ((len = getline(&itr->mi_line, &itr->mi_cap, itr->mi_fp)) >= 1) {
132 		char *line = itr->mi_line;
133 
134 		/* Nuke the trailing newline (if any) */
135 		if (line[len - 1] == '\n') {
136 			line[len - 1] = '\0';
137 		}
138 
139 		if (*line == '\0' || *line == '#') {
140 			/* Skip empty lines and comments */
141 			continue;
142 		} else if (*line == '\t') {
143 			const char *name = line + 1;
144 
145 			if (!valid_name(name)) {
146 				err(EXIT_FAILURE,
147 				    "Invalid link name: \"%s\"", name);
148 			} else if (itr->mi_target == NULL) {
149 				err(EXIT_FAILURE,
150 				    "Link without preceding target");
151 			} else {
152 				*namep = name;
153 				*targetp = itr->mi_target;
154 				return (true);
155 			}
156 		} else {
157 			if (!valid_target(line)) {
158 				errx(EXIT_FAILURE,
159 				    "Invalid link target \"%s\"", line);
160 			} else {
161 				free(itr->mi_target);
162 				itr->mi_target = strdup(line);
163 				continue;
164 			}
165 		}
166 	}
167 
168 	return (false);
169 }
170 
171 static void
do_links(const char * dest_dir,const char * input_file,bool dry_run)172 do_links(const char *dest_dir, const char *input_file, bool dry_run)
173 {
174 	int dfd = open(dest_dir, O_DIRECTORY | O_RDONLY, 0);
175 	if (dfd < 0) {
176 		err(EXIT_FAILURE, "Could not open destination dir %s",
177 		    dest_dir);
178 	}
179 
180 	FILE *ifp = fopen(input_file, "r");
181 	if (ifp == NULL) {
182 		err(EXIT_FAILURE, "Could not open input file %s", input_file);
183 	}
184 
185 	manlink_iter_t iter;
186 	link_iter_init(ifp, &iter);
187 
188 	const char *name, *target;
189 	while (link_iter_next(&iter, &name, &target)) {
190 		struct stat st;
191 
192 		const int res = fstatat(dfd, name, &st, AT_SYMLINK_NOFOLLOW);
193 		if (res == 0) {
194 			if (S_ISLNK(st.st_mode)) {
195 				char buf[MAXPATHLEN];
196 
197 				buf[0] = '\0';
198 				ssize_t len = readlinkat(dfd, name, buf,
199 				    sizeof (buf));
200 				if (len > 0) {
201 					/* NUL terminate */
202 					buf[MIN(len, sizeof (buf) - 1)] = '\0';
203 				}
204 
205 				if (strncmp(buf, target, sizeof (buf)) == 0) {
206 					continue;
207 				}
208 			}
209 			(void) printf("unlink %s/%s\n", dest_dir, name);
210 			if (!dry_run && unlinkat(dfd, name, 0) != 0) {
211 				err(EXIT_FAILURE,
212 				    "Could not unlink conflicting file %s/%s",
213 				    dest_dir, name);
214 			}
215 		} else if (errno != ENOENT) {
216 			err(EXIT_FAILURE, "stat() failure for link %s/%s",
217 			    dest_dir, name);
218 		}
219 
220 		(void) printf("link %s/%s -> %s\n", dest_dir, name, target);
221 		if (!dry_run && symlinkat(target, dfd, name) != 0) {
222 			err(EXIT_FAILURE, "failure to create link at %s/%s",
223 			    dest_dir, name);
224 		}
225 	}
226 	link_iter_fini(&iter);
227 }
228 
229 int
main(int argc,char * argv[])230 main(int argc, char *argv[])
231 {
232 	char *dest_dir = NULL;
233 	bool do_dry_run = false;
234 	progname = basename(argv[0]);
235 
236 	int c;
237 	while ((c = getopt(argc, argv, "nd:")) != -1) {
238 		switch (c) {
239 		case 'd':
240 			dest_dir = optarg;
241 			break;
242 		case 'n':
243 			do_dry_run = true;
244 			break;
245 		case '?':
246 			usage("unknown option: -%c", optopt);
247 			exit(EXIT_FAILURE);
248 		}
249 	}
250 	argc -= optind;
251 	argv += optind;
252 
253 	if (argc < 1) {
254 		usage("input file(s)");
255 		exit(EXIT_FAILURE);
256 	}
257 	for (uint_t i = 0; i < (uint_t)argc; i++) {
258 		do_links(dest_dir, argv[i], do_dry_run);
259 	}
260 
261 	return (EXIT_SUCCESS);
262 }
263