commit - /dev/null
commit + 5e11c00c40910bea9deb4b2199868f6dde63798e
blob - /dev/null
blob + 2af19fe67c4e2e1d0c6b2c91d6cd44191cfe1a44 (mode 644)
--- /dev/null
+++ .gitignore
+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
+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
+# Telescope
+
+Telescope is a w3m-like browser for Gemini.
blob - /dev/null
blob + a9e8f0bb56cd5226b87d4bef667d9269adf52a6e (mode 644)
--- /dev/null
+++ about.c
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * 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>
+
+#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
+#!/bin/sh
+
+exec autoreconf --install
blob - /dev/null
blob + 8fd6b69aad5ec4b5c2f16a83d35000248847ade6 (mode 644)
--- /dev/null
+++ configure.ac
+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
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * 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 <telescope.h>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <netinet/in.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <event.h>
+#include <netdb.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <tls.h>
+#include <unistd.h>
+
+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
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * 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 <telescope.h>
+
+#include <ctype.h>
+#include <string.h>
+#include <stdlib.h>
+
+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
+#include "telescope.h"
+
+#include <sys/socket.h>
+
+#include <err.h>
+#include <errno.h>
+#include <event.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+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
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * 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 <config.h>
+#include <url.h>
+
+/* TODO: move in config.h */
+#include <sys/queue.h>
+#include <sys/types.h>
+#include <sys/uio.h>
+#include <stdint.h>
+#include <imsg.h>
+
+#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
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * 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 <curses.h>
+#include <event.h>
+#include <locale.h>
+#include <signal.h>
+
+#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
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * 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 <config.h>
+#include <url.h>
+
+#include <ctype.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+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
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * 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 <netdb.h>
+
+#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
+/* Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
+ *
+ * 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 <stddef.h>
+#include <stdint.h>
+
+#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
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * 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 <fcntl.h>
+
+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;
+}