commit 5e11c00c40910bea9deb4b2199868f6dde63798e from: Omar Polo date: Tue Mar 02 13:35:07 2021 UTC initial commit commit - /dev/null commit + 5e11c00c40910bea9deb4b2199868f6dde63798e blob - /dev/null blob + 2af19fe67c4e2e1d0c6b2c91d6cd44191cfe1a44 (mode 644) --- /dev/null +++ .gitignore @@ -0,0 +1,15 @@ +TAGS +.deps +Makefile +Makefile.in +aclocal.m4 +autom4te.cache +compile +config.* +configure +depcomp +install-sh +missing +stamp-h1 +*.o +telescope blob - /dev/null blob + a47df7ecbb01ead9247b223e059a1ec95dbbefb5 (mode 644) --- /dev/null +++ Makefile.am @@ -0,0 +1,6 @@ +bin_PROGRAMS = telescope +telescope_SOURCES = telescope.c gemini.c gemtext.c url.c util.c about.c ui.c + +dist_doc_DATA = README.md + +AM_LDFLAGS = ${IMSG_LDFLAGS} blob - /dev/null blob + 03d13c1fc94f2b6c495979a17bcb8d3629e35af5 (mode 644) --- /dev/null +++ README.md @@ -0,0 +1,3 @@ +# Telescope + +Telescope is a w3m-like browser for Gemini. blob - /dev/null blob + a9e8f0bb56cd5226b87d4bef667d9269adf52a6e (mode 644) --- /dev/null +++ about.c @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 Omar Polo + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#define ASCII_ART \ + "```An Ascii art of the word \"Telescope\"\n" \ + " _______ __\n" \ + "|_ _|.-----.| |.-----.-----.----.-----.-----.-----.\n" \ + " | | | -__|| || -__|__ --| __| _ | _ | -__|\n" \ + " |___| |_____||__||_____|_____|____|_____| __|_____|\n" \ + " |__|\n" \ + "```\n" + +const char *about_new = + ASCII_ART + "\n" + "Version: " VERSION "\n" + "Bug reports to: " PACKAGE_BUGREPORT "\n" + "=> " PACKAGE_URL " Telescope Gemini site: " PACKAGE_URL "\n" + "\n" + "*test\n" + ">quote\n" + ; blob - /dev/null blob + 628c689871163f5bcb8d7d8e26aec2a9d144692f (mode 755) --- /dev/null +++ autogen.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exec autoreconf --install blob - /dev/null blob + 8fd6b69aad5ec4b5c2f16a83d35000248847ade6 (mode 644) --- /dev/null +++ configure.ac @@ -0,0 +1,30 @@ +AC_INIT([telescope], [0.1], [telescope@omarpolo.com], [telescope], [gemini://telescope.omarpolo.com]) +AM_INIT_AUTOMAKE([-Wall foreign]) +AC_PROG_CC + +PKG_PROG_PKG_CONFIG + +dnl AX_WITH_CURSES +dnl if test "x$ax_cv_ncursesw" != xyes && test "x$ax_cv_ncurses" != xyes; then +dnl AC_MSG_ERROR([requires either NcursesW or Ncurses library]) +dnl fi + +AC_CHECK_LIB(ncursesw, initscr, [], + AC_CHECK_LIB(ncurses, initscr, [], + AC_MSG_ERROR([requires either ncursesw or ncurses library]))) + +AC_CHECK_LIB(tls, tls_init, [], + AC_MSG_ERROR([requires libtls])) + +AC_CHECK_LIB(event, event_init, [], + AC_MSG_ERROR([requires libevent])) + +IMSG_LDFLAGS=-lutil +AC_SUBST([IMSG_LDFLAGS]) + +AC_CONFIG_HEADERS([config.h]) +AC_CONFIG_FILES([ + Makefile +]) + +AC_OUTPUT blob - /dev/null blob + f510a44129c6f2446a709f1546452e51701bbe85 (mode 644) --- /dev/null +++ gemini.c @@ -0,0 +1,530 @@ +/* + * Copyright (c) 2021 Omar Polo + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * TODO: + * - move the various + * imsg_compose(...); + * imsg_flush(...); + * to something more asynchronous + */ + +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static struct tls_config *tlsconf; +static struct imsgbuf *ibuf; + +struct req; + +static void die(void) __attribute__((__noreturn__)); +static char *xasprintf(const char*, ...); +static int conn_towards(struct url*, char**); + +static void close_with_err(struct req*, const char *err); +static struct req *req_by_id(uint32_t); + +static void do_handshake(int, short, void*); +static void write_request(int, short, void*); +static void read_reply(int, short, void*); +static void parse_reply(struct req*); +static void copy_body(int, short, void*); + +static void check_special_page(struct req*, const char*); + +static void handle_get(struct imsg*, size_t); +static void handle_cert_status(struct imsg*, size_t); +static void handle_stop(struct imsg*, size_t); +static void handle_quit(struct imsg*, size_t); + +static imsg_handlerfn *handlers[] = { + [IMSG_GET] = handle_get, + [IMSG_CERT_STATUS] = handle_cert_status, + [IMSG_STOP] = handle_stop, + [IMSG_QUIT] = handle_quit, +}; + +typedef void (*statefn)(int, short, void*); + +TAILQ_HEAD(, req) reqhead; +/* a pending request */ +struct req { + struct event ev; + struct url url; + uint32_t id; + int fd; + struct tls *ctx; + char buf[1024]; + size_t off; + TAILQ_ENTRY(req) reqs; +}; + +static inline void +yield_r(struct req *req, statefn fn, struct timeval *tv) +{ + event_once(req->fd, EV_READ, fn, req, tv); +} + +static inline void +yield_w(struct req *req, statefn fn, struct timeval *tv) +{ + event_once(req->fd, EV_WRITE, fn, req, tv); +} + +static inline void +advance_buf(struct req *req, size_t len) +{ + assert(len <= req->off); + + req->off -= len; + memmove(req->buf, req->buf + len, req->off); +} + +static void __attribute__((__noreturn__)) +die(void) +{ + abort(); /* TODO */ +} + +static char * +xasprintf(const char *fmt, ...) +{ + va_list ap; + char *s; + + va_start(ap, fmt); + if (vasprintf(&s, fmt, ap) == -1) + s = NULL; + va_end(ap); + + return s; +} + +static int +conn_towards(struct url *url, char **err) +{ + struct addrinfo hints, *servinfo, *p; + int status, sock; + const char *proto = "1965"; + + *err = NULL; + + if (*url->port != '\0') + proto = url->port; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + if ((status = getaddrinfo(url->host, proto, &hints, &servinfo))) { + *err = xasprintf("failed to resolve %s: %s", + url->host, gai_strerror(status)); + return -1; + } + + sock = -1; + for (p = servinfo; p != NULL; p = p->ai_next) { + if ((sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) + continue; + if (connect(sock, p->ai_addr, p->ai_addrlen) != -1) + break; + close(sock); + } + + if (sock == -1) + *err = xasprintf("couldn't connect to %s", url->host); + else + mark_nonblock(sock); + + freeaddrinfo(servinfo); + return sock; +} + +static struct req * +req_by_id(uint32_t id) +{ + struct req *r; + + TAILQ_FOREACH(r, &reqhead, reqs) { + if (r->id == id) + return r; + } + + die(); +} + +static void +close_conn(int fd, short ev, void *d) +{ + struct req *req = d; + + if (req->ctx != NULL) { + switch (tls_close(req->ctx)) { + case TLS_WANT_POLLIN: + yield_r(req, close_conn, NULL); + return; + case TLS_WANT_POLLOUT: + yield_w(req, close_conn, NULL); + return; + } + + tls_free(req->ctx); + } + + TAILQ_REMOVE(&reqhead, req, reqs); + if (req->fd != -1) + close(req->fd); + free(req); +} + +static void +close_with_err(struct req *req, const char *err) +{ + imsg_compose(ibuf, IMSG_ERR, req->id, 0, -1, err, strlen(err)+1); + imsg_flush(ibuf); + close_conn(0, 0, req); +} + +static void +do_handshake(int fd, short ev, void *d) +{ + struct req *req = d; + const char *hash; + + switch (tls_handshake(req->ctx)) { + case TLS_WANT_POLLIN: + yield_r(req, do_handshake, NULL); + return; + case TLS_WANT_POLLOUT: + yield_w(req, do_handshake, NULL); + return; + } + + hash = tls_peer_cert_hash(req->ctx); + imsg_compose(ibuf, IMSG_CHECK_CERT, req->id, 0, -1, hash, strlen(hash)+1); + imsg_flush(ibuf); +} + +static void +write_request(int fd, short ev, void *d) +{ + struct req *req = d; + ssize_t r; + size_t len; + char buf[1024], *err; + + strlcpy(buf, "gemini://", sizeof(buf)); + strlcat(buf, req->url.host, sizeof(buf)); + strlcat(buf, "/", sizeof(buf)); + strlcat(buf, req->url.path, sizeof(buf)); + + if (req->url.query[0] != '\0') { + strlcat(buf, "?", sizeof(buf)); + strlcat(buf, req->url.query, sizeof(buf)); + } + + len = strlcat(buf, "\r\n", sizeof(buf)); + + assert(len <= sizeof(buf)); + + switch (r = tls_write(req->ctx, buf, len)) { + case -1: + err = xasprintf("tls_write: %s", tls_error(req->ctx)); + close_with_err(req, err); + free(err); + break; + case TLS_WANT_POLLIN: + yield_r(req, write_request, NULL); + break; + case TLS_WANT_POLLOUT: + yield_w(req, write_request, NULL); + break; + default: + /* assume r == len */ + (void)r; + yield_r(req, read_reply, NULL); + break; + } +} + +static void +read_reply(int fd, short ev, void *d) +{ + struct req *req = d; + size_t len; + ssize_t r; + char *buf, *e; + + buf = req->buf + req->off; + len = sizeof(req->buf) - req->off; + + switch (r = tls_read(req->ctx, buf, len)) { + case -1: + e = xasprintf("tls_read: %s", tls_error(req->ctx)); + close_with_err(req, e); + free(e); + break; + case TLS_WANT_POLLIN: + yield_r(req, read_reply, NULL); + break; + case TLS_WANT_POLLOUT: + yield_w(req, read_reply, NULL); + break; + default: + req->off += r; + + /* TODO: really watch for \r\n not \n alone */ + if ((e = telescope_strnchr(req->buf, '\n', req->off)) != NULL) + parse_reply(req); + else if (req->off == sizeof(req->buf)) + close_with_err(req, "invalid response"); + else + yield_r(req, read_reply, NULL); + break; + } +} + +static void +parse_reply(struct req *req) +{ + int code; + char *e; + size_t len; + + if (req->off < 4) + goto err; + + if (!isdigit(req->buf[0]) || !isdigit(req->buf[1])) + goto err; + + code = (req->buf[0] - '0')*10 + (req->buf[1] - '0'); + + if (!isspace(req->buf[2])) + goto err; + + advance_buf(req, 3); + if ((e = telescope_strnchr(req->buf, '\r', req->off)) == NULL) + goto err; + + *e = '\0'; + e++; + len = e - req->buf; + imsg_compose(ibuf, IMSG_GOT_CODE, req->id, 0, -1, &code, sizeof(code)); + imsg_compose(ibuf, IMSG_GOT_META, req->id, 0, -1, + req->buf, len); + imsg_flush(ibuf); + + yield_r(req, copy_body, NULL); + return; + +err: + close_with_err(req, "malformed request"); +} + +static void +copy_body(int fd, short ev, void *d) +{ + struct req *req = d; + char buf[BUFSIZ]; + ssize_t r; + + for (;;) { + switch (r = tls_read(req->ctx, buf, sizeof(buf))) { + case TLS_WANT_POLLIN: + yield_r(req, copy_body, NULL); + return; + case TLS_WANT_POLLOUT: + yield_w(req, copy_body, NULL); + return; + case -1: + case 0: + imsg_compose(ibuf, IMSG_EOF, req->id, 0, -1, NULL, 0); + imsg_flush(ibuf); + close_conn(0, 0, req); + return; + default: + imsg_compose(ibuf, IMSG_BUF, req->id, 0, -1, buf, r); + imsg_flush(ibuf); + break; + } + } +} + +static int +serve_special_page(struct req *req, const char *url) +{ + int code = 20; + const char *meta = "text/gemini"; + + if (strcmp(url, "about:new")) + return 0; + + imsg_compose(ibuf, IMSG_GOT_CODE, req->id, 0, -1, &code, sizeof(code)); + imsg_compose(ibuf, IMSG_GOT_META, req->id, 0, -1, meta, strlen(meta)+1); + imsg_compose(ibuf, IMSG_BUF, req->id, 0, -1, about_new, strlen(about_new)); + imsg_compose(ibuf, IMSG_EOF, req->id, 0, -1, NULL, 0); + imsg_flush(ibuf); + + /* don't close_page here, since req is not added to requests + * queue */ + free(req); + return 1; +} + +static void +handle_get(struct imsg *imsg, size_t datalen) +{ + struct req *req; + const char *e; + char *data, *err = NULL; + + data = imsg->data; + + if (data[datalen-1] != '\0') + die(); + + if ((req = calloc(1, sizeof(*req))) == NULL) + die(); + + req->id = imsg->hdr.peerid; + + if (serve_special_page(req, data)) + return; + + if (!url_parse(imsg->data, &req->url, &e)) { + fprintf(stderr, "failed to parse url: %s\n", e); + close_with_err(req, e); + return; + } + + if ((req->fd = conn_towards(&req->url, &err)) == -1) + goto err; + if ((req->ctx = tls_client()) == NULL) + goto err; + if (tls_configure(req->ctx, tlsconf) == -1) { + err = xasprintf("tls_configure: %s", tls_error(req->ctx)); + goto err; + } + if (tls_connect_socket(req->ctx, req->fd, req->url.host) == -1) { + err = xasprintf("tls_connect_socket: %s", tls_error(req->ctx)); + goto err; + } + + TAILQ_INSERT_HEAD(&reqhead, req, reqs); + yield_w(req, do_handshake, NULL); + return; + +err: + close_with_err(req, err); + free(err); +} + +static void +handle_cert_status(struct imsg *imsg, size_t datalen) +{ + struct req *req; + int is_ok; + + req = req_by_id(imsg->hdr.peerid); + + if (datalen < sizeof(is_ok)) + die(); + memcpy(&is_ok, imsg->data, sizeof(is_ok)); + + if (is_ok) + yield_w(req, write_request, NULL); + else + close_conn(0, 0, req); +} + +static void +handle_stop(struct imsg *imsg, size_t datalen) +{ + struct req *req; + + req = req_by_id(imsg->hdr.peerid); + close_conn(0, 0, req); +} + +static void +handle_quit(struct imsg *imsg, size_t datalen) +{ + event_loopbreak(); +} + +static void +dispatch_imsg(int fd, short ev, void *d) +{ + struct imsgbuf *ibuf = d; + struct imsg imsg; + ssize_t n; + size_t datalen; + + if ((n = imsg_read(ibuf)) == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + return; + die(); + } + + if (n == 0) + die(); + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + die(); + if (n == 0) + return; + datalen = imsg.hdr.len - IMSG_HEADER_SIZE; + handlers[imsg.hdr.type](&imsg, datalen); + imsg_free(&imsg); + } +} + +int +client_main(struct imsgbuf *b) +{ + ibuf = b; + + TAILQ_INIT(&reqhead); + + if ((tlsconf = tls_config_new()) == NULL) + die(); + tls_config_insecure_noverifycert(tlsconf); + + event_init(); + + event_set(&imsgev, ibuf->fd, EV_READ | EV_PERSIST, dispatch_imsg, ibuf); + event_add(&imsgev, NULL); + + event_dispatch(); + return 0; +} blob - /dev/null blob + b74c5ce7f4397b3d7d60c58c9180459d235feb23 (mode 644) --- /dev/null +++ gemtext.c @@ -0,0 +1,406 @@ +/* + * Copyright (c) 2021 Omar Polo + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * A streaming gemtext parser. + * + * TODO: + * - handle NULs + * - UTF8 + */ + +#include + +#include +#include +#include + +static int gemtext_parse(struct parser*, const char*, size_t); +static void gemtext_free(struct parser*); + +static int parse_text(struct parser*, enum line_type, const char*, size_t); +static int parse_link(struct parser*, enum line_type, const char*, size_t); +static int parse_title(struct parser*, enum line_type, const char*, size_t); +static int parse_item(struct parser*, enum line_type, const char*, size_t); +static int parse_quote(struct parser*, enum line_type, const char*, size_t); +static int parse_pre_start(struct parser*, enum line_type, const char*, size_t); +static int parse_pre_cnt(struct parser*, enum line_type, const char*, size_t); +static int parse_pre_end(struct parser*, enum line_type, const char*, size_t); + +typedef int (parselinefn)(struct parser*, enum line_type, const char*, size_t); + +static parselinefn *parsers[] = { + parse_text, /* LINE_TEXT */ + parse_link, /* LINE_LINK */ + parse_title, /* LINE_TITLE_1 */ + parse_title, /* LINE_TITLE_2 */ + parse_title, /* LINE_TITLE_3 */ + parse_item, /* LINE_ITEM */ + parse_quote, /* LINE_QUOTE */ + parse_pre_start, /* LINE_PRE_START */ + parse_pre_cnt, /* LINE_PRE_CONTENT */ + parse_pre_end, /* LINE_PRE_END */ +}; + +void +gemtext_initparser(struct parser *p) +{ + memset(p, 0, sizeof(*p)); + + p->parse = &gemtext_parse; + p->free = &gemtext_free; +} + +static inline int +emit_line(struct parser *p, enum line_type type, char *line, char *alt) +{ + struct line *l; + + if ((l = calloc(1, sizeof(*l))) == NULL) + return 0; + + l->type = type; + l->line = line; + l->alt = alt; + + if (TAILQ_EMPTY(&p->head)) + TAILQ_INSERT_HEAD(&p->head, l, lines); + else + TAILQ_INSERT_TAIL(&p->head, l, lines); + + return 1; +} + +static int +parse_text(struct parser *p, enum line_type t, const char *buf, size_t len) +{ + char *l; + + if ((l = calloc(1, len+1)) == NULL) + return 0; + memcpy(l, buf, len); + return emit_line(p, t, l, NULL); +} + +static int +parse_link(struct parser *p, enum line_type t, const char *buf, size_t len) +{ + char *l, *u; + const char *url_start; + + if (len <= 2) + return emit_line(p, t, NULL, NULL); + buf += 2; + len -= 2; + + while (len > 0 && isspace(buf[0])) { + buf++; + len--; + } + + if (len == 0) + return emit_line(p, t, NULL, NULL); + + url_start = buf; + while (len > 0 && !isspace(buf[0])) { + buf++; + len--; + } + + if ((u = calloc(1, buf - url_start + 1)) == NULL) + return 0; + memcpy(u, url_start, buf - url_start); + + if (len == 0) + return emit_line(p, t, u, NULL); + + while (len > 0) { + buf++; + len--; + } + + if (len == 0) + return emit_line(p, t, u, NULL); + + if ((l = calloc(1, len + 1)) == NULL) + return 0; + + memcpy(l, buf, len); + return emit_line(p, t, u, l); +} + +static int +parse_title(struct parser *p, enum line_type t, const char *buf, size_t len) +{ + char *l; + + switch (t) { + case LINE_TITLE_1: + if (len <= 1) + return emit_line(p, t, NULL, NULL); + buf++; + len--; + break; + case LINE_TITLE_2: + if (len <= 2) + return emit_line(p, t, NULL, NULL); + buf += 2; + len -= 2; + break; + case LINE_TITLE_3: + if (len <= 3) + return emit_line(p, t, NULL, NULL); + buf += 3; + len -= 3; + break; + default: + /* unreachable */ + abort(); + } + + while (len > 0 && isspace(buf[0])) { + buf++; + len--; + } + + if (len == 0) + return emit_line(p, t, NULL, NULL); + + if ((l = calloc(1, len+1)) == NULL) + return 0; + memcpy(l, buf, len); + return emit_line(p, t, l, NULL); +} + +static int +parse_item(struct parser *p, enum line_type t, const char *buf, size_t len) +{ + char *l; + + if (len == 1) + return emit_line(p, t, NULL, NULL); + + buf++; + len--; + + while (len > 0 && isspace(buf[0])) { + buf++; + len--; + } + + if (len == 0) + return emit_line(p, t, NULL, NULL); + + if ((l = calloc(1, len+1)) == NULL) + return 0; + memcpy(l, buf, len); + return emit_line(p, t, l, NULL); +} + +static int +parse_quote(struct parser *p, enum line_type t, const char *buf, size_t len) +{ + char *l; + + if (len == 1) + return emit_line(p, t, NULL, NULL); + + buf++; + len--; + + while (len > 0 && isspace(buf[0])) { + buf++; + len--; + } + + if (len == 0) + return emit_line(p, t, NULL, NULL); + + if ((l = calloc(1, len+1)) == NULL) + return 0; + memcpy(l, buf, len); + return emit_line(p, t, l, NULL); +} + +static int +parse_pre_start(struct parser *p, enum line_type t, const char *buf, size_t len) +{ + char *l; + + if (len <= 3) + return emit_line(p, t, NULL, NULL); + + buf += 3; + len += 3; + + while (len > 0 && isspace(buf[0])) { + buf++; + len--; + } + + if (len == 0) + return emit_line(p, t, NULL, NULL); + + if ((l = calloc(1, len+1)) == NULL) + return 0; + + memcpy(l, buf, len); + return emit_line(p, t, NULL, l); +} + +static int +parse_pre_cnt(struct parser *p, enum line_type t, const char *buf, size_t len) +{ + char *l; + + if (len == 0) + return emit_line(p, t, NULL, NULL); + + if ((l = calloc(1, len+1)) == NULL) + return 0; + memcpy(l, buf, len); + return emit_line(p, t, l, NULL); +} + +static int +parse_pre_end(struct parser *p, enum line_type t, const char *buf, size_t len) +{ + return emit_line(p, t, NULL, NULL); +} + +static inline enum line_type +detect_line_type(const char *buf, size_t len, int in_pre) +{ + size_t i; + + if (len == 0) + return LINE_TEXT; + + if (in_pre) { + if (len >= 3 && + buf[0] == '`' && buf[1] == '`' && buf[2] == '`') + return LINE_PRE_END; + else + return LINE_PRE_CONTENT; + } + + switch (*buf) { + case '*': return LINE_ITEM; + case '>': return LINE_QUOTE; + case '=': + if (len >= 1 && buf[1] == '>') + return LINE_LINK; + break; + case '#': + if (len == 1) + return LINE_TEXT; + if (buf[1] != '#') + return LINE_TITLE_1; + if (len == 2) + return LINE_TEXT; + if (buf[2] != '#') + return LINE_TITLE_2; + if (len == 3) + return LINE_TEXT; + return LINE_TITLE_3; + case '`': + if (len < 3) + return LINE_TEXT; + if (buf[0] == '`' && buf[1] == '`' && buf[2] == '`') + return LINE_PRE_START; + break; + } + + return LINE_TEXT; +} + +static inline int +append(struct parser *p, const char *buf, size_t len) +{ + size_t newlen; + char *t; + + newlen = len + p->len; + if ((t = calloc(1, newlen)) == NULL) + return 0; + free(p->buf); + p->buf = t; + p->len = newlen; + return 1; +} + +static inline int +set_buf(struct parser *p, const char *buf, size_t len) +{ + free(p->buf); + p->buf = NULL; + + if (len == 0) + return 1; + + if ((p->buf = calloc(1, len)) == NULL) + return 0; + memcpy(p->buf, buf, len); + return 1; +} + +static int +gemtext_parse(struct parser *p, const char *buf, size_t size) +{ + const char *b, *e; + enum line_type t; + size_t len, l; + + if (p->len == 0) { + b = buf; + len = size; + } else { + if (!append(p, buf, size)) + return 0; + b = p->buf; + len = p->len; + } + + while (len > 0) { + if ((e = telescope_strnchr((char*)b, '\n', len)) == NULL) + break; + l = e - b; + t = detect_line_type(b, l, p->flags); + if (t == LINE_PRE_START) + p->flags = 1; + if (t == LINE_PRE_END) + p->flags = 0; + if (!parsers[t](p, t, b, l)) + return 0; + + len -= l; + b += l; + + if (len > 0) { + /* skip \n */ + len--; + b++; + } + } + + return set_buf(p, b, len); +} + +static void +gemtext_free(struct parser *p) +{ + free(p->buf); +} blob - /dev/null blob + 6ff71d7a72cabe2d1707e64e327cfba3f77372a2 (mode 644) --- /dev/null +++ telescope.c @@ -0,0 +1,212 @@ +#include "telescope.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +struct event imsgev; +struct tabshead tabshead; + +static struct imsgbuf *ibuf; +static uint32_t tab_counter; + +static void handle_imsg_err(struct imsg*, size_t); +static void handle_imsg_check_cert(struct imsg*, size_t); +static void handle_imsg_got_code(struct imsg*, size_t); +static void handle_imsg_got_meta(struct imsg*, size_t); +static void handle_imsg_buf(struct imsg*, size_t); +static void handle_imsg_eof(struct imsg*, size_t); + +static imsg_handlerfn *handlers[] = { + [IMSG_ERR] = handle_imsg_err, + [IMSG_CHECK_CERT] = handle_imsg_check_cert, + [IMSG_GOT_CODE] = handle_imsg_got_code, + [IMSG_GOT_META] = handle_imsg_got_meta, + [IMSG_BUF] = handle_imsg_buf, + [IMSG_EOF] = handle_imsg_eof, +}; + +static void __attribute__((__noreturn__)) +die(void) +{ + abort(); /* TODO */ +} + +static struct tab * +tab_by_id(uint32_t id) +{ + struct tab *t; + + TAILQ_FOREACH(t, &tabshead, tabs) { + if (t->id == id) + return t; + } + + die(); +} + +static void +handle_imsg_err(struct imsg *imsg, size_t datalen) +{ + /* write(2, imsg->data, datalen); */ + /* fprintf(stderr, "\nEOF\n"); */ + /* event_loopbreak(); */ +} + +static void +handle_imsg_check_cert(struct imsg *imsg, size_t datalen) +{ + int tofu_res = 1; + + imsg_compose(ibuf, IMSG_CERT_STATUS, imsg->hdr.peerid, 0, -1, &tofu_res, sizeof(tofu_res)); + imsg_flush(ibuf); +} + +static void +handle_imsg_got_code(struct imsg *imsg, size_t datalen) +{ + int code; + + if (sizeof(code) != datalen) + die(); + + memcpy(&code, imsg->data, sizeof(code)); + + /* fprintf(stderr, "got status code: %d\n", code); */ +} + +static void +handle_imsg_got_meta(struct imsg *imsg, size_t datalen) +{ + /* fprintf(stderr, "got meta: "); */ + /* fflush(stderr); */ + /* write(2, imsg->data, datalen); */ + /* fprintf(stderr, "\n"); */ +} + +static void +handle_imsg_buf(struct imsg *imsg, size_t datalen) +{ + struct tab *t; + struct line *l; + + t = tab_by_id(imsg->hdr.peerid); + + if (!t->page.parse(&t->page, imsg->data, datalen)) + die(); + + ui_on_tab_refresh(t); +} + +static void +handle_imsg_eof(struct imsg *imsg, size_t datalen) +{ + /* printf("===== EOF\n"); */ + /* event_loopbreak(); */ +} + +static void +dispatch_imsg(int fd, short ev, void *d) +{ + struct imsg imsg; + size_t datalen; + ssize_t n; + + if ((n = imsg_read(ibuf)) == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + return; + die(); + } + + if (n == 0) { + fprintf(stderr, "other side is dead\n"); + exit(0); + } + + for (;;) { + if ((n = imsg_get(ibuf, &imsg)) == -1) + die(); + if (n == 0) + return; + datalen = imsg.hdr.len - IMSG_HEADER_SIZE; + handlers[imsg.hdr.type](&imsg, datalen); + imsg_free(&imsg); + } +} + +void +new_tab(void) +{ + struct tab *tab; + const char *url = "about:new"; + /* const char *url = "gemini://localhost/cgi/slow-out"; */ + + if ((tab = calloc(1, sizeof(*tab))) == NULL) + die(); + + TAILQ_INSERT_HEAD(&tabshead, tab, tabs); + + tab->id = tab_counter++; + TAILQ_INIT(&tab->page.head); + gemtext_initparser(&tab->page); + + imsg_compose(ibuf, IMSG_GET, tab->id, 0, -1, url, strlen(url)+1); + imsg_flush(ibuf); + + ui_on_new_tab(tab); +} + +int +main(void) +{ + struct imsgbuf main_ibuf, network_ibuf; + int imsg_fds[2]; + + signal(SIGCHLD, SIG_IGN); + signal(SIGINT, SIG_IGN); + + if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, imsg_fds) == -1) + err(1, "socketpair"); + + switch (fork()) { + case -1: + err(1, "fork"); + case 0: + /* child */ + setproctitle("client"); + close(imsg_fds[0]); + imsg_init(&network_ibuf, imsg_fds[1]); + exit(client_main(&network_ibuf)); + } + + close(imsg_fds[1]); + imsg_init(&main_ibuf, imsg_fds[0]); + ibuf = &main_ibuf; + + TAILQ_INIT(&tabshead); + + event_init(); + + event_set(&imsgev, ibuf->fd, EV_READ | EV_PERSIST, dispatch_imsg, ibuf); + event_add(&imsgev, NULL); + + ui_init(); + + new_tab(); + + event_dispatch(); + + imsg_compose(ibuf, IMSG_QUIT, 0, 0, -1, NULL, 0); + imsg_flush(ibuf); + + ui_end(); + + return 0; +} blob - /dev/null blob + 785fae6dcf27d3a61d967a2b59313424376458e8 (mode 644) --- /dev/null +++ telescope.h @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2021 Omar Polo + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef TELESCOPE_H +#define TELESCOPE_H + +#include +#include + +/* TODO: move in config.h */ +#include +#include +#include +#include +#include + +#define GEMINI_URL_LEN 1024 + +enum imsg_type { + IMSG_GET, /* data is URL, peerid the tab id */ + IMSG_ERR, + IMSG_CHECK_CERT, + IMSG_CERT_STATUS, + IMSG_GOT_CODE, + IMSG_GOT_META, + IMSG_BUF, + IMSG_EOF, + IMSG_STOP, + IMSG_QUIT, +}; + +enum line_type { + LINE_TEXT, + LINE_LINK, + LINE_TITLE_1, + LINE_TITLE_2, + LINE_TITLE_3, + LINE_ITEM, + LINE_QUOTE, + LINE_PRE_START, + LINE_PRE_CONTENT, + LINE_PRE_END, +}; + +struct line { + enum line_type type; + char *line; + char *alt; + TAILQ_ENTRY(line) lines; +}; + +struct parser; +struct page; + +/* typedef void (*initparserfn)(struct parser*); */ + +typedef int (*parsechunkfn)(struct parser*, const char*, size_t); +typedef void (*parserfreefn)(struct parser*); + +typedef void (imsg_handlerfn)(struct imsg*, size_t); + +struct parser { + char *buf; + size_t len; + size_t cap; + int flags; + parsechunkfn parse; + parserfreefn free; + + TAILQ_HEAD(, line) head; +}; + +extern TAILQ_HEAD(tabshead, tab) tabshead; +struct tab { + struct parser page; + TAILQ_ENTRY(tab) tabs; + uint32_t id; + uint32_t flags; +}; + +extern struct event imsgev; + +/* about.c */ +extern const char *about_new; + +/* gemini.c */ +int client_main(struct imsgbuf *b); + +/* gemtext.c */ +void gemtext_initparser(struct parser*); + +/* ui.c */ +int ui_init(void); +void ui_on_new_tab(struct tab*); +void ui_on_tab_refresh(struct tab*); +void ui_end(void); + +/* util.c */ +int mark_nonblock(int); +char *telescope_strnchr(char*, char, size_t); + +#endif /* TELESCOPE_H */ blob - /dev/null blob + 99baa1da940b355e5a271985a179ba5be3d50e8e (mode 644) --- /dev/null +++ ui.c @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2021 Omar Polo + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include +#include +#include + +#define TAB_CURRENT 0x1 + +static struct event stdioev, winchev; + +static struct tab *current_tab(void); +static void dispatch_stdio(int, short, void*); +static void handle_resize(int, short, void*); +static void redraw_tab(struct tab*); + +static struct tab * +current_tab(void) +{ + struct tab *t; + + TAILQ_FOREACH(t, &tabshead, tabs) { + if (t->flags & TAB_CURRENT) + return t; + } + + /* unreachable */ + abort(); +} + +static void +dispatch_stdio(int fd, short ev, void *d) +{ + int c; + + c = getch(); + + if (c == ERR) + return; + + if (c == 'q') { + event_loopbreak(); + return; + } + + printw("You typed %c\n", c); + refresh(); +} + +static void +handle_resize(int sig, short ev, void *d) +{ + endwin(); + refresh(); + clear(); + + redraw_tab(current_tab()); +} + +static void +redraw_tab(struct tab *tab) +{ + struct line *l; + + erase(); + + TAILQ_FOREACH(l, &tab->page.head, lines) { + switch (l->type) { + case LINE_TEXT: + printw("%s\n", l->line); + break; + case LINE_LINK: + printw("=> %s\n", l->line); + break; + case LINE_TITLE_1: + printw("# %s\n", l->line); + break; + case LINE_TITLE_2: + printw("## %s\n", l->line); + break; + case LINE_TITLE_3: + printw("### %s\n", l->line); + break; + case LINE_ITEM: + printw("* %s\n", l->line); + break; + case LINE_QUOTE: + printw("> %s\n", l->line); + break; + case LINE_PRE_START: + case LINE_PRE_END: + printw("```\n"); + break; + case LINE_PRE_CONTENT: + printw("`%s\n", l->line); + break; + } + } + + refresh(); +} + +int +ui_init(void) +{ + setlocale(LC_ALL, ""); + + initscr(); + cbreak(); + noecho(); + + nonl(); + intrflush(stdscr, FALSE); + keypad(stdscr, TRUE); + + /* non-blocking input */ + timeout(0); + + mvprintw(0, 0, ""); + + event_set(&stdioev, 0, EV_READ | EV_PERSIST, dispatch_stdio, NULL); + event_add(&stdioev, NULL); + + signal_set(&winchev, SIGWINCH, handle_resize, NULL); + signal_add(&winchev, NULL); + + return 1; +} + +void +ui_on_new_tab(struct tab *tab) +{ + struct tab *t; + + TAILQ_FOREACH(t, &tabshead, tabs) { + t->flags &= ~TAB_CURRENT; + } + + tab->flags = TAB_CURRENT; + + /* TODO: redraw the tab list */ +} + +void +ui_on_tab_refresh(struct tab *tab) +{ + if (!(tab->flags & TAB_CURRENT)) + return; + + redraw_tab(tab); +} + +void +ui_end(void) +{ + endwin(); +} + blob - /dev/null blob + c1931a699d4fd7f28ab71456257f3c97eceb250d (mode 644) --- /dev/null +++ url.c @@ -0,0 +1,436 @@ +/* + * Copyright (c) 2021 Omar Polo + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/* + * This is mostly copied from gmid' iri.c, with minor changes. Bugs + * fixed here should be ported to gmid and vice-versa. + */ + +#include +#include + +#include +#include +#include +#include + +struct shallow_url { + char *scheme; + char *host; + char *port; + uint16_t port_no; + char *path; + char *query; + char *fragment; +}; + +struct parser { + char buf[GEMINI_URL_LEN+1]; + char *iri; + struct shallow_url *parsed; + const char *err; +}; + +static inline int +unreserved(int p) +{ + return isalnum(p) + || p == '-' + || p == '.' + || p == '_' + || p == '~'; +} + +static inline int +sub_delimiters(int p) +{ + return p == '!' + || p == '$' + || p == '&' + || p == '\'' + || p == '(' + || p == ')' + || p == '*' + || p == '+' + || p == ',' + || p == ';' + || p == '='; +} + +static int +valid_pct_enc_string(char *s) +{ + if (*s != '%') + return 1; + + if (!isxdigit(s[1]) || !isxdigit(s[2])) + return 0; + + if (s[1] == '0' && s[2] == '0') + return 0; + + return 1; +} + +static int +valid_pct_encoded(struct parser *p) +{ + if (p->iri[0] != '%') + return 0; + + if (!valid_pct_enc_string(p->iri)) { + p->err = "illegal percent-encoding"; + return 0; + } + + p->iri += 2; + return 1; +} + +static void +pct_decode(char *s) +{ + sscanf(s+1, "%2hhx", s); + memmove(s+1, s+3, strlen(s+3)+1); +} + +static int +parse_pct_encoded(struct parser *p) +{ + if (p->iri[0] != '%') + return 0; + + if (!valid_pct_enc_string(p->iri)) { + p->err = "illegal percent-encoding"; + return 0; + } + + pct_decode(p->iri); + if (*p->iri == '\0') { + p->err = "illegal percent-encoding"; + return 0; + } + + return 1; +} + +/* ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) "://" */ +static int +parse_scheme(struct parser *p) +{ + p->parsed->scheme = p->iri; + + if (!isalpha(*p->iri)) { + p->err = "illegal character in scheme"; + return 0; + } + + do { + /* normalize the scheme (i.e. lowercase it) + * + * XXX: since we cannot have good things, tolower + * behaviour depends on the LC_CTYPE locale. The good + * news is that we're sure p->iri points to something + * that's in the ASCII range, so tolower can't + * mis-behave on some systems due to the locale. */ + *p->iri = tolower(*p->iri); + p->iri++; + } while (isalnum(*p->iri) + || *p->iri == '+' + || *p->iri == '-' + || *p->iri == '.'); + + if (*p->iri != ':') { + p->err = "illegal character in scheme"; + return 0; + } + + *p->iri = '\0'; + if (p->iri[1] != '/' || p->iri[2] != '/') { + p->err = "invalid marker after scheme"; + return 0; + } + + p->iri += 3; + return 1; +} + +/* *DIGIT */ +static int +parse_port(struct parser *p) +{ + uint32_t i = 0; + + p->parsed->port = p->iri; + + for (; isdigit(*p->iri); p->iri++) { + i = i * 10 + *p->iri - '0'; + if (i > UINT16_MAX) { + p->err = "port number too large"; + return 0; + } + } + + if (*p->iri != '/' && *p->iri != '\0') { + p->err = "illegal character in port number"; + return 0; + } + + p->parsed->port_no = i; + + if (*p->iri != '\0') { + *p->iri = '\0'; + p->iri++; + } + + return 1; +} + +/* TODO: add support for ip-literal and ipv4addr ? */ +/* *( unreserved / sub-delims / pct-encoded ) */ +static int +parse_authority(struct parser *p) +{ + p->parsed->host = p->iri; + + while (unreserved(*p->iri) + || sub_delimiters(*p->iri) + || parse_pct_encoded(p)) { + /* normalize the host name. */ + if (*p->iri < 0x7F) + *p->iri = tolower(*p->iri); + p->iri++; + } + + if (p->err != NULL) + return 0; + + if (*p->iri == ':') { + *p->iri = '\0'; + p->iri++; + return parse_port(p); + } else + p->parsed->port_no = 1965; + + if (*p->iri == '/') { + *p->iri = '\0'; + p->iri++; + return 1; + } + + if (*p->iri == '\0') + return 1; + + p->err = "illegal character in authority section"; + return 0; +} + +/* Routine for path_clean. Elide the pointed .. with the preceding + * element. Return 0 if it's not possible. incr is the length of + * the increment, 3 for ../ and 2 for .. */ +static int +path_elide_dotdot(char *path, char *i, int incr) +{ + char *j; + + if (i == path) + return 0; + for (j = i-2; j != path && *j != '/'; j--) + /* noop */ ; + if (*j == '/') + j++; + i += incr; + memmove(j, i, strlen(i)+1); + return 1; +} + +/* + * Use an algorithm similar to the one implemented in go' path.Clean: + * + * 1. Replace multiple slashes with a single slash + * 2. Eliminate each . path name element + * 3. Eliminate each inner .. along with the non-.. element that precedes it + * 4. Eliminate trailing .. if possible or error (go would only discard) + * + * Unlike path.Clean, this function return the empty string if the + * original path is equivalent to "/". + */ +static int +path_clean(char *path) +{ + char *i; + + /* 1. replace multiple slashes with a single one */ + for (i = path; *i; ++i) { + if (*i == '/' && *(i+1) == '/') { + memmove(i, i+1, strlen(i)); /* move also the \0 */ + i--; + } + } + + /* 2. eliminate each . path name element */ + for (i = path; *i; ++i) { + if ((i == path || *i == '/') && + *i != '.' && i[1] == '.' && i[2] == '/') { + /* move also the \0 */ + memmove(i, i+2, strlen(i)-1); + i--; + } + } + if (!strcmp(path, ".") || !strcmp(path, "/.")) { + *path = '\0'; + return 1; + } + + /* 3. eliminate each inner .. along with the preceding non-.. */ + for (i = strstr(path, "../"); i != NULL; i = strstr(path, "..")) + if (!path_elide_dotdot(path, i, 3)) + return 0; + + /* 4. eliminate trailing ..*/ + if ((i = strstr(path, "..")) != NULL) + if (!path_elide_dotdot(path, i, 2)) + return 0; + + return 1; +} + +static int +parse_query(struct parser *p) +{ + p->parsed->query = p->iri; + if (*p->iri == '\0') + return 1; + + while (unreserved(*p->iri) + || sub_delimiters(*p->iri) + || *p->iri == '/' + || *p->iri == '?' + || *p->iri == ':' + || *p->iri == '@' + || valid_pct_encoded(p)) + p->iri++; + + if (p->err != NULL) + return 0; + + if (*p->iri != '\0' && *p->iri != '#') { + p->err = "illegal character in query"; + return 0; + } + + if (*p->iri != '\0') { + *p->iri = '\0'; + p->iri++; + } + + return 1; +} + +/* don't even bother */ +static int +parse_fragment(struct parser *p) +{ + p->parsed->fragment = p->iri; + return 1; +} + +/* XXX: is it too broad? */ +/* *(pchar / "/") */ +static int +parse_path(struct parser *p) +{ + char c; + + /* trim initial slashes */ + while (*p->iri == '/') + p->iri++; + + p->parsed->path = p->iri; + if (*p->iri == '\0') { + p->parsed->query = p->parsed->fragment = p->iri; + return 1; + } + + while (unreserved(*p->iri) + || sub_delimiters(*p->iri) + || *p->iri == '/' + || parse_pct_encoded(p)) + p->iri++; + + if (p->err != NULL) + return 0; + + if (*p->iri != '\0' && *p->iri != '?' && *p->iri != '#') { + p->err = "illegal character in path"; + return 0; + } + + if (*p->iri != '\0') { + c = *p->iri; + *p->iri = '\0'; + p->iri++; + + if (c == '#') { + if (!parse_fragment(p)) + return 0; + } else + if (!parse_query(p) || !parse_fragment(p)) + return 0; + } + + if (!path_clean(p->parsed->path)) { + p->err = "illegal path"; + return 0; + } + + return 1; +} + +int +url_parse(const char *data, struct url *url, const char **err) +{ + struct shallow_url u; + struct parser p; + + memset(url, 0, sizeof(*url)); + memset(&p, 0, sizeof(p)); + memset(&u, 0, sizeof(u)); + + strlcpy(p.buf, data, sizeof(p.buf)); + p.iri = p.buf; + p.parsed = &u; + + if (!parse_scheme(&p) || !parse_authority(&p) || !parse_path(&p)) { + *err = p.err; + return 0; + } + + if (u.scheme != NULL) + strlcpy(url->scheme, u.scheme, sizeof(url->scheme)); + if (u.host != NULL) + strlcpy(url->host, u.host, sizeof(url->host)); + if (u.port != NULL) + strlcpy(url->port, u.port, sizeof(url->port)); + if (u.path != NULL) + strlcpy(url->path, u.path, sizeof(url->path)); + if (u.query != NULL) + strlcpy(url->query, u.query, sizeof(url->query)); + if (u.fragment != NULL) + strlcpy(url->fragment, u.fragment, sizeof(url->fragment)); + + return 1; +} blob - /dev/null blob + b43ea710c8ecda1d6981658ecdab720281d9da80 (mode 644) --- /dev/null +++ url.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2021 Omar Polo + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#define GEMINI_URL_LEN 1024 + +/* +1 for NUL */ +struct url { + char scheme[32]; + char host[NI_MAXHOST+1]; + char port[NI_MAXSERV+1]; + char path[GEMINI_URL_LEN+1]; + char query[GEMINI_URL_LEN+1]; + char fragment[GEMINI_URL_LEN+1]; +}; + +int url_parse(const char*, struct url*, const char**); blob - /dev/null blob + 341da330169c7050c05b46fbe6337da7c133f590 (mode 644) --- /dev/null +++ utf8.c @@ -0,0 +1,96 @@ +/* Copyright (c) 2008-2009 Bjoern Hoehrmann + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, + * modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "gmid.h" + +#include +#include + +#define UTF8_ACCEPT 0 +#define UTF8_REJECT 1 + +static const uint8_t utf8d[] = { + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f + 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf + 8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df + 0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef + 0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff + 0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0 + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2 + 1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4 + 1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6 + 1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // s7..s8 +}; + +static inline uint32_t +utf8_decode(uint32_t* state, uint32_t* codep, uint8_t byte) { + uint32_t type = utf8d[byte]; + + *codep = (*state != UTF8_ACCEPT) ? + (byte & 0x3fu) | (*codep << 6) : + (0xff >> type) & (byte); + + *state = utf8d[256 + *state*16 + type]; + return *state; +} + +/* for the iri parser. Modelled after printCodePoints */ +int +valid_multibyte_utf8(struct parser *p) +{ + uint32_t cp = 0, state = 0; + + for (; *p->iri; p->iri++) + if (!utf8_decode(&state, &cp, *p->iri)) + break; + + /* reject the ASCII range */ + if (state || cp <= 0x7F) { + /* XXX: do some error recovery? */ + if (state) + p->err = "invalid UTF-8 character"; + return 0; + } + return 1; +} + +char * +utf8_nth(char *s, size_t n) +{ + size_t i; + uint32_t cp = 0, state = 0; + + for (i = 0; *s && i < n; ++s) + if (!utf8_decode(&state, &cp, *s)) + ++i; + + if (state != UTF8_ACCEPT) + return NULL; + if (i == n) + return s; + return NULL; +} blob - /dev/null blob + 7a56d2d26e7ab2c29c2f604d20bef208c3415dd1 (mode 644) --- /dev/null +++ util.c @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021 Omar Polo + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "telescope.h" + +#include + +int +mark_nonblock(int fd) +{ + int flags; + + if ((flags = fcntl(fd, F_GETFL)) == -1) + return 0; + if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) + return 0; + return 1; +} + +char * +telescope_strnchr(char *b, char d, size_t len) +{ + size_t i; + + for (i = 0; i < len; ++i) { + if (b[i] == d) + return &b[i]; + } + + return NULL; +}