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