1169689Skan/*-
2169689Skan * Copyright (c) 2002 John Rochester
3169689Skan * All rights reserved.
4169689Skan *
5169689Skan * Redistribution and use in source and binary forms, with or without
6169689Skan * modification, are permitted provided that the following conditions
7169689Skan * are met:
8169689Skan * 1. Redistributions of source code must retain the above copyright
9169689Skan *    notice, this list of conditions and the following disclaimer,
10169689Skan *    in this position and unchanged.
11169689Skan * 2. Redistributions in binary form must reproduce the above copyright
12169689Skan *    notice, this list of conditions and the following disclaimer in the
13169689Skan *    documentation and/or other materials provided with the distribution.
14169689Skan * 3. The name of the author may not be used to endorse or promote products
15169689Skan *    derived from this software without specific prior written permission
16169689Skan *
17169689Skan * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
18169689Skan * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19169689Skan * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20169689Skan * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
21169689Skan * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22169689Skan * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23169689Skan * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24169689Skan * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25169689Skan * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26169689Skan * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27169689Skan */
28169689Skan
29169689Skan#include <sys/cdefs.h>
30169689Skan__FBSDID("$FreeBSD: releng/11.0/usr.bin/catman/catman.c 299589 2016-05-13 05:49:02Z truckman $");
31169689Skan
32169689Skan#include <sys/types.h>
33169689Skan#include <sys/stat.h>
34169689Skan#include <sys/param.h>
35169689Skan#include <sys/utsname.h>
36169689Skan
37169689Skan#include <assert.h>
38169689Skan#include <ctype.h>
39169689Skan#include <dirent.h>
40169689Skan#include <err.h>
41169689Skan#include <errno.h>
42169689Skan#include <fcntl.h>
43169689Skan#include <locale.h>
44169689Skan#include <langinfo.h>
45169689Skan#include <libgen.h>
46169689Skan#include <stdio.h>
47169689Skan#include <stdlib.h>
48169689Skan#include <string.h>
49169689Skan#include <unistd.h>
50169689Skan
51169689Skan#define DEFAULT_MANPATH		"/usr/share/man"
52169689Skan
53169689Skan#define TOP_LEVEL_DIR	0	/* signifies a top-level man directory */
54169689Skan#define MAN_SECTION_DIR	1	/* signifies a man section directory */
55169689Skan#define UNKNOWN		2	/* signifies an unclassifiable directory */
56169689Skan
57169689Skan#define TEST_EXISTS	0x01
58169689Skan#define TEST_DIR	0x02
59169689Skan#define TEST_FILE	0x04
60169689Skan#define TEST_READABLE	0x08
61169689Skan#define TEST_WRITABLE	0x10
62169689Skan
63169689Skanstatic int verbose;		/* -v flag: be verbose with warnings */
64169689Skanstatic int pretend;		/* -n, -p flags: print out what would be done
65169689Skan				   instead of actually doing it */
66169689Skanstatic int force;		/* -f flag: force overwriting all cat pages */
67169689Skanstatic int rm_junk;		/* -r flag: remove garbage pages */
68169689Skanstatic char *locale;		/* user's locale if -L is used */
69169689Skanstatic char *lang_locale;	/* short form of locale */
70169689Skanstatic const char *machine, *machine_arch;
71169689Skanstatic int exit_code;		/* exit code to use when finished */
72169689Skan
73169689Skan/*
74169689Skan * -T argument for nroff
75169689Skan */
76169689Skanstatic const char *nroff_device = "ascii";
77169689Skan
78169689Skan/*
79169689Skan * Mapping from locale to nroff device
80169689Skan */
81169689Skanstatic const char *locale_device[] = {
82169689Skan	"KOI8-R",	"koi8-r",
83169689Skan	"ISO8859-1",	"latin1",
84169689Skan	"ISO8859-15",	"latin1",
85169689Skan	NULL
86169689Skan};
87169689Skan
88169689Skan#define	BZ2_CMD		"bzip2"
89169689Skan#define	BZ2_EXT		".bz2"
90169689Skan#define	BZ2CAT_CMD	"bz"
91169689Skan#define	GZ_CMD		"gzip"
92169689Skan#define	GZ_EXT		".gz"
93169689Skan#define	GZCAT_CMD	"z"
94169689Skanenum Ziptype {NONE, BZIP, GZIP};
95169689Skan
96169689Skanstatic uid_t uid;
97169689Skanstatic int starting_dir;
98169689Skanstatic char tmp_file[MAXPATHLEN];
99169689Skanstatic struct stat test_st;
100169689Skan
101169689Skan/*
102169689Skan * A hashtable is an array of chains composed of this entry structure.
103169689Skan */
104169689Skanstruct hash_entry {
105169689Skan	ino_t		inode_number;
106169689Skan	dev_t		device_number;
107169689Skan	const char	*data;
108169689Skan	struct hash_entry *next;
109169689Skan};
110169689Skan
111169689Skan#define HASHTABLE_ALLOC	16384	/* allocation for hashtable (power of 2) */
112169689Skan#define HASH_MASK	(HASHTABLE_ALLOC - 1)
113169689Skan
114169689Skanstatic struct hash_entry *visited[HASHTABLE_ALLOC];
115169689Skanstatic struct hash_entry *links[HASHTABLE_ALLOC];
116169689Skan
117169689Skan/*
118169689Skan * Inserts a string into a hashtable keyed by inode & device number.
119169689Skan */
120169689Skanstatic void
121169689Skaninsert_hashtable(struct hash_entry **table,
122169689Skan    ino_t inode_number,
123169689Skan    dev_t device_number,
124169689Skan    const char *data)
125169689Skan{
126169689Skan	struct hash_entry *new_entry;
127169689Skan	struct hash_entry **chain;
128169689Skan
129169689Skan	new_entry = (struct hash_entry *) malloc(sizeof(struct hash_entry));
130169689Skan	if (new_entry == NULL)
131169689Skan		err(1, "can't insert into hashtable");
132169689Skan	chain = &table[inode_number & HASH_MASK];
133169689Skan	new_entry->inode_number = inode_number;
134169689Skan	new_entry->device_number = device_number;
135169689Skan	new_entry->data = data;
136169689Skan	new_entry->next = *chain;
137169689Skan	*chain = new_entry;
138169689Skan}
139169689Skan
140169689Skan/*
141169689Skan * Finds a string in a hashtable keyed by inode & device number.
142169689Skan */
143169689Skanstatic const char *
144169689Skanfind_hashtable(struct hash_entry **table,
145169689Skan    ino_t inode_number,
146169689Skan    dev_t device_number)
147169689Skan{
148169689Skan	struct hash_entry *chain;
149169689Skan
150169689Skan	chain = table[inode_number & HASH_MASK];
151169689Skan	while (chain != NULL) {
152169689Skan		if (chain->inode_number == inode_number &&
153169689Skan		    chain->device_number == device_number)
154169689Skan			return chain->data;
155169689Skan		chain = chain->next;
156169689Skan	}
157169689Skan	return NULL;
158169689Skan}
159169689Skan
160169689Skanstatic void
161169689Skantrap_signal(int sig __unused)
162169689Skan{
163169689Skan	if (tmp_file[0] != '\0')
164169689Skan		unlink(tmp_file);
165169689Skan	exit(1);
166169689Skan}
167169689Skan
168169689Skan/*
169169689Skan * Deals with junk files in the man or cat section directories.
170169689Skan */
171169689Skanstatic void
172169689Skanjunk(const char *mandir, const char *name, const char *reason)
173169689Skan{
174169689Skan	if (verbose)
175169689Skan		fprintf(stderr, "%s/%s: %s\n", mandir, name, reason);
176169689Skan	if (rm_junk) {
177169689Skan		fprintf(stderr, "rm %s/%s\n", mandir, name);
178169689Skan		if (!pretend && unlink(name) < 0)
179169689Skan			warn("%s/%s", mandir, name);
180169689Skan	}
181169689Skan}
182169689Skan
183169689Skan/*
184169689Skan * Returns TOP_LEVEL_DIR for .../man, MAN_SECTION_DIR for .../manXXX,
185169689Skan * and UNKNOWN for everything else.
186169689Skan */
187169689Skanstatic int
188169689Skandirectory_type(char *dir)
189169689Skan{
190169689Skan	char *p;
191169689Skan
192169689Skan	for (;;) {
193169689Skan		p = strrchr(dir, '/');
194169689Skan		if (p == NULL || p[1] != '\0')
195169689Skan			break;
196169689Skan		*p = '\0';
197169689Skan	}
198169689Skan	if (p == NULL)
199169689Skan		p = dir;
200169689Skan	else
201169689Skan		p++;
202169689Skan	if (strncmp(p, "man", 3) == 0) {
203169689Skan		p += 3;
204169689Skan		if (*p == '\0')
205169689Skan			return TOP_LEVEL_DIR;
206169689Skan		while (isalnum((unsigned char)*p) || *p == '_') {
207169689Skan			if (*++p == '\0')
208169689Skan				return MAN_SECTION_DIR;
209169689Skan		}
210169689Skan	}
211169689Skan	return UNKNOWN;
212169689Skan}
213169689Skan
214169689Skan/*
215169689Skan * Tests whether the given file name (without a preceding path)
216169689Skan * is a proper man page name (like "mk-amd-map.8.gz").
217169689Skan * Only alphanumerics and '_' are allowed after the last '.' and
218169689Skan * the last '.' can't be the first or last characters.
219169689Skan */
220169689Skanstatic int
221169689Skanis_manpage_name(char *name)
222169689Skan{
223169689Skan	char *lastdot = NULL;
224169689Skan	char *n = name;
225169689Skan
226169689Skan	while (*n != '\0') {
227169689Skan		if (!isalnum((unsigned char)*n)) {
228169689Skan			switch (*n) {
229169689Skan			case '_':
230169689Skan				break;
231169689Skan			case '-':
232169689Skan			case '+':
233169689Skan			case '[':
234169689Skan			case ':':
235169689Skan				lastdot = NULL;
236169689Skan				break;
237169689Skan			case '.':
238169689Skan				lastdot = n;
239169689Skan				break;
240169689Skan			default:
241169689Skan				return 0;
242169689Skan			}
243169689Skan		}
244169689Skan		n++;
245169689Skan	}
246169689Skan	return lastdot > name && lastdot + 1 < n;
247169689Skan}
248169689Skan
249169689Skanstatic int
250169689Skanis_bzipped(char *name)
251169689Skan{
252169689Skan	int len = strlen(name);
253169689Skan	return len >= 5 && strcmp(&name[len - 4], BZ2_EXT) == 0;
254169689Skan}
255169689Skan
256169689Skanstatic int
257169689Skanis_gzipped(char *name)
258169689Skan{
259169689Skan	int len = strlen(name);
260169689Skan	return len >= 4 && strcmp(&name[len - 3], GZ_EXT) == 0;
261169689Skan}
262169689Skan
263169689Skan/*
264169689Skan * Converts manXXX to catXXX.
265169689Skan */
266169689Skanstatic char *
267169689Skanget_cat_section(char *section)
268169689Skan{
269169689Skan	char *cat_section;
270169689Skan
271169689Skan	cat_section = strdup(section);
272169689Skan	assert(strlen(section) > 3 && strncmp(section, "man", 3) == 0);
273169689Skan	memcpy(cat_section, "cat", 3);
274169689Skan	return cat_section;
275169689Skan}
276169689Skan
277169689Skan/*
278169689Skan * Tests to see if the given directory has already been visited.
279169689Skan */
280169689Skanstatic int
281169689Skanalready_visited(char *mandir, char *dir, int count_visit)
282169689Skan{
283169689Skan	struct stat st;
284169689Skan
285169689Skan	if (stat(dir, &st) < 0) {
286169689Skan		if (mandir != NULL)
287169689Skan			warn("%s/%s", mandir, dir);
288169689Skan		else
289169689Skan			warn("%s", dir);
290169689Skan		exit_code = 1;
291169689Skan		return 1;
292169689Skan	}
293169689Skan	if (find_hashtable(visited, st.st_ino, st.st_dev) != NULL) {
294169689Skan		if (mandir != NULL)
295169689Skan			warnx("already visited %s/%s", mandir, dir);
296169689Skan		else
297169689Skan			warnx("already visited %s", dir);
298169689Skan		return 1;
299169689Skan	}
300169689Skan	if (count_visit)
301169689Skan		insert_hashtable(visited, st.st_ino, st.st_dev, "");
302169689Skan	return 0;
303169689Skan}
304169689Skan
305169689Skan/*
306169689Skan * Returns a set of TEST_* bits describing a file's type and permissions.
307169689Skan * If mod_time isn't NULL, it will contain the file's modification time.
308169689Skan */
309169689Skanstatic int
310169689Skantest_path(char *name, time_t *mod_time)
311169689Skan{
312169689Skan	int result;
313169689Skan
314169689Skan	if (stat(name, &test_st) < 0)
315169689Skan		return 0;
316169689Skan	result = TEST_EXISTS;
317169689Skan	if (mod_time != NULL)
318169689Skan		*mod_time = test_st.st_mtime;
319169689Skan	if (S_ISDIR(test_st.st_mode))
320169689Skan		result |= TEST_DIR;
321169689Skan	else if (S_ISREG(test_st.st_mode))
322169689Skan		result |= TEST_FILE;
323169689Skan	if (access(name, R_OK))
324169689Skan		result |= TEST_READABLE;
325169689Skan	if (access(name, W_OK))
326169689Skan		result |= TEST_WRITABLE;
327169689Skan	return result;
328169689Skan}
329169689Skan
330169689Skan/*
331169689Skan * Checks whether a file is a symbolic link.
332169689Skan */
333169689Skanstatic int
334169689Skanis_symlink(char *path)
335169689Skan{
336169689Skan	struct stat st;
337169689Skan
338169689Skan	return lstat(path, &st) >= 0 && S_ISLNK(st.st_mode);
339169689Skan}
340169689Skan
341169689Skan/*
342169689Skan * Tests to see if the given directory can be written to.
343169689Skan */
344169689Skanstatic void
345169689Skancheck_writable(char *mandir)
346169689Skan{
347169689Skan	if (verbose && !(test_path(mandir, NULL) & TEST_WRITABLE))
348169689Skan		fprintf(stderr, "%s: not writable - will only be able to write to existing cat directories\n", mandir);
349169689Skan}
350169689Skan
351169689Skan/*
352169689Skan * If the directory exists, attempt to make it writable, otherwise
353169689Skan * attempt to create it.
354169689Skan */
355169689Skanstatic int
356169689Skanmake_writable_dir(char *mandir, char *dir)
357169689Skan{
358169689Skan	int test;
359169689Skan
360169689Skan	if ((test = test_path(dir, NULL)) != 0) {
361169689Skan		if (!(test & TEST_WRITABLE) && chmod(dir, 0755) < 0) {
362169689Skan			warn("%s/%s: chmod", mandir, dir);
363169689Skan			exit_code = 1;
364169689Skan			return 0;
365169689Skan		}
366169689Skan	} else {
367169689Skan		if (verbose || pretend)
368169689Skan			fprintf(stderr, "mkdir %s\n", dir);
369169689Skan		if (!pretend) {
370169689Skan			unlink(dir);
371169689Skan			if (mkdir(dir, 0755) < 0) {
372169689Skan				warn("%s/%s: mkdir", mandir, dir);
373169689Skan				exit_code = 1;
374169689Skan				return 0;
375169689Skan			}
376169689Skan		}
377169689Skan	}
378169689Skan	return 1;
379169689Skan}
380169689Skan
381169689Skan/*
382169689Skan * Processes a single man page source by using nroff to create
383169689Skan * the preformatted cat page.
384169689Skan */
385169689Skanstatic void
386169689Skanprocess_page(char *mandir, char *src, char *cat, enum Ziptype zipped)
387169689Skan{
388169689Skan	int src_test, cat_test;
389169689Skan	time_t src_mtime, cat_mtime;
390169689Skan	char cmd[MAXPATHLEN];
391169689Skan	dev_t src_dev;
392169689Skan	ino_t src_ino;
393169689Skan	const char *link_name;
394169689Skan
395169689Skan	src_test = test_path(src, &src_mtime);
396169689Skan	if (!(src_test & (TEST_FILE|TEST_READABLE))) {
397169689Skan		if (!(src_test & TEST_DIR)) {
398169689Skan			warnx("%s/%s: unreadable", mandir, src);
399169689Skan			exit_code = 1;
400169689Skan			if (rm_junk && is_symlink(src))
401169689Skan				junk(mandir, src, "bogus symlink");
402169689Skan		}
403169689Skan		return;
404169689Skan	}
405169689Skan	src_dev = test_st.st_dev;
406169689Skan	src_ino = test_st.st_ino;
407169689Skan	cat_test = test_path(cat, &cat_mtime);
408169689Skan	if (cat_test & (TEST_FILE|TEST_READABLE)) {
409169689Skan		if (!force && cat_mtime >= src_mtime) {
410169689Skan			if (verbose) {
411169689Skan				fprintf(stderr, "\t%s/%s: up to date\n",
412169689Skan				    mandir, src);
413169689Skan			}
414169689Skan			return;
415169689Skan		}
416169689Skan	}
417169689Skan	/*
418169689Skan	 * Is the man page a link to one we've already processed?
419169689Skan	 */
420169689Skan	if ((link_name = find_hashtable(links, src_ino, src_dev)) != NULL) {
421169689Skan		if (verbose || pretend) {
422169689Skan			fprintf(stderr, "%slink %s -> %s\n",
423169689Skan			    verbose ? "\t" : "", cat, link_name);
424169689Skan		}
425169689Skan		if (!pretend) {
426169689Skan			(void) unlink(cat);
427169689Skan			if (link(link_name, cat) < 0)
428169689Skan				warn("%s %s: link", link_name, cat);
429169689Skan		}
430169689Skan		return;
431169689Skan	}
432169689Skan	insert_hashtable(links, src_ino, src_dev, strdup(cat));
433169689Skan	if (verbose || pretend) {
434169689Skan		fprintf(stderr, "%sformat %s -> %s\n",
435169689Skan		    verbose ? "\t" : "", src, cat);
436169689Skan		if (pretend)
437169689Skan			return;
438169689Skan	}
439169689Skan	snprintf(tmp_file, sizeof tmp_file, "%s.tmp", cat);
440169689Skan	snprintf(cmd, sizeof cmd,
441169689Skan	    "%scat %s | tbl | nroff -c -T%s -man | %s > %s.tmp",
442169689Skan	    zipped == BZIP ? BZ2CAT_CMD : zipped == GZIP ? GZCAT_CMD : "",
443169689Skan	    src, nroff_device,
444169689Skan	    zipped == BZIP ? BZ2_CMD : zipped == GZIP ? GZ_CMD : "cat",
445169689Skan	    cat);
446169689Skan	if (system(cmd) != 0)
447169689Skan		err(1, "formatting pipeline");
448169689Skan	if (rename(tmp_file, cat) < 0)
449169689Skan		warn("%s", cat);
450169689Skan	tmp_file[0] = '\0';
451169689Skan}
452169689Skan
453169689Skan/*
454169689Skan * Scan the man section directory for pages and process each one,
455169689Skan * then check for junk in the corresponding cat section.
456169689Skan */
457169689Skanstatic void
458169689Skanscan_section(char *mandir, char *section, char *cat_section)
459169689Skan{
460169689Skan	struct dirent **entries;
461169689Skan	char **expected = NULL;
462169689Skan	int npages;
463169689Skan	int nexpected = 0;
464169689Skan	int i, e;
465169689Skan	enum Ziptype zipped;
466169689Skan	char *page_name;
467169689Skan	char page_path[MAXPATHLEN];
468169689Skan	char cat_path[MAXPATHLEN];
469169689Skan	char zip_path[MAXPATHLEN];
470169689Skan
471169689Skan	/*
472169689Skan	 * scan the man section directory for pages
473169689Skan	 */
474169689Skan	npages = scandir(section, &entries, NULL, alphasort);
475169689Skan	if (npages < 0) {
476169689Skan		warn("%s/%s", mandir, section);
477169689Skan		exit_code = 1;
478169689Skan		return;
479169689Skan	}
480169689Skan	if (verbose || rm_junk) {
481169689Skan		/*
482169689Skan		 * Maintain a list of all cat pages that should exist,
483169689Skan		 * corresponding to existing man pages.
484169689Skan		 */
485169689Skan		expected = (char **) calloc(npages, sizeof(char *));
486169689Skan	}
487169689Skan	for (i = 0; i < npages; free(entries[i++])) {
488169689Skan		page_name = entries[i]->d_name;
489169689Skan		snprintf(page_path, sizeof page_path, "%s/%s", section,
490169689Skan		    page_name);
491169689Skan		if (!is_manpage_name(page_name)) {
492169689Skan			if (!(test_path(page_path, NULL) & TEST_DIR)) {
493169689Skan				junk(mandir, page_path,
494169689Skan				    "invalid man page name");
495169689Skan			}
496169689Skan			continue;
497169689Skan		}
498169689Skan		zipped = is_bzipped(page_name) ? BZIP :
499169689Skan		    is_gzipped(page_name) ? GZIP : NONE;
500169689Skan		if (zipped != NONE) {
501169689Skan			snprintf(cat_path, sizeof cat_path, "%s/%s",
502169689Skan			    cat_section, page_name);
503169689Skan			if (expected != NULL)
504169689Skan				expected[nexpected++] = strdup(page_name);
505169689Skan			process_page(mandir, page_path, cat_path, zipped);
506169689Skan		} else {
507169689Skan			/*
508169689Skan			 * We've got an uncompressed man page,
509169689Skan			 * check to see if there's a (preferred)
510169689Skan			 * compressed one.
511169689Skan			 */
512169689Skan			snprintf(zip_path, sizeof zip_path, "%s%s",
513169689Skan			    page_path, GZ_EXT);
514169689Skan			if (test_path(zip_path, NULL) != 0) {
515169689Skan				junk(mandir, page_path,
516169689Skan				    "man page unused due to existing " GZ_EXT);
517169689Skan			} else {
518169689Skan				if (verbose) {
519169689Skan					fprintf(stderr,
520169689Skan						"warning, %s is uncompressed\n",
521169689Skan						page_path);
522169689Skan				}
523169689Skan				snprintf(cat_path, sizeof cat_path, "%s/%s",
524169689Skan				    cat_section, page_name);
525169689Skan				if (expected != NULL) {
526169689Skan					asprintf(&expected[nexpected++],
527169689Skan					    "%s", page_name);
528169689Skan				}
529169689Skan				process_page(mandir, page_path, cat_path, NONE);
530169689Skan			}
531169689Skan		}
532169689Skan	}
533169689Skan	free(entries);
534169689Skan	if (expected == NULL)
535169689Skan	    return;
536169689Skan	/*
537169689Skan	 * scan cat sections for junk
538169689Skan	 */
539169689Skan	npages = scandir(cat_section, &entries, NULL, alphasort);
540169689Skan	e = 0;
541169689Skan	for (i = 0; i < npages; free(entries[i++])) {
542169689Skan		const char *junk_reason;
543169689Skan		int cmp = 1;
544169689Skan
545169689Skan		page_name = entries[i]->d_name;
546169689Skan		if (strcmp(page_name, ".") == 0 || strcmp(page_name, "..") == 0)
547169689Skan			continue;
548169689Skan		/*
549169689Skan		 * Keep the index into the expected cat page list
550169689Skan		 * ahead of the name we've found.
551169689Skan		 */
552169689Skan		while (e < nexpected &&
553169689Skan		    (cmp = strcmp(page_name, expected[e])) > 0)
554169689Skan			free(expected[e++]);
555169689Skan		if (cmp == 0)
556169689Skan			continue;
557169689Skan		/* we have an unexpected page */
558169689Skan		snprintf(cat_path, sizeof cat_path, "%s/%s", cat_section,
559169689Skan		    page_name);
560169689Skan		if (!is_manpage_name(page_name)) {
561169689Skan			if (test_path(cat_path, NULL) & TEST_DIR)
562169689Skan				continue;
563169689Skan			junk_reason = "invalid cat page name";
564169689Skan		} else if (!is_gzipped(page_name) && e + 1 < nexpected &&
565169689Skan		    strncmp(page_name, expected[e + 1], strlen(page_name)) == 0 &&
566169689Skan		    strlen(expected[e + 1]) == strlen(page_name) + 3) {
567169689Skan			junk_reason = "cat page unused due to existing " GZ_EXT;
568169689Skan		} else
569169689Skan			junk_reason = "cat page without man page";
570169689Skan		junk(mandir, cat_path, junk_reason);
571169689Skan	}
572169689Skan	free(entries);
573169689Skan	while (e < nexpected)
574169689Skan		free(expected[e++]);
575169689Skan	free(expected);
576169689Skan}
577169689Skan
578169689Skan
579169689Skan/*
580169689Skan * Processes a single man section.
581169689Skan */
582169689Skanstatic void
583169689Skanprocess_section(char *mandir, char *section)
584169689Skan{
585169689Skan	char *cat_section;
586169689Skan
587169689Skan	if (already_visited(mandir, section, 1))
588169689Skan		return;
589169689Skan	if (verbose)
590169689Skan		fprintf(stderr, "  section %s\n", section);
591169689Skan	cat_section = get_cat_section(section);
592169689Skan	if (make_writable_dir(mandir, cat_section))
593169689Skan		scan_section(mandir, section, cat_section);
594169689Skan	free(cat_section);
595169689Skan}
596169689Skan
597169689Skanstatic int
598169689Skanselect_sections(const struct dirent *entry)
599169689Skan{
600169689Skan	char *name;
601169689Skan	int ret;
602169689Skan
603169689Skan	name = strdup(entry->d_name);
604169689Skan	ret = directory_type(name) == MAN_SECTION_DIR;
605169689Skan	free(name);
606169689Skan	return (ret);
607169689Skan}
608169689Skan
609169689Skan/*
610169689Skan * Processes a single top-level man directory.  If section isn't NULL,
611169689Skan * it will only process that section sub-directory, otherwise it will
612169689Skan * process all of them.
613169689Skan */
614169689Skanstatic void
615169689Skanprocess_mandir(char *dir_name, char *section)
616169689Skan{
617169689Skan	if (fchdir(starting_dir) < 0)
618169689Skan		err(1, "fchdir");
619169689Skan	if (already_visited(NULL, dir_name, section == NULL))
620169689Skan		return;
621169689Skan	check_writable(dir_name);
622169689Skan	if (verbose)
623169689Skan		fprintf(stderr, "man directory %s\n", dir_name);
624169689Skan	if (pretend)
625169689Skan		fprintf(stderr, "cd %s\n", dir_name);
626169689Skan	if (chdir(dir_name) < 0) {
627169689Skan		warn("%s: chdir", dir_name);
628169689Skan		exit_code = 1;
629169689Skan		return;
630169689Skan	}
631169689Skan	if (section != NULL) {
632169689Skan		process_section(dir_name, section);
633169689Skan	} else {
634169689Skan		struct dirent **entries;
635169689Skan		char *machine_dir, *arch_dir;
636169689Skan		int nsections;
637169689Skan		int i;
638169689Skan
639169689Skan		nsections = scandir(".", &entries, select_sections, alphasort);
640169689Skan		if (nsections < 0) {
641169689Skan			warn("%s", dir_name);
642			exit_code = 1;
643			return;
644		}
645		for (i = 0; i < nsections; i++) {
646			process_section(dir_name, entries[i]->d_name);
647			asprintf(&machine_dir, "%s/%s", entries[i]->d_name,
648			    machine);
649			if (test_path(machine_dir, NULL) & TEST_DIR)
650				process_section(dir_name, machine_dir);
651			free(machine_dir);
652			if (strcmp(machine_arch, machine) != 0) {
653				asprintf(&arch_dir, "%s/%s", entries[i]->d_name,
654				    machine_arch);
655				if (test_path(arch_dir, NULL) & TEST_DIR)
656					process_section(dir_name, arch_dir);
657				free(arch_dir);
658			}
659			free(entries[i]);
660		}
661		free(entries);
662	}
663}
664
665/*
666 * Processes one argument, which may be a colon-separated list of
667 * directories.
668 */
669static void
670process_argument(const char *arg)
671{
672	char *dir;
673	char *mandir;
674	char *section;
675	char *parg;
676
677	parg = strdup(arg);
678	if (parg == NULL)
679		err(1, "out of memory");
680	while ((dir = strsep(&parg, ":")) != NULL) {
681		switch (directory_type(dir)) {
682		case TOP_LEVEL_DIR:
683			if (locale != NULL) {
684				asprintf(&mandir, "%s/%s", dir, locale);
685				process_mandir(mandir, NULL);
686				free(mandir);
687				if (lang_locale != NULL) {
688					asprintf(&mandir, "%s/%s", dir,
689					    lang_locale);
690					process_mandir(mandir, NULL);
691					free(mandir);
692				}
693			} else {
694				process_mandir(dir, NULL);
695			}
696			break;
697		case MAN_SECTION_DIR: {
698			mandir = strdup(dirname(dir));
699			section = strdup(basename(dir));
700			process_mandir(mandir, section);
701			free(mandir);
702			free(section);
703			break;
704			}
705		default:
706			warnx("%s: directory name not in proper man form", dir);
707			exit_code = 1;
708		}
709	}
710	free(parg);
711}
712
713static void
714determine_locale(void)
715{
716	char *sep;
717
718	if ((locale = setlocale(LC_CTYPE, "")) == NULL) {
719		warnx("-L option used, but no locale found\n");
720		return;
721	}
722	sep = strchr(locale, '_');
723	if (sep != NULL && isupper((unsigned char)sep[1])
724			&& isupper((unsigned char)sep[2])) {
725		asprintf(&lang_locale, "%.*s%s", (int)(sep - locale),
726		    locale, &sep[3]);
727	}
728	sep = nl_langinfo(CODESET);
729	if (sep != NULL && *sep != '\0' && strcmp(sep, "US-ASCII") != 0) {
730		int i;
731
732		for (i = 0; locale_device[i] != NULL; i += 2) {
733			if (strcmp(sep, locale_device[i]) == 0) {
734				nroff_device = locale_device[i + 1];
735				break;
736			}
737		}
738	}
739	if (verbose) {
740		if (lang_locale != NULL)
741			fprintf(stderr, "short locale is %s\n", lang_locale);
742		fprintf(stderr, "nroff device is %s\n", nroff_device);
743	}
744}
745
746static void
747usage(void)
748{
749	fprintf(stderr, "usage: %s [-fLnrv] [directories ...]\n",
750	    getprogname());
751	exit(1);
752}
753
754int
755main(int argc, char **argv)
756{
757	int opt;
758
759	if ((uid = getuid()) == 0) {
760		fprintf(stderr, "don't run %s as root, use:\n   echo", argv[0]);
761		for (optind = 0; optind < argc; optind++) {
762			fprintf(stderr, " %s", argv[optind]);
763		}
764		fprintf(stderr, " | nice -5 su -m man\n");
765		exit(1);
766	}
767	while ((opt = getopt(argc, argv, "vnfLrh")) != -1) {
768		switch (opt) {
769		case 'f':
770			force++;
771			break;
772		case 'L':
773			determine_locale();
774			break;
775		case 'n':
776			pretend++;
777			break;
778		case 'r':
779			rm_junk++;
780			break;
781		case 'v':
782			verbose++;
783			break;
784		default:
785			usage();
786			/* NOTREACHED */
787		}
788	}
789	if ((starting_dir = open(".", 0)) < 0) {
790		err(1, ".");
791	}
792	umask(022);
793	signal(SIGINT, trap_signal);
794	signal(SIGHUP, trap_signal);
795	signal(SIGQUIT, trap_signal);
796	signal(SIGTERM, trap_signal);
797
798	if ((machine = getenv("MACHINE")) == NULL) {
799		static struct utsname utsname;
800
801		if (uname(&utsname) == -1)
802			err(1, "uname");
803		machine = utsname.machine;
804	}
805
806	if ((machine_arch = getenv("MACHINE_ARCH")) == NULL)
807		machine_arch = MACHINE_ARCH;
808
809	if (optind == argc) {
810		const char *manpath = getenv("MANPATH");
811		if (manpath == NULL)
812			manpath = DEFAULT_MANPATH;
813		process_argument(manpath);
814	} else {
815		while (optind < argc)
816			process_argument(argv[optind++]);
817	}
818	exit(exit_code);
819}
820