#include #include #include #include #include #include #include #include #include #define ARENA_IMPL #define STR_IMPL #include "dynarr.h" #include "arena.h" #include "str.h" /* utils */ Str str_fmt(Arena *a, const char *fmt, ...) { va_list ap; va_start(ap, fmt); int n = vsnprintf(NULL, 0, fmt, ap); va_end(ap); va_start(ap, fmt); char *s = new_arr(a, char, n); vsprintf(s, fmt, ap); va_end(ap); return (Str) { s, n }; } /* address cache */ #define ADDR_MAP_MAX 128 #define ADDR_HOST_MAX 128 typedef struct { char host[ADDR_HOST_MAX]; int hostn, port; struct addrinfo *ai; } AddrMap; static AddrMap addr_mapv[ADDR_MAP_MAX]; static int addr_mapc = 0; struct addrinfo * addr_get(Str host, int port, Arena *scratch) { if (host.n > ADDR_HOST_MAX) { fprintf(stderr, "hostname too long\n"); return NULL; } /* TODO: maybe move fetched addr to front of table? */ for (int i = 0; i < addr_mapc; i++) { AddrMap *m = &addr_mapv[i]; printf("%.*s:%d = %.*s:%d?\n", (int)m->hostn, m->host, m->port, (int)host.n, host.s, port); if (port == m->port && str_eql((Str) { m->host, m->hostn }, host)) return m->ai; } printf("lookup %.*s\n", (int)host.n, host.s); struct addrinfo *ai; int e = getaddrinfo(str_to_cstr(host, scratch), str_to_cstr(str_fmt(scratch, "%d", port), scratch), &(struct addrinfo) { .ai_family = AF_INET, .ai_socktype = SOCK_STREAM, .ai_protocol = IPPROTO_TCP, .ai_flags = AI_NUMERICSERV }, &ai); if (e) { fprintf(stderr, "bad address: %s\n", gai_strerror(e)); return NULL; } if (addr_mapc == ADDR_MAP_MAX) { freeaddrinfo(addr_mapv[0].ai); for (int i = 1; i < ADDR_MAP_MAX; i++) addr_mapv[i-1] = addr_mapv[i]; addr_mapc--; } AddrMap *m = &addr_mapv[addr_mapc++]; memcpy(m->host, host.s, host.n); m->hostn = host.n; m->port = port; m->ai = ai; return ai; } void addr_init(void) { /* do nothing */ } void addr_fini(void) { for (int i = 0; i < addr_mapc; i++) freeaddrinfo(addr_mapv[i].ai); } int addr_conn(Str host, int port, Arena *scratch) { struct addrinfo *ai = addr_get(host, port, scratch); if (!ai) return -1; int s = socket(AF_INET, SOCK_STREAM, 0); if (s == -1) { fprintf(stderr, "couldn't create socket\n"); return -1; } if (connect(s, ai->ai_addr, ai->ai_addrlen) < 0) { fprintf(stderr, "couldn't connect to host\n"); close(s); return -1; } return s; } /* url parsing */ #define PROTO_DEFAULT PROT_GOPHER #define PORT_DEFAULT 70 typedef enum { PROT_UNKNOWN, PROT_GOPHER, PROT_FILE, } Proto; typedef enum { GI_TEXT, GI_MENU, GI_SEARCH, GI_UNSUPPORTED, } GopherItemType; typedef struct { Str host, path; Proto proto; int port; } Request; int parse_port(Str s) { int x = 0; for (int i = 0; i < s.n; i++) { if (s.s[i] < '0' || s.s[i] > '9') return -1; if (x >= INT_MAX / 10 - 1) return -1; x = (x * 10) + (s.s[i] - '0'); } return x; } int parse_request(Str url, Request *req) { int i; static struct { Str s; Proto p; int port; } prot_tbl[] = { { Ss("gopher"), PROT_GOPHER, 70 }, { Ss("file"), PROT_FILE, 0 }, }; /* protocol */ req->proto = PROT_UNKNOWN; Cut cprot = str_cuts(url, S("://")); if (cprot.tail.n > 0) { for (i = 0; i < sizeof prot_tbl / sizeof *prot_tbl; i++) { if (str_eql(cprot.head, prot_tbl[i].s)) { req->proto = prot_tbl[i].p; req->port = prot_tbl[i].port; } } url = cprot.tail; switch (req->proto) { case PROT_UNKNOWN: fprintf(stderr, "bad protocol\n"); return -1; case PROT_FILE: req->path = url; return 0; default: break; } } else { req->proto = PROTO_DEFAULT; req->port = PORT_DEFAULT; } /* host & port */ Cut chost = str_cut(url, '/'); Cut cport = str_cut(chost.head, ':'); req->path = chost.tail; req->host = cport.head; if (cport.tail.n > 0) { req->port = parse_port(cport.tail); if (req->port == -1) { fprintf(stderr, "bad port\n"); return -1; } } return 0; } /* documents */ typedef enum { DOC_TEXT, DOC_GOPHERMAP, DOC_ERROR, DOC_UNKNOWN } DocType; typedef struct DocLine { struct DocLine *next, *prev; Str s; int link; } DocLine; typedef DYNARR(Str) DocLinks; typedef struct { Arena arena; DocType type; Str src; DocLine *head, *tail; DocLinks links; } Doc; /* document parsing */ DocLine * doc_push_line(Doc *d, Str s) { DocLine *n = new(&d->arena, DocLine); n->s = s; n->prev = d->tail; if (d->tail) d->tail->next = n; if (!d->head) d->head = n; d->tail = n; return n; } void doc_link(Doc *d, DocLine *l, Str url) { DA_APUSH(&d->links, &d->arena, url); l->link = d->links.n; } int parse_linewise(Doc *d, int (*fn)(Doc *, Str)) { Str s = d->src; while (s.n > 0) { Cut c = str_cut(s, '\n'); if (fn(d, str_trim(c.head))) return -1; s = c.tail; } return 0; } int parse_text_line(Doc *d, Str s) { doc_push_line(d, s); return 0; } int parse_gophermap_line(Doc *d, Str s) { if (!s.n) { doc_push_line(d, s); return 0; } if (str_eql(s, S("."))) return 0; char item = s.s[0]; s = str_skip(s, 1); Cut cdsp = str_cut(s, '\t'); Str dsp = cdsp.head; s = cdsp.tail; Cut csel = str_cut(s, '\t'); Str sel = csel.head; s = csel.tail; Cut chost = str_cut(s, '\t'); Str host = chost.head; int port = parse_port(chost.tail); if (port == -1) { fprintf(stderr, "bad port '%.*s'\n", (int)chost.tail.n, chost.tail.s); return -1; } DocLine *l = doc_push_line(d, dsp); if (item != 'i') { Str url = str_fmt(&d->arena, "gopher://%.*s:%d/%c%.*s", (int)host.n, host.s, port, item, (int)sel.n, sel.s); doc_link(d, l, url); } return 0; } int parse_doc(Doc *d) { switch (d->type) { case DOC_TEXT: return parse_linewise(d, parse_text_line); case DOC_GOPHERMAP: return parse_linewise(d, parse_gophermap_line); case DOC_ERROR: doc_push_line(d, S("Error")); return -1; default: break; } doc_push_line(d, S("Unknown document type")); return -1; } /* document fetching */ ssize_t write_str(int fd, Str s) { return write(fd, s.s, s.n); } Str read_all(int fd, Arena *a) { DYNARR(char) r = { 0 }; for (;;) { DA_AFIT(&r, a, r.n + 1024); ssize_t n = read(fd, r.v + r.n, r.c_ - r.n); if (n <= 0) break; r.n += n; } return (Str) { r.v, r.n }; } DocType fetch_gopher(Str *buf, Request req, Arena *perm, Arena *scratch) { DocType t = DOC_UNKNOWN; if (req.path.n == 0) { t = DOC_GOPHERMAP; } else { switch (req.path.s[0]) { case '0': t = DOC_TEXT; break; case '1': t = DOC_GOPHERMAP; break; default: /* don't bother fetching */ *buf = S(""); return DOC_UNKNOWN; } req.path = str_skip(req.path, 1); } int s = addr_conn(req.host, req.port, scratch); if (s == -1) return DOC_ERROR; write_str(s, req.path); write_str(s, S("\r\n")); *buf = read_all(s, perm); close(s); return t; } DocType fetch_file(Str *buf, Request req, Arena *perm, Arena *scratch) { int f = open(str_to_cstr(req.path, scratch), O_RDONLY); if (f == -1) return DOC_ERROR; *buf = read_all(f, perm); close(f); return DOC_TEXT; } DocType fetch(Str *buf, Request req, Arena *perm, Arena *scratch) { switch (req.proto) { case PROT_GOPHER: return fetch_gopher(buf, req, perm, scratch); case PROT_FILE: return fetch_file(buf, req, perm, scratch); default: *buf = S(""); fprintf(stderr, "unknown type to fetch file\n"); return DOC_ERROR; } } /* navigation */ #define HIST_MAX 64 typedef struct { Doc doc; DocLine *ln; } NavDoc; typedef struct { NavDoc histv[HIST_MAX]; int histc, histi; } NavState; void nav_free_doc(NavDoc *nd) { arena_free(&nd->doc.arena); } void nav_init(NavState *ns) { memset(ns, 0, sizeof(NavState)); } void nav_fini(NavState *ns) { for (int i = 0; i < ns->histc; i++) nav_free_doc(&ns->histv[i]); } int nav_to(NavState *ns, Str url, Arena *scratch) { Request req; if (parse_request(url, &req)) { fprintf(stderr, "bad request\n"); return -1; } Arena a = { 0 }; Str buf = { 0 }; arena_reserve(&a, 1L << 20); DocType t = fetch(&buf, req, &a, scratch); if (t == DOC_ERROR) { arena_free(&a); fprintf(stderr, "fetch failed\n"); return -1; } Doc d = { .arena = a, .src = buf, .type = t, }; if (parse_doc(&d)) { arena_free(&a); fprintf(stderr, "parse error\n"); return -1; } while (ns->histc > ns->histi + 1) nav_free_doc(&ns->histv[--ns->histc]); if (ns->histc == HIST_MAX) { nav_free_doc(&ns->histv[0]); for (int i = 1; i < ns->histc; i++) ns->histv[i-1] = ns->histv[i]; ns->histc--; } if (ns->histi < ns->histc) ns->histi++; ns->histv[ns->histi] = (NavDoc) { .doc = d, .ln = d.head }; ns->histc = ns->histi + 1; return 0; } int nav_link(NavState *ns, int link, Arena *scratch) { if (!ns->histc) return -1; NavDoc *nd = &ns->histv[ns->histi]; if (link < 1 || link > nd->doc.links.n) return -1; write_str(STDOUT_FILENO, nd->doc.links.v[link - 1]); puts(""); return nav_to(ns, nd->doc.links.v[link - 1], scratch); } void term_size(int *rows, int *cols) { struct winsize sz = { 0 }; ioctl(STDOUT_FILENO, TIOCGWINSZ, &sz); if (rows) *rows = sz.ws_row; if (cols) *cols = sz.ws_col; } void nav_putln(DocLine *l) { if (l->link) printf("[%d]", l->link); printf("\t%.*s\n", (int)l->s.n, l->s.s); } void nav_pgdn(NavState *ns) { if (!ns->histc) { puts("(no document)"); return; } int rows; term_size(&rows, NULL); NavDoc *nd = &ns->histv[ns->histi]; for (int i = 0; i < rows - 1; i++) { nav_putln(nd->ln); if (!nd->ln->next) break; nd->ln = nd->ln->next; } } void nav_pgup(NavState *ns) { if (!ns->histc) { puts("(no document)"); return; } int rows; term_size(&rows, NULL); NavDoc *nd = &ns->histv[ns->histi]; for (int i = 0; i < 2 * (rows - 1); i++) { if (!nd->ln->prev) break; nd->ln = nd->ln->prev; } nav_pgdn(ns); } void nav_repg(NavState *ns) { if (!ns->histc) { puts("(no document)"); return; } int rows; term_size(&rows, NULL); NavDoc *nd = &ns->histv[ns->histi]; for (int i = 0; i < (rows - 1); i++) { if (!nd->ln->prev) break; nd->ln = nd->ln->prev; } nav_pgdn(ns); } void nav_prev(NavState *ns) { if (ns->histi == 0) { puts("(at beginning)"); return; } ns->histi--; nav_repg(ns); } void nav_next(NavState *ns) { if (ns->histi + 1 >= ns->histc) { puts("(at end)"); return; } ns->histi++; nav_repg(ns); } void nav_prompt(NavState *ns) { if (ns->histi < ns->histc && ns->histv[ns->histi].ln->next) printf("MORE"); printf("* "); fflush(stdout); } int main(void) { Arena scratch = { 0 }; NavState ns = { 0 }; arena_reserve(&scratch, 1L << 20); addr_init(); if (nav_to(&ns, S("gopher://tilde.town"), &scratch)) { fprintf(stderr, "failed to load homepage\n"); goto err; } nav_pgdn(&ns); while (!feof(stdin)) { arena_reset(&scratch); nav_prompt(&ns); char buf[1024] = { 0 }; fgets(buf, sizeof(buf) - 1, stdin); Str s = str_trim(str_from_cstr(buf)); if (s.n == 0) s = S("f"); switch (s.s[0]) { case 'f': nav_pgdn(&ns); break; case 'b': nav_pgup(&ns); break; case 'r': nav_repg(&ns); break; case 'p': nav_prev(&ns); break; case 'n': nav_next(&ns); break; case 'q': goto ok; case 'g': if (nav_to(&ns, str_skip(s, 1), &scratch)) puts("failed"); else nav_pgdn(&ns); break; case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': { int p = parse_port(s); if (p == -1) { puts("invalid link number!"); } else { if (nav_link(&ns, p, &scratch)) puts("failed"); else nav_pgdn(&ns); } break; default: puts("?"); break; } } } ok: puts("Goodbye!"); addr_fini(); return 0; err: puts("Error occurred. Aborting."); addr_fini(); return 1; }