Add resident.conf(5) and varsym.conf(5) manual pages.
[dragonfly/vkernel-mp.git] / usr.bin / catman / catman.c
blob7ffccdccf0f083c13be806815b489f84be8dae15
1 /*-
2 * Copyright (c) 2002 John Rochester
3 * All rights reserved.
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer,
10 * in this position and unchanged.
11 * 2. Redistributions in binary form must reproduce the above copyright
12 * notice, this list of conditions and the following disclaimer in the
13 * documentation and/or other materials provided with the distribution.
14 * 3. The name of the author may not be used to endorse or promote products
15 * derived from this software without specific prior written permission
17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
18 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
21 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 * $FreeBSD: src/usr.bin/catman/catman.c,v 1.9 2003/06/10 02:18:00 ache Exp $
29 * $DragonFly: src/usr.bin/catman/catman.c,v 1.2 2004/05/17 19:44:26 cpressey Exp $
32 #include <sys/types.h>
33 #include <sys/stat.h>
34 #include <sys/param.h>
36 #include <ctype.h>
37 #include <dirent.h>
38 #include <err.h>
39 #include <fcntl.h>
40 #include <locale.h>
41 #include <langinfo.h>
42 #include <stdio.h>
43 #include <stdlib.h>
44 #include <string.h>
45 #include <unistd.h>
47 #define DEFAULT_MANPATH "/usr/share/man"
49 #define TOP_LEVEL_DIR 0 /* signifies a top-level man directory */
50 #define MAN_SECTION_DIR 1 /* signifies a man section directory */
51 #define UNKNOWN 2 /* signifies an unclassifiable directory */
53 #define TEST_EXISTS 0x01
54 #define TEST_DIR 0x02
55 #define TEST_FILE 0x04
56 #define TEST_READABLE 0x08
57 #define TEST_WRITABLE 0x10
58 #define TEST_EXECUTABLE 0x20
60 static int verbose; /* -v flag: be verbose with warnings */
61 static int pretend; /* -n, -p flags: print out what would be done
62 instead of actually doing it */
63 static int force; /* -f flag: force overwriting all cat pages */
64 static int rm_junk; /* -r flag: remove garbage pages */
65 static char *locale; /* user's locale if -L is used */
66 static char *lang_locale; /* short form of locale */
67 static int exit_code; /* exit code to use when finished */
70 * -T argument for nroff
72 static const char *nroff_device = "ascii";
75 * Mapping from locale to nroff device
77 static const char *locale_device[] = {
78 "KOI8-R", "koi8-r",
79 "ISO8859-1", "latin1",
80 "ISO8859-15", "latin1",
81 NULL
84 #define BZ2_CMD "bzip2"
85 #define BZ2_EXT ".bz2"
86 #define BZ2CAT_CMD "bz"
87 #define GZ_CMD "gzip"
88 #define GZ_EXT ".gz"
89 #define GZCAT_CMD "z"
90 enum Ziptype {NONE, BZIP, GZIP};
92 static uid_t uid;
93 static gid_t gids[NGROUPS_MAX];
94 static int ngids;
95 static int starting_dir;
96 static char tmp_file[MAXPATHLEN];
97 struct stat test_st;
100 * A hashtable is an array of chains composed of this entry structure.
102 struct hash_entry {
103 ino_t inode_number;
104 dev_t device_number;
105 const char *data;
106 struct hash_entry *next;
109 #define HASHTABLE_ALLOC 16384 /* allocation for hashtable (power of 2) */
110 #define HASH_MASK (HASHTABLE_ALLOC - 1)
112 static struct hash_entry *visited[HASHTABLE_ALLOC];
113 static struct hash_entry *links[HASHTABLE_ALLOC];
116 * Inserts a string into a hashtable keyed by inode & device number.
118 static void
119 insert_hashtable(struct hash_entry **table, ino_t inode_number,
120 dev_t device_number, const char *data)
122 struct hash_entry *new_entry;
123 struct hash_entry **chain;
125 new_entry = malloc(sizeof(struct hash_entry));
126 if (new_entry == NULL)
127 err(1, "can't insert into hashtable");
128 chain = &table[inode_number & HASH_MASK];
129 new_entry->inode_number = inode_number;
130 new_entry->device_number = device_number;
131 new_entry->data = data;
132 new_entry->next = *chain;
133 *chain = new_entry;
137 * Finds a string in a hashtable keyed by inode & device number.
139 static const char *
140 find_hashtable(struct hash_entry **table, ino_t inode_number,
141 dev_t device_number)
143 struct hash_entry *chain;
145 chain = table[inode_number & HASH_MASK];
146 while (chain != NULL) {
147 if (chain->inode_number == inode_number &&
148 chain->device_number == device_number)
149 return(chain->data);
150 chain = chain->next;
152 return(NULL);
155 static void
156 trap_signal(int sig __unused)
158 if (tmp_file[0] != '\0')
159 unlink(tmp_file);
160 exit(1);
164 * Deals with junk files in the man or cat section directories.
166 static void
167 junk(const char *mandir, const char *name, const char *reason)
169 if (verbose)
170 fprintf(stderr, "%s/%s: %s\n", mandir, name, reason);
171 if (rm_junk) {
172 fprintf(stderr, "rm %s/%s\n", mandir, name);
173 if (!pretend && unlink(name) < 0)
174 warn("%s/%s", mandir, name);
179 * Returns TOP_LEVEL_DIR for .../man, MAN_SECTION_DIR for .../manXXX,
180 * and UNKNOWN for everything else.
182 static int
183 directory_type(char *dir)
185 char *p;
187 for (;;) {
188 p = strrchr(dir, '/');
189 if (p == NULL || p[1] != '\0')
190 break;
191 *p = '\0';
193 if (p == NULL)
194 p = dir;
195 else
196 p++;
197 if (strncmp(p, "man", 3) == 0) {
198 p += 3;
199 if (*p == '\0')
200 return TOP_LEVEL_DIR;
201 while (isalnum((unsigned char)*p) || *p == '_') {
202 if (*++p == '\0')
203 return(MAN_SECTION_DIR);
206 return(UNKNOWN);
210 * Tests whether the given file name (without a preceding path)
211 * is a proper man page name (like "mk-amd-map.8.gz").
212 * Only alphanumerics and '_' are allowed after the last '.' and
213 * the last '.' can't be the first or last characters.
215 static int
216 is_manpage_name(char *name)
218 char *lastdot = NULL;
219 char *n;
221 for (n = name; *n != '\0'; n++) {
222 if (isalnum(*n))
223 continue;
224 switch (*n) {
225 case '_':
226 break;
227 case '-':
228 case '+':
229 case '[':
230 case ':':
231 lastdot = NULL;
232 break;
233 case '.':
234 lastdot = n;
235 break;
236 default:
237 return(0);
240 return(lastdot > name && lastdot + 1 < n);
243 static int
244 is_bzipped(char *name)
246 int len = strlen(name);
247 return(len >= 5 && strcmp(&name[len - 4], BZ2_EXT) == 0);
250 static int
251 is_gzipped(char *name)
253 int len = strlen(name);
254 return(len >= 4 && strcmp(&name[len - 3], GZ_EXT) == 0);
258 * Converts manXXX to catXXX.
260 static char *
261 get_cat_section(char *section)
263 char *cat_section;
265 cat_section = strdup(section);
266 strncpy(cat_section, "cat", 3);
267 return(cat_section);
271 * Converts .../man/manXXX to .../man.
273 static char *
274 get_mandir(char *section)
276 char *slash;
277 char *mandir;
279 slash = strrchr(section, '/');
280 mandir = malloc(slash - section + 1);
281 strncpy(mandir, section, slash - section);
282 mandir[slash - section] = '\0';
283 return(mandir);
287 * Tests to see if the given directory has already been visited.
289 static int
290 already_visited(char *mandir, char *dir, int count_visit)
292 struct stat st;
294 if (stat(dir, &st) < 0) {
295 if (mandir != NULL)
296 warn("%s/%s", mandir, dir);
297 else
298 warn("%s", dir);
299 exit_code = 1;
300 return(1);
302 if (find_hashtable(visited, st.st_ino, st.st_dev) != NULL) {
303 if (mandir != NULL)
304 warnx("already visited %s/%s", mandir, dir);
305 else
306 warnx("already visited %s", dir);
307 return(1);
309 if (count_visit)
310 insert_hashtable(visited, st.st_ino, st.st_dev, "");
311 return(0);
315 * Returns a set of TEST_* bits describing a file's type and permissions.
316 * If mod_time isn't NULL, it will contain the file's modification time.
318 static int
319 test_path(char *name, time_t *mod_time)
321 int result;
323 if (stat(name, &test_st) < 0)
324 return(0);
325 result = TEST_EXISTS;
326 if (mod_time != NULL)
327 *mod_time = test_st.st_mtime;
328 if (S_ISDIR(test_st.st_mode))
329 result |= TEST_DIR;
330 else if (S_ISREG(test_st.st_mode))
331 result |= TEST_FILE;
332 if (test_st.st_uid == uid) {
333 test_st.st_mode >>= 6;
334 } else {
335 int i;
336 for (i = 0; i < ngids; i++) {
337 if (test_st.st_gid == gids[i]) {
338 test_st.st_mode >>= 3;
339 break;
343 if (test_st.st_mode & S_IROTH)
344 result |= TEST_READABLE;
345 if (test_st.st_mode & S_IWOTH)
346 result |= TEST_WRITABLE;
347 if (test_st.st_mode & S_IXOTH)
348 result |= TEST_EXECUTABLE;
349 return(result);
353 * Checks whether a file is a symbolic link.
355 static int
356 is_symlink(char *path)
358 struct stat st;
360 return(lstat(path, &st) >= 0 && S_ISLNK(st.st_mode));
364 * Tests to see if the given directory can be written to.
366 static void
367 check_writable(char *mandir)
369 if (verbose && !(test_path(mandir, NULL) & TEST_WRITABLE))
370 fprintf(stderr, "%s: not writable - will only be able to write "
371 "to existing cat directories\n", mandir);
375 * If the directory exists, attempt to make it writable, otherwise
376 * attempt to create it.
378 static int
379 make_writable_dir(char *mandir, char *dir)
381 int test;
383 if ((test = test_path(dir, NULL)) != 0) {
384 if (!(test & TEST_WRITABLE) && chmod(dir, 0755) < 0) {
385 warn("%s/%s: chmod", mandir, dir);
386 exit_code = 1;
387 return(0);
389 } else {
390 if (verbose || pretend)
391 fprintf(stderr, "mkdir %s\n", dir);
392 if (!pretend) {
393 unlink(dir);
394 if (mkdir(dir, 0755) < 0) {
395 warn("%s/%s: mkdir", mandir, dir);
396 exit_code = 1;
397 return(0);
401 return(1);
405 * Processes a single man page source by using nroff to create
406 * the preformatted cat page.
408 static void
409 process_page(char *mandir, char *src, char *cat, enum Ziptype zipped)
411 int src_test, cat_test;
412 time_t src_mtime, cat_mtime;
413 char cmd[MAXPATHLEN];
414 dev_t src_dev;
415 ino_t src_ino;
416 const char *link_name;
418 src_test = test_path(src, &src_mtime);
419 if (!(src_test & (TEST_FILE|TEST_READABLE))) {
420 if (!(src_test & TEST_DIR)) {
421 warnx("%s/%s: unreadable", mandir, src);
422 exit_code = 1;
423 if (rm_junk && is_symlink(src))
424 junk(mandir, src, "bogus symlink");
426 return;
428 src_dev = test_st.st_dev;
429 src_ino = test_st.st_ino;
430 cat_test = test_path(cat, &cat_mtime);
431 if (cat_test & (TEST_FILE|TEST_READABLE)) {
432 if (!force && cat_mtime >= src_mtime) {
433 if (verbose)
434 fprintf(stderr, "\t%s/%s: up to date\n",
435 mandir, src);
436 return;
440 * Is the man page a link to one we've already processed?
442 if ((link_name = find_hashtable(links, src_ino, src_dev)) != NULL) {
443 if (verbose || pretend)
444 fprintf(stderr, "%slink %s -> %s\n",
445 verbose ? "\t" : "", cat, link_name);
446 if (!pretend)
447 link(link_name, cat);
448 return;
450 insert_hashtable(links, src_ino, src_dev, strdup(cat));
451 if (verbose || pretend) {
452 fprintf(stderr, "%sformat %s -> %s\n",
453 verbose ? "\t" : "", src, cat);
454 if (pretend)
455 return;
457 snprintf(tmp_file, sizeof tmp_file, "%s.tmp", cat);
458 snprintf(cmd, sizeof cmd,
459 "%scat %s | tbl | nroff -T%s -man | col | %s > %s.tmp",
460 zipped == BZIP ? BZ2CAT_CMD : zipped == GZIP ? GZCAT_CMD : "",
461 src, nroff_device,
462 zipped == BZIP ? BZ2_CMD : zipped == GZIP ? GZ_CMD : "cat",
463 cat);
464 if (system(cmd) != 0)
465 err(1, "formatting pipeline");
466 if (rename(tmp_file, cat) < 0)
467 warn("%s", cat);
468 tmp_file[0] = '\0';
472 * Scan the man section directory for pages and process each one,
473 * then check for junk in the corresponding cat section.
475 static void
476 scan_section(char *mandir, char *section, char *cat_section)
478 struct dirent **entries;
479 char **expected = NULL;
480 int npages;
481 int nexpected = 0;
482 int i, e;
483 enum Ziptype zipped;
484 char *page_name;
485 char page_path[MAXPATHLEN];
486 char cat_path[MAXPATHLEN];
487 char zip_path[MAXPATHLEN];
490 * scan the man section directory for pages
492 npages = scandir(section, &entries, NULL, alphasort);
493 if (npages < 0) {
494 warn("%s/%s", mandir, section);
495 exit_code = 1;
496 return;
498 if (verbose || rm_junk) {
500 * Maintain a list of all cat pages that should exist,
501 * corresponding to existing man pages.
503 expected = (char **) calloc(npages, sizeof(char *));
505 for (i = 0; i < npages; free(entries[i++])) {
506 page_name = entries[i]->d_name;
507 snprintf(page_path, sizeof page_path, "%s/%s", section,
508 page_name);
509 if (!is_manpage_name(page_name)) {
510 if (!(test_path(page_path, NULL) & TEST_DIR)) {
511 junk(mandir, page_path,
512 "invalid man page name");
514 continue;
516 zipped = is_bzipped(page_name) ? BZIP :
517 is_gzipped(page_name) ? GZIP : NONE;
518 if (zipped != NONE) {
519 snprintf(cat_path, sizeof cat_path, "%s/%s",
520 cat_section, page_name);
521 if (expected != NULL)
522 expected[nexpected++] = strdup(page_name);
523 process_page(mandir, page_path, cat_path, zipped);
524 } else {
526 * We've got an uncompressed man page,
527 * check to see if there's a (preferred)
528 * compressed one.
530 snprintf(zip_path, sizeof zip_path, "%s%s",
531 page_path, GZ_EXT);
532 if (test_path(zip_path, NULL) != 0) {
533 junk(mandir, page_path,
534 "man page unused due to existing " GZ_EXT);
535 } else {
536 if (verbose) {
537 fprintf(stderr,
538 "warning, %s is uncompressed\n",
539 page_path);
541 snprintf(cat_path, sizeof cat_path, "%s/%s",
542 cat_section, page_name);
543 if (expected != NULL) {
544 asprintf(&expected[nexpected++],
545 "%s", page_name);
547 process_page(mandir, page_path, cat_path, NONE);
551 free(entries);
552 if (expected == NULL)
553 return;
555 * scan cat sections for junk
557 npages = scandir(cat_section, &entries, NULL, alphasort);
558 e = 0;
559 for (i = 0; i < npages; free(entries[i++])) {
560 const char *junk_reason;
561 int cmp = 1;
563 page_name = entries[i]->d_name;
564 if (strcmp(page_name, ".") == 0 || strcmp(page_name, "..") == 0)
565 continue;
567 * Keep the index into the expected cat page list
568 * ahead of the name we've found.
570 while (e < nexpected &&
571 (cmp = strcmp(page_name, expected[e])) > 0)
572 free(expected[e++]);
573 if (cmp == 0)
574 continue;
575 /* we have an unexpected page */
576 if (!is_manpage_name(page_name)) {
577 junk_reason = "invalid cat page name";
578 } else if (!is_gzipped(page_name) && e + 1 < nexpected &&
579 strncmp(page_name, expected[e + 1], strlen(page_name)) == 0 &&
580 strlen(expected[e + 1]) == strlen(page_name) + 3) {
581 junk_reason = "cat page unused due to existing " GZ_EXT;
582 } else
583 junk_reason = "cat page without man page";
584 snprintf(cat_path, sizeof cat_path, "%s/%s", cat_section,
585 page_name);
586 junk(mandir, cat_path, junk_reason);
588 free(entries);
589 while (e < nexpected)
590 free(expected[e++]);
591 free(expected);
596 * Processes a single man section.
598 static void
599 process_section(char *mandir, char *section)
601 char *cat_section;
603 if (already_visited(mandir, section, 1))
604 return;
605 if (verbose)
606 fprintf(stderr, " section %s\n", section);
607 cat_section = get_cat_section(section);
608 if (make_writable_dir(mandir, cat_section))
609 scan_section(mandir, section, cat_section);
612 static int
613 select_sections(struct dirent *entry)
615 return(directory_type(entry->d_name) == MAN_SECTION_DIR);
619 * Processes a single top-level man directory. If section isn't NULL,
620 * it will only process that section sub-directory, otherwise it will
621 * process all of them.
623 static void
624 process_mandir(char *dir_name, char *section)
626 fchdir(starting_dir);
627 if (already_visited(NULL, dir_name, section == NULL))
628 return;
629 check_writable(dir_name);
630 if (verbose)
631 fprintf(stderr, "man directory %s\n", dir_name);
632 if (pretend)
633 fprintf(stderr, "cd %s\n", dir_name);
634 if (chdir(dir_name) < 0) {
635 warn("%s: chdir", dir_name);
636 exit_code = 1;
637 return;
639 if (section != NULL) {
640 process_section(dir_name, section);
641 } else {
642 struct dirent **entries;
643 int nsections;
644 int i;
646 nsections = scandir(".", &entries, select_sections, alphasort);
647 if (nsections < 0) {
648 warn("%s", dir_name);
649 exit_code = 1;
650 return;
652 for (i = 0; i < nsections; i++) {
653 process_section(dir_name, entries[i]->d_name);
654 free(entries[i]);
656 free(entries);
661 * Processes one argument, which may be a colon-separated list of
662 * directories.
664 static void
665 process_argument(const char *arg)
667 char *dir;
668 char *mandir;
669 char *parg;
671 parg = strdup(arg);
672 if (parg == NULL)
673 err(1, "out of memory");
674 while ((dir = strsep(&parg, ":")) != NULL) {
675 switch (directory_type(dir)) {
676 case TOP_LEVEL_DIR:
677 if (locale != NULL) {
678 asprintf(&mandir, "%s/%s", dir, locale);
679 process_mandir(mandir, NULL);
680 free(mandir);
681 if (lang_locale != NULL) {
682 asprintf(&mandir, "%s/%s", dir,
683 lang_locale);
684 process_mandir(mandir, NULL);
685 free(mandir);
687 } else {
688 process_mandir(dir, NULL);
690 break;
691 case MAN_SECTION_DIR: {
692 mandir = get_mandir(dir);
693 process_mandir(mandir, dir);
694 break;
696 default:
697 warnx("%s: directory name not in proper man form", dir);
698 exit_code = 1;
701 free(parg);
704 static void
705 determine_locale(void)
707 char *sep;
709 if ((locale = setlocale(LC_CTYPE, "")) == NULL) {
710 warnx("-L option used, but no locale found\n");
711 return;
713 sep = strchr(locale, '_');
714 if (sep != NULL && isupper(sep[1]) && isupper(sep[2]))
715 asprintf(&lang_locale, "%.*s%s", sep - locale, locale, &sep[3]);
716 sep = nl_langinfo(CODESET);
717 if (sep != NULL && *sep != '\0' && strcmp(sep, "US-ASCII") != 0) {
718 int i;
720 for (i = 0; locale_device[i] != NULL; i += 2) {
721 if (strcmp(sep, locale_device[i]) == 0) {
722 nroff_device = locale_device[i + 1];
723 break;
727 if (verbose) {
728 if (lang_locale != NULL)
729 fprintf(stderr, "short locale is %s\n", lang_locale);
730 fprintf(stderr, "nroff device is %s\n", nroff_device);
734 static void
735 usage(void)
737 fprintf(stderr, "usage: %s [-fLnrv] [directories...]\n", getprogname());
738 exit(1);
742 main(int argc, char **argv)
744 int opt;
746 if ((uid = getuid()) == 0) {
747 fprintf(stderr, "don't run %s as root, use:\n echo", argv[0]);
748 for (optind = 0; optind < argc; optind++)
749 fprintf(stderr, " %s", argv[optind]);
750 fprintf(stderr, " | nice -5 su -m man\n");
751 exit(1);
753 while ((opt = getopt(argc, argv, "vnfLrh")) != -1) {
754 switch (opt) {
755 case 'f':
756 force++;
757 break;
758 case 'L':
759 determine_locale();
760 break;
761 case 'n':
762 pretend++;
763 break;
764 case 'r':
765 rm_junk++;
766 break;
767 case 'v':
768 verbose++;
769 break;
770 default:
771 usage();
772 /* NOTREACHED */
775 ngids = getgroups(NGROUPS_MAX, gids);
776 if ((starting_dir = open(".", 0)) < 0)
777 err(1, ".");
778 umask(022);
779 signal(SIGINT, trap_signal);
780 signal(SIGHUP, trap_signal);
781 signal(SIGQUIT, trap_signal);
782 signal(SIGTERM, trap_signal);
783 if (optind == argc) {
784 const char *manpath = getenv("MANPATH");
785 if (manpath == NULL)
786 manpath = DEFAULT_MANPATH;
787 process_argument(manpath);
788 } else {
789 while (optind < argc)
790 process_argument(argv[optind++]);
792 exit(exit_code);