enable WARNS_WERROR by default
[dragonfly.git] / usr.bin / catman / catman.c
blob1a1dd26fe6abd5eb79b583bd0563b689c2aa684d
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.4 2008/11/10 16:10:34 swildner 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 <libgen.h>
43 #include <stdio.h>
44 #include <stdlib.h>
45 #include <string.h>
46 #include <unistd.h>
48 #define DEFAULT_MANPATH "/usr/share/man"
50 #define TOP_LEVEL_DIR 0 /* signifies a top-level man directory */
51 #define MAN_SECTION_DIR 1 /* signifies a man section directory */
52 #define UNKNOWN 2 /* signifies an unclassifiable directory */
54 #define TEST_EXISTS 0x01
55 #define TEST_DIR 0x02
56 #define TEST_FILE 0x04
57 #define TEST_READABLE 0x08
58 #define TEST_WRITABLE 0x10
59 #define TEST_EXECUTABLE 0x20
61 static int verbose; /* -v flag: be verbose with warnings */
62 static int pretend; /* -n, -p flags: print out what would be done
63 instead of actually doing it */
64 static int force; /* -f flag: force overwriting all cat pages */
65 static int rm_junk; /* -r flag: remove garbage pages */
66 static char *locale; /* user's locale if -L is used */
67 static char *lang_locale; /* short form of locale */
68 static const char *machine;
69 static int exit_code; /* exit code to use when finished */
72 * -T argument for nroff
74 static const char *nroff_device = "ascii";
77 * Mapping from locale to nroff device
79 static const char *locale_device[] = {
80 "KOI8-R", "koi8-r",
81 "ISO8859-1", "latin1",
82 "ISO8859-15", "latin1",
83 NULL
86 #define BZ2_CMD "bzip2"
87 #define BZ2_EXT ".bz2"
88 #define BZ2CAT_CMD "bz"
89 #define GZ_CMD "gzip"
90 #define GZ_EXT ".gz"
91 #define GZCAT_CMD "z"
92 enum Ziptype {NONE, BZIP, GZIP};
94 static uid_t uid;
95 static gid_t gids[NGROUPS_MAX];
96 static int ngids;
97 static int starting_dir;
98 static char tmp_file[MAXPATHLEN];
99 struct stat test_st;
102 * A hashtable is an array of chains composed of this entry structure.
104 struct hash_entry {
105 ino_t inode_number;
106 dev_t device_number;
107 const char *data;
108 struct hash_entry *next;
111 #define HASHTABLE_ALLOC 16384 /* allocation for hashtable (power of 2) */
112 #define HASH_MASK (HASHTABLE_ALLOC - 1)
114 static struct hash_entry *visited[HASHTABLE_ALLOC];
115 static struct hash_entry *links[HASHTABLE_ALLOC];
118 * Inserts a string into a hashtable keyed by inode & device number.
120 static void
121 insert_hashtable(struct hash_entry **table, ino_t inode_number,
122 dev_t device_number, const char *data)
124 struct hash_entry *new_entry;
125 struct hash_entry **chain;
127 new_entry = malloc(sizeof(struct hash_entry));
128 if (new_entry == NULL)
129 err(1, "can't insert into hashtable");
130 chain = &table[inode_number & HASH_MASK];
131 new_entry->inode_number = inode_number;
132 new_entry->device_number = device_number;
133 new_entry->data = data;
134 new_entry->next = *chain;
135 *chain = new_entry;
139 * Finds a string in a hashtable keyed by inode & device number.
141 static const char *
142 find_hashtable(struct hash_entry **table, ino_t inode_number,
143 dev_t device_number)
145 struct hash_entry *chain;
147 chain = table[inode_number & HASH_MASK];
148 while (chain != NULL) {
149 if (chain->inode_number == inode_number &&
150 chain->device_number == device_number)
151 return(chain->data);
152 chain = chain->next;
154 return(NULL);
157 static void
158 trap_signal(int sig __unused)
160 if (tmp_file[0] != '\0')
161 unlink(tmp_file);
162 exit(1);
166 * Deals with junk files in the man or cat section directories.
168 static void
169 junk(const char *mandir, const char *name, const char *reason)
171 if (verbose)
172 fprintf(stderr, "%s/%s: %s\n", mandir, name, reason);
173 if (rm_junk) {
174 fprintf(stderr, "rm %s/%s\n", mandir, name);
175 if (!pretend && unlink(name) < 0)
176 warn("%s/%s", mandir, name);
181 * Returns TOP_LEVEL_DIR for .../man, MAN_SECTION_DIR for .../manXXX,
182 * and UNKNOWN for everything else.
184 static int
185 directory_type(char *dir)
187 char *p;
189 for (;;) {
190 p = strrchr(dir, '/');
191 if (p == NULL || p[1] != '\0')
192 break;
193 *p = '\0';
195 if (p == NULL)
196 p = dir;
197 else
198 p++;
199 if (strncmp(p, "man", 3) == 0) {
200 p += 3;
201 if (*p == '\0')
202 return TOP_LEVEL_DIR;
203 while (isalnum((unsigned char)*p) || *p == '_') {
204 if (*++p == '\0')
205 return(MAN_SECTION_DIR);
208 return(UNKNOWN);
212 * Tests whether the given file name (without a preceding path)
213 * is a proper man page name (like "mk-amd-map.8.gz").
214 * Only alphanumerics and '_' are allowed after the last '.' and
215 * the last '.' can't be the first or last characters.
217 static int
218 is_manpage_name(char *name)
220 char *lastdot = NULL;
221 char *n;
223 for (n = name; *n != '\0'; n++) {
224 if (isalnum(*n))
225 continue;
226 switch (*n) {
227 case '_':
228 break;
229 case '-':
230 case '+':
231 case '[':
232 case ':':
233 lastdot = NULL;
234 break;
235 case '.':
236 lastdot = n;
237 break;
238 default:
239 return(0);
242 return(lastdot > name && lastdot + 1 < n);
245 static int
246 is_bzipped(char *name)
248 int len = strlen(name);
249 return(len >= 5 && strcmp(&name[len - 4], BZ2_EXT) == 0);
252 static int
253 is_gzipped(char *name)
255 int len = strlen(name);
256 return(len >= 4 && strcmp(&name[len - 3], GZ_EXT) == 0);
260 * Converts manXXX to catXXX.
262 static char *
263 get_cat_section(char *section)
265 char *cat_section;
267 cat_section = strdup(section);
268 strncpy(cat_section, "cat", 3);
269 return(cat_section);
273 * Tests to see if the given directory has already been visited.
275 static int
276 already_visited(char *mandir, char *dir, int count_visit)
278 struct stat st;
280 if (stat(dir, &st) < 0) {
281 if (mandir != NULL)
282 warn("%s/%s", mandir, dir);
283 else
284 warn("%s", dir);
285 exit_code = 1;
286 return(1);
288 if (find_hashtable(visited, st.st_ino, st.st_dev) != NULL) {
289 if (mandir != NULL)
290 warnx("already visited %s/%s", mandir, dir);
291 else
292 warnx("already visited %s", dir);
293 return(1);
295 if (count_visit)
296 insert_hashtable(visited, st.st_ino, st.st_dev, "");
297 return(0);
301 * Returns a set of TEST_* bits describing a file's type and permissions.
302 * If mod_time isn't NULL, it will contain the file's modification time.
304 static int
305 test_path(char *name, time_t *mod_time)
307 int result;
309 if (stat(name, &test_st) < 0)
310 return(0);
311 result = TEST_EXISTS;
312 if (mod_time != NULL)
313 *mod_time = test_st.st_mtime;
314 if (S_ISDIR(test_st.st_mode))
315 result |= TEST_DIR;
316 else if (S_ISREG(test_st.st_mode))
317 result |= TEST_FILE;
318 if (test_st.st_uid == uid) {
319 test_st.st_mode >>= 6;
320 } else {
321 int i;
322 for (i = 0; i < ngids; i++) {
323 if (test_st.st_gid == gids[i]) {
324 test_st.st_mode >>= 3;
325 break;
329 if (test_st.st_mode & S_IROTH)
330 result |= TEST_READABLE;
331 if (test_st.st_mode & S_IWOTH)
332 result |= TEST_WRITABLE;
333 if (test_st.st_mode & S_IXOTH)
334 result |= TEST_EXECUTABLE;
335 return(result);
339 * Checks whether a file is a symbolic link.
341 static int
342 is_symlink(char *path)
344 struct stat st;
346 return(lstat(path, &st) >= 0 && S_ISLNK(st.st_mode));
350 * Tests to see if the given directory can be written to.
352 static void
353 check_writable(char *mandir)
355 if (verbose && !(test_path(mandir, NULL) & TEST_WRITABLE))
356 fprintf(stderr, "%s: not writable - will only be able to write "
357 "to existing cat directories\n", mandir);
361 * If the directory exists, attempt to make it writable, otherwise
362 * attempt to create it.
364 static int
365 make_writable_dir(char *mandir, char *dir)
367 int test;
369 if ((test = test_path(dir, NULL)) != 0) {
370 if (!(test & TEST_WRITABLE) && chmod(dir, 0755) < 0) {
371 warn("%s/%s: chmod", mandir, dir);
372 exit_code = 1;
373 return(0);
375 } else {
376 if (verbose || pretend)
377 fprintf(stderr, "mkdir %s\n", dir);
378 if (!pretend) {
379 unlink(dir);
380 if (mkdir(dir, 0755) < 0) {
381 warn("%s/%s: mkdir", mandir, dir);
382 exit_code = 1;
383 return(0);
387 return(1);
391 * Processes a single man page source by using nroff to create
392 * the preformatted cat page.
394 static void
395 process_page(char *mandir, char *src, char *cat, enum Ziptype zipped)
397 int src_test, cat_test;
398 time_t src_mtime, cat_mtime;
399 char cmd[MAXPATHLEN];
400 dev_t src_dev;
401 ino_t src_ino;
402 const char *link_name;
404 src_test = test_path(src, &src_mtime);
405 if (!(src_test & (TEST_FILE|TEST_READABLE))) {
406 if (!(src_test & TEST_DIR)) {
407 warnx("%s/%s: unreadable", mandir, src);
408 exit_code = 1;
409 if (rm_junk && is_symlink(src))
410 junk(mandir, src, "bogus symlink");
412 return;
414 src_dev = test_st.st_dev;
415 src_ino = test_st.st_ino;
416 cat_test = test_path(cat, &cat_mtime);
417 if (cat_test & (TEST_FILE|TEST_READABLE)) {
418 if (!force && cat_mtime >= src_mtime) {
419 if (verbose)
420 fprintf(stderr, "\t%s/%s: up to date\n",
421 mandir, src);
422 return;
426 * Is the man page a link to one we've already processed?
428 if ((link_name = find_hashtable(links, src_ino, src_dev)) != NULL) {
429 if (verbose || pretend)
430 fprintf(stderr, "%slink %s -> %s\n",
431 verbose ? "\t" : "", cat, link_name);
432 if (!pretend)
433 link(link_name, cat);
434 return;
436 insert_hashtable(links, src_ino, src_dev, strdup(cat));
437 if (verbose || pretend) {
438 fprintf(stderr, "%sformat %s -> %s\n",
439 verbose ? "\t" : "", src, cat);
440 if (pretend)
441 return;
443 snprintf(tmp_file, sizeof tmp_file, "%s.tmp", cat);
444 snprintf(cmd, sizeof cmd,
445 "%scat %s | tbl | nroff -T%s -man | col | %s > %s.tmp",
446 zipped == BZIP ? BZ2CAT_CMD : zipped == GZIP ? GZCAT_CMD : "",
447 src, nroff_device,
448 zipped == BZIP ? BZ2_CMD : zipped == GZIP ? GZ_CMD : "cat",
449 cat);
450 if (system(cmd) != 0)
451 err(1, "formatting pipeline");
452 if (rename(tmp_file, cat) < 0)
453 warn("%s", cat);
454 tmp_file[0] = '\0';
458 * Scan the man section directory for pages and process each one,
459 * then check for junk in the corresponding cat section.
461 static void
462 scan_section(char *mandir, char *section, char *cat_section)
464 struct dirent **entries;
465 char **expected = NULL;
466 int npages;
467 int nexpected = 0;
468 int i, e;
469 enum Ziptype zipped;
470 char *page_name;
471 char page_path[MAXPATHLEN];
472 char cat_path[MAXPATHLEN];
473 char zip_path[MAXPATHLEN];
476 * scan the man section directory for pages
478 npages = scandir(section, &entries, NULL, alphasort);
479 if (npages < 0) {
480 warn("%s/%s", mandir, section);
481 exit_code = 1;
482 return;
484 if (verbose || rm_junk) {
486 * Maintain a list of all cat pages that should exist,
487 * corresponding to existing man pages.
489 expected = (char **) calloc(npages, sizeof(char *));
491 for (i = 0; i < npages; free(entries[i++])) {
492 page_name = entries[i]->d_name;
493 snprintf(page_path, sizeof page_path, "%s/%s", section,
494 page_name);
495 if (!is_manpage_name(page_name)) {
496 if (!(test_path(page_path, NULL) & TEST_DIR)) {
497 junk(mandir, page_path,
498 "invalid man page name");
500 continue;
502 zipped = is_bzipped(page_name) ? BZIP :
503 is_gzipped(page_name) ? GZIP : NONE;
504 if (zipped != NONE) {
505 snprintf(cat_path, sizeof cat_path, "%s/%s",
506 cat_section, page_name);
507 if (expected != NULL)
508 expected[nexpected++] = strdup(page_name);
509 process_page(mandir, page_path, cat_path, zipped);
510 } else {
512 * We've got an uncompressed man page,
513 * check to see if there's a (preferred)
514 * compressed one.
516 snprintf(zip_path, sizeof zip_path, "%s%s",
517 page_path, GZ_EXT);
518 if (test_path(zip_path, NULL) != 0) {
519 junk(mandir, page_path,
520 "man page unused due to existing " GZ_EXT);
521 } else {
522 if (verbose) {
523 fprintf(stderr,
524 "warning, %s is uncompressed\n",
525 page_path);
527 snprintf(cat_path, sizeof cat_path, "%s/%s",
528 cat_section, page_name);
529 if (expected != NULL) {
530 asprintf(&expected[nexpected++],
531 "%s", page_name);
533 process_page(mandir, page_path, cat_path, NONE);
537 free(entries);
538 if (expected == NULL)
539 return;
541 * scan cat sections for junk
543 npages = scandir(cat_section, &entries, NULL, alphasort);
544 e = 0;
545 for (i = 0; i < npages; free(entries[i++])) {
546 const char *junk_reason;
547 int cmp = 1;
549 page_name = entries[i]->d_name;
550 if (strcmp(page_name, ".") == 0 || strcmp(page_name, "..") == 0)
551 continue;
553 * Keep the index into the expected cat page list
554 * ahead of the name we've found.
556 while (e < nexpected &&
557 (cmp = strcmp(page_name, expected[e])) > 0)
558 free(expected[e++]);
559 if (cmp == 0)
560 continue;
561 /* we have an unexpected page */
562 snprintf(cat_path, sizeof cat_path, "%s/%s", cat_section,
563 page_name);
564 if (!is_manpage_name(page_name)) {
565 if (test_path(cat_path, NULL) & TEST_DIR)
566 continue;
567 junk_reason = "invalid cat page name";
568 } else if (!is_gzipped(page_name) && e + 1 < nexpected &&
569 strncmp(page_name, expected[e + 1], strlen(page_name)) == 0 &&
570 strlen(expected[e + 1]) == strlen(page_name) + 3) {
571 junk_reason = "cat page unused due to existing " GZ_EXT;
572 } else
573 junk_reason = "cat page without man page";
574 junk(mandir, cat_path, junk_reason);
576 free(entries);
577 while (e < nexpected)
578 free(expected[e++]);
579 free(expected);
584 * Processes a single man section.
586 static void
587 process_section(char *mandir, char *section)
589 char *cat_section;
591 if (already_visited(mandir, section, 1))
592 return;
593 if (verbose)
594 fprintf(stderr, " section %s\n", section);
595 cat_section = get_cat_section(section);
596 if (make_writable_dir(mandir, cat_section))
597 scan_section(mandir, section, cat_section);
598 free(cat_section);
601 static int
602 select_sections(struct dirent *entry)
604 return(directory_type(entry->d_name) == MAN_SECTION_DIR);
608 * Processes a single top-level man directory. If section isn't NULL,
609 * it will only process that section sub-directory, otherwise it will
610 * process all of them.
612 static void
613 process_mandir(char *dir_name, char *section)
615 fchdir(starting_dir);
616 if (already_visited(NULL, dir_name, section == NULL))
617 return;
618 check_writable(dir_name);
619 if (verbose)
620 fprintf(stderr, "man directory %s\n", dir_name);
621 if (pretend)
622 fprintf(stderr, "cd %s\n", dir_name);
623 if (chdir(dir_name) < 0) {
624 warn("%s: chdir", dir_name);
625 exit_code = 1;
626 return;
628 if (section != NULL) {
629 process_section(dir_name, section);
630 } else {
631 struct dirent **entries;
632 char *machine_dir;
633 int nsections;
634 int i;
636 nsections = scandir(".", &entries, select_sections, alphasort);
637 if (nsections < 0) {
638 warn("%s", dir_name);
639 exit_code = 1;
640 return;
642 for (i = 0; i < nsections; i++) {
643 process_section(dir_name, entries[i]->d_name);
644 asprintf(&machine_dir, "%s/%s", entries[i]->d_name,
645 machine);
646 if (test_path(machine_dir, NULL) & TEST_DIR)
647 process_section(dir_name, machine_dir);
648 free(machine_dir);
649 free(entries[i]);
651 free(entries);
656 * Processes one argument, which may be a colon-separated list of
657 * directories.
659 static void
660 process_argument(const char *arg)
662 char *dir;
663 char *mandir;
664 char *section;
665 char *parg;
667 parg = strdup(arg);
668 if (parg == NULL)
669 err(1, "out of memory");
670 while ((dir = strsep(&parg, ":")) != NULL) {
671 switch (directory_type(dir)) {
672 case TOP_LEVEL_DIR:
673 if (locale != NULL) {
674 asprintf(&mandir, "%s/%s", dir, locale);
675 process_mandir(mandir, NULL);
676 free(mandir);
677 if (lang_locale != NULL) {
678 asprintf(&mandir, "%s/%s", dir,
679 lang_locale);
680 process_mandir(mandir, NULL);
681 free(mandir);
683 } else {
684 process_mandir(dir, NULL);
686 break;
687 case MAN_SECTION_DIR: {
688 mandir = strdup(dirname(dir));
689 section = strdup(basename(dir));
690 process_mandir(mandir, section);
691 free(mandir);
692 free(section);
693 break;
695 default:
696 warnx("%s: directory name not in proper man form", dir);
697 exit_code = 1;
700 free(parg);
703 static void
704 determine_locale(void)
706 char *sep;
708 if ((locale = setlocale(LC_CTYPE, "")) == NULL) {
709 warnx("-L option used, but no locale found\n");
710 return;
712 sep = strchr(locale, '_');
713 if (sep != NULL && isupper(sep[1]) && isupper(sep[2]))
714 asprintf(&lang_locale, "%.*s%s", (int)(sep - locale), locale,
715 &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",
738 getprogname());
739 exit(1);
743 main(int argc, char **argv)
745 int opt;
747 if ((uid = getuid()) == 0) {
748 fprintf(stderr, "don't run %s as root, use:\n echo", argv[0]);
749 for (optind = 0; optind < argc; optind++)
750 fprintf(stderr, " %s", argv[optind]);
751 fprintf(stderr, " | nice -5 su -m man\n");
752 exit(1);
754 while ((opt = getopt(argc, argv, "vnfLrh")) != -1) {
755 switch (opt) {
756 case 'f':
757 force++;
758 break;
759 case 'L':
760 determine_locale();
761 break;
762 case 'n':
763 pretend++;
764 break;
765 case 'r':
766 rm_junk++;
767 break;
768 case 'v':
769 verbose++;
770 break;
771 default:
772 usage();
773 /* NOTREACHED */
776 ngids = getgroups(NGROUPS_MAX, gids);
777 if ((starting_dir = open(".", 0)) < 0)
778 err(1, ".");
779 umask(022);
780 signal(SIGINT, trap_signal);
781 signal(SIGHUP, trap_signal);
782 signal(SIGQUIT, trap_signal);
783 signal(SIGTERM, trap_signal);
785 if ((machine = getenv("MACHINE")) == NULL)
786 machine = MACHINE;
788 if (optind == argc) {
789 const char *manpath = getenv("MANPATH");
790 if (manpath == NULL)
791 manpath = DEFAULT_MANPATH;
792 process_argument(manpath);
793 } else {
794 while (optind < argc)
795 process_argument(argv[optind++]);
797 exit(exit_code);