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 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 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 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 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 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 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 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 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