sacc

sacc (saccomys): simple gopher client.
Log | Files | Refs | LICENSE

sacc.c (19153B)


      1 /* See LICENSE file for copyright and license details. */
      2 #include <ctype.h>
      3 #include <errno.h>
      4 #include <fcntl.h>
      5 #include <limits.h>
      6 #include <locale.h>
      7 #include <netdb.h>
      8 #include <netinet/in.h>
      9 #include <signal.h>
     10 #include <stdarg.h>
     11 #include <stdio.h>
     12 #include <stdlib.h>
     13 #include <string.h>
     14 #include <unistd.h>
     15 #include <wchar.h>
     16 #include <sys/socket.h>
     17 #include <sys/stat.h>
     18 #include <sys/types.h>
     19 #include <sys/wait.h>
     20 
     21 #include "version.h"
     22 #include "common.h"
     23 #include "io.h"
     24 
     25 enum {
     26 	TXT,
     27 	DIR,
     28 	CSO,
     29 	ERR,
     30 	MAC,
     31 	DOS,
     32 	UUE,
     33 	IND,
     34 	TLN,
     35 	BIN,
     36 	MIR,
     37 	IBM,
     38 	GIF,
     39 	IMG,
     40 	URL,
     41 	INF,
     42 	UNK,
     43 	BRK,
     44 };
     45 
     46 #define NEED_CONF
     47 #include "config.h"
     48 #undef NEED_CONF
     49 
     50 void (*diag)(char *, ...);
     51 
     52 int interactive;
     53 const char ident[] = "@(#) sacc(omys): " VERSION;
     54 
     55 static char intbuf[256]; /* 256B ought to be enough for any URI */
     56 static char *mainurl;
     57 static Item *mainentry;
     58 static int devnullfd;
     59 static int parent = 1;
     60 
     61 static void
     62 stddiag(char *fmt, ...)
     63 {
     64 	va_list arg;
     65 
     66 	va_start(arg, fmt);
     67 	vfprintf(stderr, fmt, arg);
     68 	va_end(arg);
     69 	fputc('\n', stderr);
     70 }
     71 
     72 void
     73 die(const char *fmt, ...)
     74 {
     75 	va_list arg;
     76 
     77 	va_start(arg, fmt);
     78 	vfprintf(stderr, fmt, arg);
     79 	va_end(arg);
     80 	fputc('\n', stderr);
     81 
     82 	exit(1);
     83 }
     84 
     85 #ifdef NEED_ASPRINTF
     86 int
     87 asprintf(char **s, const char *fmt, ...)
     88 {
     89 	va_list ap;
     90 	int n;
     91 
     92 	va_start(ap, fmt);
     93 	n = vsnprintf(NULL, 0, fmt, ap);
     94 	va_end(ap);
     95 
     96 	if (n == INT_MAX || !(*s = malloc(++n)))
     97 		return -1;
     98 
     99 	va_start(ap, fmt);
    100 	vsnprintf(*s, n, fmt, ap);
    101 	va_end(ap);
    102 
    103 	return n;
    104 }
    105 #endif /* NEED_ASPRINTF */
    106 
    107 #ifdef NEED_STRCASESTR
    108 char *
    109 strcasestr(const char *h, const char *n)
    110 {
    111 	size_t i;
    112 
    113 	if (!n[0])
    114 		return (char *)h;
    115 
    116 	for (; *h; ++h) {
    117 		for (i = 0; n[i] && tolower((unsigned char)n[i]) ==
    118 		            tolower((unsigned char)h[i]); ++i)
    119 			;
    120 		if (n[i] == '\0')
    121 			return (char *)h;
    122 	}
    123 
    124 	return NULL;
    125 }
    126 #endif /* NEED_STRCASESTR */
    127 
    128 /* print `len' columns of characters. */
    129 size_t
    130 mbsprint(const char *s, size_t len)
    131 {
    132 	wchar_t wc;
    133 	size_t col = 0, i, slen;
    134 	const char *p;
    135 	int rl, pl, w;
    136 
    137 	if (!len)
    138 		return col;
    139 
    140 	slen = strlen(s);
    141 	for (i = 0; i < slen; i += rl) {
    142 		rl = mbtowc(&wc, s + i, slen - i < 4 ? slen - i : 4);
    143 		if (rl == -1) {
    144 			/* reset state */
    145 			mbtowc(NULL, NULL, 0);
    146 			p = "\xef\xbf\xbd"; /* replacement character */
    147 			pl = 3;
    148 			rl = w = 1;
    149 		} else {
    150 			if ((w = wcwidth(wc)) == -1)
    151 				continue;
    152 			pl = rl;
    153 			p = s + i;
    154 		}
    155 		if (col + w > len || (col + w == len && s[i + rl])) {
    156 			fputs("\xe2\x80\xa6", stdout); /* ellipsis */
    157 			col++;
    158 			break;
    159 		}
    160 		fwrite(p, 1, pl, stdout);
    161 		col += w;
    162 	}
    163 	return col;
    164 }
    165 
    166 static void *
    167 xreallocarray(void *m, size_t n, size_t s)
    168 {
    169 	void *nm;
    170 
    171 	if (n == 0 || s == 0) {
    172 		free(m);
    173 		return NULL;
    174 	}
    175 	if (s && n > (size_t)-1/s)
    176 		die("realloc: overflow");
    177 	if (!(nm = realloc(m, n * s)))
    178 		die("realloc: %s", strerror(errno));
    179 
    180 	return nm;
    181 }
    182 
    183 static void *
    184 xmalloc(const size_t n)
    185 {
    186 	void *m = malloc(n);
    187 
    188 	if (!m)
    189 		die("malloc: %s", strerror(errno));
    190 
    191 	return m;
    192 }
    193 
    194 static void *
    195 xcalloc(size_t n)
    196 {
    197 	char *m = calloc(1, n);
    198 
    199 	if (!m)
    200 		die("calloc: %s", strerror(errno));
    201 
    202 	return m;
    203 }
    204 
    205 static char *
    206 xstrdup(const char *str)
    207 {
    208 	char *s;
    209 
    210 	if (!(s = strdup(str)))
    211 		die("strdup: %s", strerror(errno));
    212 
    213 	return s;
    214 }
    215 
    216 static void
    217 usage(void)
    218 {
    219 	die("usage: sacc URL");
    220 }
    221 
    222 static void
    223 clearitem(Item *item)
    224 {
    225 	Dir *dir;
    226 	Item *items;
    227 	char *tag;
    228 	size_t i;
    229 
    230 	if (!item)
    231 		return;
    232 
    233 	if (dir = item->dat) {
    234 		items = dir->items;
    235 		for (i = 0; i < dir->nitems; ++i)
    236 			clearitem(&items[i]);
    237 		free(items);
    238 		clear(&item->dat);
    239 	}
    240 
    241 	if (parent && (tag = item->tag) &&
    242 	    !strncmp(tag, tmpdir, strlen(tmpdir)))
    243 		unlink(tag);
    244 
    245 	clear(&item->tag);
    246 	clear(&item->raw);
    247 }
    248 
    249 const char *
    250 typedisplay(char t)
    251 {
    252 	switch (t) {
    253 	case '0':
    254 		return typestr[TXT];
    255 	case '1':
    256 		return typestr[DIR];
    257 	case '2':
    258 		return typestr[CSO];
    259 	case '3':
    260 		return typestr[ERR];
    261 	case '4':
    262 		return typestr[MAC];
    263 	case '5':
    264 		return typestr[DOS];
    265 	case '6':
    266 		return typestr[UUE];
    267 	case '7':
    268 		return typestr[IND];
    269 	case '8':
    270 		return typestr[TLN];
    271 	case '9':
    272 		return typestr[BIN];
    273 	case '+':
    274 		return typestr[MIR];
    275 	case 'T':
    276 		return typestr[IBM];
    277 	case 'g':
    278 		return typestr[GIF];
    279 	case 'I':
    280 		return typestr[IMG];
    281 	case 'h':
    282 		return typestr[URL];
    283 	case 'i':
    284 		return typestr[INF];
    285 	default:
    286 		/* "Characters '0' through 'Z' are reserved." (ASCII) */
    287 		if (t >= '0' && t <= 'Z')
    288 			return typestr[BRK];
    289 		else
    290 			return typestr[UNK];
    291 	}
    292 }
    293 
    294 int
    295 itemuri(Item *item, char *buf, size_t bsz)
    296 {
    297 	int n;
    298 
    299 	switch (item->type) {
    300 	case '8':
    301 		n = snprintf(buf, bsz, "telnet://%s@%s:%s",
    302 		             item->selector, item->host, item->port);
    303 		break;
    304 	case 'T':
    305 		n = snprintf(buf, bsz, "tn3270://%s@%s:%s",
    306 		             item->selector, item->host, item->port);
    307 		break;
    308 	case 'h':
    309 		n = snprintf(buf, bsz, "%s", item->selector +
    310 		             (strncmp(item->selector, "URL:", 4) ? 0 : 4));
    311 		break;
    312 	default:
    313 		n = snprintf(buf, bsz, "gopher://%s", item->host);
    314 
    315 		if (n < bsz-1 && strcmp(item->port, "70"))
    316 			n += snprintf(buf+n, bsz-n, ":%s", item->port);
    317 		if (n < bsz-1) {
    318 			n += snprintf(buf+n, bsz-n, "/%c%s",
    319 			              item->type, item->selector);
    320 		}
    321 		if (n < bsz-1 && item->type == '7' && item->tag) {
    322 			n += snprintf(buf+n, bsz-n, "%%09%s",
    323 			              item->tag + strlen(item->selector));
    324 		}
    325 		break;
    326 	}
    327 
    328 	return n;
    329 }
    330 
    331 static void
    332 printdir(Item *item)
    333 {
    334 	Dir *dir;
    335 	Item *items;
    336 	size_t i, nitems;
    337 
    338 	if (!item || !(dir = item->dat))
    339 		return;
    340 
    341 	items = dir->items;
    342 	nitems = dir->nitems;
    343 
    344 	for (i = 0; i < nitems; ++i) {
    345 		printf("%s%s\n",
    346 		       typedisplay(items[i].type), items[i].username);
    347 	}
    348 }
    349 
    350 static void
    351 displaytextitem(Item *item)
    352 {
    353 	struct sigaction sa;
    354 	FILE *pagerin;
    355 	int pid, wpid;
    356 
    357 	sigemptyset(&sa.sa_mask);
    358 	sa.sa_flags = SA_RESTART;
    359 	sa.sa_handler = SIG_DFL;
    360 	sigaction(SIGWINCH, &sa, NULL);
    361 
    362 	uicleanup();
    363 
    364 	switch (pid = fork()) {
    365 	case -1:
    366 		diag("Couldn't fork.");
    367 		return;
    368 	case 0:
    369 		parent = 0;
    370 		if (!(pagerin = popen("$PAGER", "w")))
    371 			_exit(1);
    372 		fputs(item->raw, pagerin);
    373 		exit(pclose(pagerin));
    374 	default:
    375 		while ((wpid = wait(NULL)) >= 0 && wpid != pid)
    376 			;
    377 	}
    378 	uisetup();
    379 
    380 	sa.sa_handler = uisigwinch;
    381 	sigaction(SIGWINCH, &sa, NULL);
    382 	uisigwinch(SIGWINCH); /* force redraw */
    383 }
    384 
    385 static char *
    386 pickfield(char **raw, const char *sep)
    387 {
    388 	char c, *r, *f = *raw;
    389 
    390 	for (r = *raw; (c = *r) && !strchr(sep, c); ++r) {
    391 		if (c == '\n')
    392 			goto skipsep;
    393 	}
    394 
    395 	*r++ = '\0';
    396 skipsep:
    397 	*raw = r;
    398 
    399 	return f;
    400 }
    401 
    402 static char *
    403 invaliditem(char *raw)
    404 {
    405 	char c;
    406 	int tabs;
    407 
    408 	for (tabs = 0; (c = *raw) && c != '\n'; ++raw) {
    409 		if (c == '\t')
    410 			++tabs;
    411 	}
    412 	if (tabs < 3) {
    413 		*raw++ = '\0';
    414 		return raw;
    415 	}
    416 
    417 	return NULL;
    418 }
    419 
    420 static void
    421 molditem(Item *item, char **raw)
    422 {
    423 	char *next;
    424 
    425 	if (!*raw)
    426 		return;
    427 
    428 	if ((next = invaliditem(*raw))) {
    429 		item->username = *raw;
    430 		*raw = next;
    431 		return;
    432 	}
    433 
    434 	item->type = *raw[0]++;
    435 	item->username = pickfield(raw, "\t");
    436 	item->selector = pickfield(raw, "\t");
    437 	item->host = pickfield(raw, "\t");
    438 	item->port = pickfield(raw, "\t\r");
    439 	while (*raw[0] != '\n')
    440 		++*raw;
    441 	*raw[0]++ = '\0';
    442 }
    443 
    444 static Dir *
    445 molddiritem(char *raw)
    446 {
    447 	Item *item, *items = NULL;
    448 	char *nl, *p;
    449 	Dir *dir;
    450 	size_t i, n, nitems;
    451 
    452 	for (nl = raw, nitems = 0; p = strchr(nl, '\n'); nl = p+1)
    453 		++nitems;
    454 
    455 	if (!nitems) {
    456 		diag("Couldn't parse dir item");
    457 		return NULL;
    458 	}
    459 
    460 	dir = xmalloc(sizeof(Dir));
    461 	items = xreallocarray(items, nitems, sizeof(Item));
    462 	memset(items, 0, nitems * sizeof(Item));
    463 
    464 	for (i = 0; i < nitems; ++i) {
    465 		item = &items[i];
    466 		molditem(item, &raw);
    467 		if (item->type == '+') {
    468 			for (n = i - 1; n < (size_t)-1; --n) {
    469 				if (items[n].type != '+') {
    470 					item->redtype = items[n].type;
    471 					break;
    472 				}
    473 			}
    474 		}
    475 	}
    476 
    477 	dir->items = items;
    478 	dir->nitems = nitems;
    479 	dir->printoff = dir->curline = 0;
    480 
    481 	return dir;
    482 }
    483 
    484 static char *
    485 getrawitem(struct cnx *c)
    486 {
    487 	char *raw, *buf;
    488 	size_t bn, bs;
    489 	ssize_t n;
    490 
    491 	raw = buf = NULL;
    492 	bn = bs = n = 0;
    493 
    494 	do {
    495 		bs -= n;
    496 		buf += n;
    497 
    498 		if (buf - raw >= 5) {
    499 			if (strncmp(buf-5, "\r\n.\r\n", 5) == 0) {
    500 				buf[-3] = '\0';
    501 				break;
    502 			}
    503 		} else if (buf - raw == 3 && strncmp(buf-3, ".\r\n", 3)) {
    504 			buf[-3] = '\0';
    505 			break;
    506 		}
    507 
    508 		if (bs < 1) {
    509 			raw = xreallocarray(raw, ++bn, BUFSIZ);
    510 			buf = raw + (bn-1) * BUFSIZ;
    511 			bs = BUFSIZ;
    512 		}
    513 
    514 	} while ((n = ioread(c, buf, bs)) > 0);
    515 
    516 	*buf = '\0';
    517 
    518 	if (n == -1) {
    519 		diag("Can't read socket: %s", strerror(errno));
    520 		clear(&raw);
    521 	}
    522 
    523 	return raw;
    524 }
    525 
    526 static int
    527 sendselector(struct cnx *c, const char *selector)
    528 {
    529 	char *msg, *p;
    530 	size_t ln;
    531 	ssize_t n;
    532 
    533 	ln = strlen(selector) + 3;
    534 	msg = p = xmalloc(ln);
    535 	snprintf(msg, ln--, "%s\r\n", selector);
    536 
    537 	while ((n = iowrite(c, p, ln)) > 0) {
    538 		ln -= n;
    539 		p += n;
    540 	};
    541 
    542 	free(msg);
    543 	if (n == -1)
    544 		diag("Can't send message: %s", strerror(errno));
    545 
    546 	return n;
    547 }
    548 
    549 static int
    550 connectto(const char *host, const char *port, struct cnx *c)
    551 {
    552 	sigset_t set, oset;
    553 	static const struct addrinfo hints = {
    554 	    .ai_family = AF_UNSPEC,
    555 	    .ai_socktype = SOCK_STREAM,
    556 	    .ai_protocol = IPPROTO_TCP,
    557 	};
    558 	struct addrinfo *addrs, *ai;
    559 	int r, err;
    560 
    561 	sigemptyset(&set);
    562 	sigaddset(&set, SIGWINCH);
    563 	sigprocmask(SIG_BLOCK, &set, &oset);
    564 
    565 	if (r = getaddrinfo(host, port, &hints, &addrs)) {
    566 		diag("Can't resolve hostname \"%s\": %s",
    567 		     host, gai_strerror(r));
    568 		goto err;
    569 	}
    570 
    571 	r = -1;
    572 	for (ai = addrs; ai && r == -1; ai = ai->ai_next) {
    573 		do {
    574 			if ((c->sock = socket(ai->ai_family, ai->ai_socktype,
    575 			                      ai->ai_protocol)) == -1) {
    576 				err = errno;
    577 				break;
    578 			}
    579 
    580 			if ((r = ioconnect(c, ai, host)) < 0) {
    581 				err = errno;
    582 				ioclose(c);
    583 			}
    584 		} while (r == CONN_RETRY);
    585 	}
    586 
    587 	freeaddrinfo(addrs);
    588 
    589 	if (r == CONN_ERROR)
    590 		ioconnerr(c, host, port, err);
    591 err:
    592 	sigprocmask(SIG_SETMASK, &oset, NULL);
    593 
    594 	return r;
    595 }
    596 
    597 static int
    598 download(Item *item, int dest)
    599 {
    600 	char buf[BUFSIZ];
    601 	struct cnx c = { 0 };
    602 	ssize_t r, w;
    603 
    604 	if (item->tag == NULL) {
    605 		if (connectto(item->host, item->port, &c) < 0 ||
    606 		    sendselector(&c, item->selector) == -1)
    607 			return 0;
    608 	} else {
    609 		if ((c.sock = open(item->tag, O_RDONLY)) == -1) {
    610 			printf("Can't open source file %s: %s",
    611 			       item->tag, strerror(errno));
    612 			errno = 0;
    613 			return 0;
    614 		}
    615 	}
    616 
    617 	w = 0;
    618 	while ((r = ioread(&c, buf, BUFSIZ)) > 0) {
    619 		while ((w = write(dest, buf, r)) > 0)
    620 			r -= w;
    621 	}
    622 
    623 	if (r == -1 || w == -1) {
    624 		printf("Error downloading file %s: %s",
    625 		       item->selector, strerror(errno));
    626 		errno = 0;
    627 	}
    628 
    629 	close(dest);
    630 	ioclose(&c);
    631 
    632 	return (r == 0 && w == 0);
    633 }
    634 
    635 static void
    636 downloaditem(Item *item)
    637 {
    638 	char *file, *path, *tag;
    639 	mode_t mode = S_IRUSR|S_IWUSR|S_IRGRP;
    640 	int dest;
    641 
    642 	if (file = strrchr(item->selector, '/'))
    643 		++file;
    644 	else
    645 		file = item->selector;
    646 
    647 	if (!(path = uiprompt("Download to [%s] (^D cancel): ", file)))
    648 		return;
    649 
    650 	if (!path[0])
    651 		path = xstrdup(file);
    652 
    653 	if (tag = item->tag) {
    654 		if (access(tag, R_OK) == -1) {
    655 			clear(&item->tag);
    656 		} else if (!strcmp(tag, path)) {
    657 			goto cleanup;
    658 		}
    659 	}
    660 
    661 	if ((dest = open(path, O_WRONLY|O_CREAT|O_EXCL, mode)) == -1) {
    662 		diag("Can't open destination file %s: %s",
    663 		     path, strerror(errno));
    664 		errno = 0;
    665 		goto cleanup;
    666 	}
    667 
    668 	if (!download(item, dest))
    669 		goto cleanup;
    670 
    671 	if (item->tag)
    672 		goto cleanup;
    673 
    674 	item->tag = path;
    675 
    676 	return;
    677 cleanup:
    678 	free(path);
    679 	return;
    680 }
    681 
    682 static int
    683 fetchitem(Item *item)
    684 {
    685 	struct cnx c;
    686 	char *raw;
    687 
    688 	if (connectto(item->host, item->port, &c) < 0 ||
    689 	    sendselector(&c, item->selector) == -1)
    690 		return 0;
    691 
    692 	raw = getrawitem(&c);
    693 	ioclose(&c);
    694 
    695 	if (raw == NULL || !*raw) {
    696 		diag("Empty response from server");
    697 		clear(&raw);
    698 	}
    699 
    700 	return ((item->raw = raw) != NULL);
    701 }
    702 
    703 static void
    704 pipeuri(char *cmd, char *msg, char *uri)
    705 {
    706 	FILE *sel;
    707 
    708 	if ((sel = popen(cmd, "w")) == NULL) {
    709 		diag("URI not %s\n", msg);
    710 		return;
    711 	}
    712 
    713 	fputs(uri, sel);
    714 	pclose(sel);
    715 	diag("%s \"%s\"", msg, uri);
    716 }
    717 
    718 static void
    719 execuri(char *cmd, char *msg, char *uri)
    720 {
    721 	switch (fork()) {
    722 	case -1:
    723 		diag("Couldn't fork.");
    724 		return;
    725 	case 0:
    726 		parent = 0;
    727 		dup2(devnullfd, 1);
    728 		dup2(devnullfd, 2);
    729 		if (execlp(cmd, cmd, uri, NULL) == -1)
    730 			_exit(1);
    731 	default:
    732 		if (modalplumber) {
    733 			while (waitpid(-1, NULL, 0) != -1)
    734 				;
    735 		}
    736 	}
    737 
    738 	diag("%s \"%s\"", msg, uri);
    739 }
    740 
    741 static void
    742 plumbitem(Item *item)
    743 {
    744 	char *file, *path, *tag;
    745 	mode_t mode = S_IRUSR|S_IWUSR|S_IRGRP;
    746 	int dest, plumbitem;
    747 
    748 	if (file = strrchr(item->selector, '/'))
    749 		++file;
    750 	else
    751 		file = item->selector;
    752 
    753 	path = uiprompt("Download %s to (^D cancel, <empty> plumb): ",
    754 	                file);
    755 	if (!path)
    756 		return;
    757 
    758 	if ((tag = item->tag) && access(tag, R_OK) == -1) {
    759 		clear(&item->tag);
    760 		tag = NULL;
    761 	}
    762 
    763 	plumbitem = path[0] ? 0 : 1;
    764 
    765 	if (!path[0]) {
    766 		clear(&path);
    767 		if (!tag) {
    768 			if (asprintf(&path, "%s/%s", tmpdir, file) == -1)
    769 				die("Can't generate tmpdir path: %s/%s: %s",
    770 				    tmpdir, file, strerror(errno));
    771 		}
    772 	}
    773 
    774 	if (path && (!tag || strcmp(tag, path))) {
    775 		if ((dest = open(path, O_WRONLY|O_CREAT|O_EXCL, mode)) == -1) {
    776 			diag("Can't open destination file %s: %s",
    777 			     path, strerror(errno));
    778 			errno = 0;
    779 			goto cleanup;
    780 		}
    781 		if (!download(item, dest) || tag)
    782 			goto cleanup;
    783 	}
    784 
    785 	if (!tag)
    786 		item->tag = path;
    787 
    788 	if (plumbitem)
    789 		execuri(plumber, "Plumbed", item->tag);
    790 
    791 	return;
    792 cleanup:
    793 	free(path);
    794 	return;
    795 }
    796 
    797 void
    798 yankitem(Item *item)
    799 {
    800 	itemuri(item, intbuf, sizeof(intbuf));
    801 	pipeuri(yanker, "Yanked", intbuf);
    802 }
    803 
    804 static int
    805 dig(Item *entry, Item *item)
    806 {
    807 	char *plumburi = NULL;
    808 	int t;
    809 
    810 	if (item->raw) /* already in cache */
    811 		return item->type;
    812 	if (!item->entry)
    813 		item->entry = entry ? entry : item;
    814 
    815 	t = item->redtype ? item->redtype : item->type;
    816 	switch (t) {
    817 	case 'h': /* fallthrough */
    818 		if (!strncmp(item->selector, "URL:", 4)) {
    819 			execuri(plumber, "Plumbed", item->selector+4);
    820 			return 0;
    821 		}
    822 	case '0':
    823 		if (!fetchitem(item))
    824 			return 0;
    825 		break;
    826 	case '1':
    827 	case '7':
    828 		if (!fetchitem(item) || !(item->dat = molddiritem(item->raw)))
    829 			return 0;
    830 		break;
    831 	case '4':
    832 	case '5':
    833 	case '6':
    834 	case '9':
    835 		downloaditem(item);
    836 		return 0;
    837 	case '8':
    838 		if (asprintf(&plumburi, "telnet://%s%s%s:%s",
    839 		             item->selector, item->selector ? "@" : "",
    840 		             item->host, item->port) == -1)
    841 			return 0;
    842 		execuri(plumber, "Plumbed", plumburi);
    843 		free(plumburi);
    844 		return 0;
    845 	case 'T':
    846 		if (asprintf(&plumburi, "tn3270://%s%s%s:%s",
    847 		             item->selector, item->selector ? "@" : "",
    848 		             item->host, item->port) == -1)
    849 			return 0;
    850 		execuri(plumburi, "Plumbed", plumburi);
    851 		free(plumburi);
    852 		return 0;
    853 	default:
    854 		if (t >= '0' && t <= 'Z') {
    855 			diag("Type %c (%s) not supported", t, typedisplay(t));
    856 			return 0;
    857 		}
    858 	case 'g':
    859 	case 'I':
    860 		plumbitem(item);
    861 	case 'i':
    862 		return 0;
    863 	}
    864 
    865 	return item->type;
    866 }
    867 
    868 static char *
    869 searchselector(Item *item)
    870 {
    871 	char *pexp, *exp, *tag, *selector = item->selector;
    872 	size_t n = strlen(selector);
    873 
    874 	if ((tag = item->tag) && !strncmp(tag, selector, n))
    875 		pexp = tag + n+1;
    876 	else
    877 		pexp = "";
    878 
    879 	if (!(exp = uiprompt("Enter search string (^D cancel) [%s]: ", pexp)))
    880 		return NULL;
    881 
    882 	if (exp[0] && strcmp(exp, pexp)) {
    883 		n += strlen(exp) + 2;
    884 		tag = xmalloc(n);
    885 		snprintf(tag, n, "%s\t%s", selector, exp);
    886 	}
    887 
    888 	free(exp);
    889 	return tag;
    890 }
    891 
    892 static int
    893 searchitem(Item *entry, Item *item)
    894 {
    895 	char *sel, *selector;
    896 
    897 	if (!(sel = searchselector(item)))
    898 		return 0;
    899 
    900 	if (sel != item->tag)
    901 		clearitem(item);
    902 	if (!item->dat) {
    903 		selector = item->selector;
    904 		item->selector = item->tag = sel;
    905 		dig(entry, item);
    906 		item->selector = selector;
    907 	}
    908 	return (item->dat != NULL);
    909 }
    910 
    911 static void
    912 printout(Item *hole)
    913 {
    914 	char t = 0;
    915 
    916 	if (!hole)
    917 		return;
    918 
    919 	switch (hole->redtype ? hole->redtype : (t = hole->type)) {
    920 	case '0':
    921 		if (dig(hole, hole))
    922 			fputs(hole->raw, stdout);
    923 		return;
    924 	case '1':
    925 	case '7':
    926 		if (dig(hole, hole))
    927 			printdir(hole);
    928 		return;
    929 	default:
    930 		if (t >= '0' && t <= 'Z') {
    931 			diag("Type %c (%s) not supported", t, typedisplay(t));
    932 			return;
    933 		}
    934 	case '4':
    935 	case '5':
    936 	case '6':
    937 	case '9':
    938 	case 'g':
    939 	case 'I':
    940 		download(hole, 1);
    941 	case '2':
    942 	case '3':
    943 	case '8':
    944 	case 'T':
    945 		return;
    946 	}
    947 }
    948 
    949 static void
    950 delve(Item *hole)
    951 {
    952 	Item *entry = NULL;
    953 
    954 	while (hole) {
    955 		switch (hole->redtype ? hole->redtype : hole->type) {
    956 		case 'h':
    957 		case '0':
    958 			if (dig(entry, hole))
    959 				displaytextitem(hole);
    960 			break;
    961 		case '1':
    962 		case '+':
    963 			if (dig(entry, hole) && hole->dat)
    964 				entry = hole;
    965 			break;
    966 		case '7':
    967 			if (searchitem(entry, hole))
    968 				entry = hole;
    969 			break;
    970 		case 0:
    971 			diag("Couldn't get %s:%s/%c%s", hole->host,
    972 			     hole->port, hole->type, hole->selector);
    973 			break;
    974 		case '4':
    975 		case '5':
    976 		case '6': /* TODO decode? */
    977 		case '8':
    978 		case '9':
    979 		case 'g':
    980 		case 'I':
    981 		case 'T':
    982 		default:
    983 			dig(entry, hole);
    984 			break;
    985 		}
    986 
    987 		if (!entry)
    988 			return;
    989 
    990 		do {
    991 			uidisplay(entry);
    992 			hole = uiselectitem(entry);
    993 		} while (hole == entry);
    994 	}
    995 }
    996 
    997 static Item *
    998 moldentry(char *url)
    999 {
   1000 	Item *entry;
   1001 	char *p, *host = url, *port = "70", *gopherpath = "1";
   1002 	int parsed, ipv6;
   1003 
   1004 	host = ioparseurl(url);
   1005 
   1006 	if (*host == '[') {
   1007 		ipv6 = 1;
   1008 		++host;
   1009 	} else {
   1010 		ipv6 = 0;
   1011 	}
   1012 
   1013 	for (parsed = 0, p = host; !parsed && *p; ++p) {
   1014 		switch (*p) {
   1015 		case ']':
   1016 			if (ipv6) {
   1017 				*p = '\0';
   1018 				ipv6 = 0;
   1019 			}
   1020 			continue;
   1021 		case ':':
   1022 			if (!ipv6) {
   1023 				*p = '\0';
   1024 				port = p+1;
   1025 			}
   1026 			continue;
   1027 		case '/':
   1028 			*p = '\0';
   1029 			parsed = 1;
   1030 			continue;
   1031 		}
   1032 	}
   1033 
   1034 	if (*host == '\0' || *port == '\0' || ipv6)
   1035 		die("Can't parse url");
   1036 
   1037 	if (*p != '\0')
   1038 		gopherpath = p;
   1039 
   1040 	entry = xcalloc(sizeof(Item));
   1041 	entry->type = gopherpath[0];
   1042 	entry->username = entry->selector = ++gopherpath;
   1043 	if (entry->type == '7') {
   1044 		if (p = strstr(gopherpath, "%09")) {
   1045 			memmove(p+1, p+3, strlen(p+3)+1);
   1046 			*p = '\t';
   1047 		}
   1048 		if (p || (p = strchr(gopherpath, '\t'))) {
   1049 			asprintf(&entry->tag, "%s", gopherpath);
   1050 			*p = '\0';
   1051 		}
   1052 	}
   1053 	entry->host = host;
   1054 	entry->port = port;
   1055 	entry->entry = entry;
   1056 
   1057 	return entry;
   1058 }
   1059 
   1060 static void
   1061 cleanup(void)
   1062 {
   1063 	clearitem(mainentry);
   1064 	if (parent)
   1065 		rmdir(tmpdir);
   1066 	free(mainentry);
   1067 	free(mainurl);
   1068 	if (interactive)
   1069 		uicleanup();
   1070 }
   1071 
   1072 static void
   1073 sighandler(int signo)
   1074 {
   1075 	exit(128 + signo);
   1076 }
   1077 
   1078 static void
   1079 setup(void)
   1080 {
   1081 	struct sigaction sa;
   1082 	int fd;
   1083 
   1084 	setlocale(LC_CTYPE, "");
   1085 	setenv("PAGER", "more", 0);
   1086 	atexit(cleanup);
   1087 	/* reopen stdin in case we're reading from a pipe */
   1088 	if ((fd = open("/dev/tty", O_RDONLY)) == -1)
   1089 		die("open: /dev/tty: %s", strerror(errno));
   1090 	if (dup2(fd, 0) == -1)
   1091 		die("dup2: /dev/tty, stdin: %s", strerror(errno));
   1092 	close(fd);
   1093 	if ((devnullfd = open("/dev/null", O_WRONLY)) == -1)
   1094 		die("open: /dev/null: %s", strerror(errno));
   1095 
   1096 	sigemptyset(&sa.sa_mask);
   1097 	sa.sa_flags = SA_RESTART;
   1098 	sa.sa_handler = sighandler;
   1099 	sigaction(SIGINT, &sa, NULL);
   1100 	sigaction(SIGHUP, &sa, NULL);
   1101 	sigaction(SIGTERM, &sa, NULL);
   1102 
   1103 	sa.sa_handler = SIG_IGN;
   1104 	sigaction(SIGCHLD, &sa, NULL);
   1105 
   1106 	if (!mkdtemp(tmpdir))
   1107 		die("mkdir: %s: %s", tmpdir, strerror(errno));
   1108 	if (interactive = isatty(1)) {
   1109 		uisetup();
   1110 		diag = uistatus;
   1111 		sa.sa_handler = uisigwinch;
   1112 		sigaction(SIGWINCH, &sa, NULL);
   1113 	} else {
   1114 		diag = stddiag;
   1115 	}
   1116 	iosetup();
   1117 }
   1118 
   1119 int
   1120 main(int argc, char *argv[])
   1121 {
   1122 	if (argc != 2)
   1123 		usage();
   1124 
   1125 	setup();
   1126 
   1127 	mainurl = xstrdup(argv[1]);
   1128 	mainentry = moldentry(mainurl);
   1129 
   1130 	if (interactive)
   1131 		delve(mainentry);
   1132 	else
   1133 		printout(mainentry);
   1134 
   1135 	exit(0);
   1136 }