commit d0eb06405db6ac117f9d3b18d3790f01c370c2db
parent c56e15213502aa1a2cb0de033d02cb6d25869d8e
Author: Ondřej Fiala <ofiala@airmail.cc>
Date: Tue, 26 Dec 2023 13:12:42 +0100
several interconnected improvements
* gmni.h: make response body directly accessible
* make BearSSL-related data private
* parser: simplify, remove gemini_parser_finish
* client: add support for file:// URLs to gemini_request
* gmnlm: add pagination of plaintext files, reduce code repetition
Diffstat:
M | include/gmni/gmni.h | | | 29 | ++++++----------------------- |
M | include/util.h | | | 5 | +++-- |
M | src/client.c | | | 147 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------- |
M | src/gmni.c | | | 45 | ++++++++------------------------------------- |
M | src/gmnlm.c | | | 142 | +++++++------------------------------------------------------------------------ |
M | src/parser.c | | | 132 | +++++++++++++++++++++++++++++++++++++------------------------------------------ |
M | src/util.c | | | 123 | +++++++++++++++++++++++++++++++++++++++++-------------------------------------- |
7 files changed, 267 insertions(+), 356 deletions(-)
diff --git a/include/gmni/gmni.h b/include/gmni/gmni.h
@@ -51,14 +51,7 @@ enum gemini_status_class {
struct gemini_response {
enum gemini_status status;
char *meta;
-
- // TODO: Make these private
- // Response body may be read from here if appropriate:
- br_sslio_context body;
-
- // Connection state
- br_ssl_client_context *sc;
- int fd;
+ char *body;
};
struct gmni_client_certificate;
@@ -142,23 +135,13 @@ struct gemini_token {
};
struct gemini_parser {
- int (*read)(void *state, void *buf, size_t nbyte);
- void *state;
- char *buf;
- size_t bufsz;
- size_t bufln;
+ char *bof;
+ char *bol;
bool preformatted;
};
-// Initializes a text/gemini parser. The provided "read" function will be called
-// with the provided "state" value in order to obtain more gemtext data. The
-// read function should behave like read(3).
-void gemini_parser_init(struct gemini_parser *p,
- int (*read)(void *state, void *buf, size_t nbyte),
- void *state);
-
-// Finishes this text/gemini parser and frees up its resources.
-void gemini_parser_finish(struct gemini_parser *p);
+// Initializes a text/gemini parser.
+void gemini_parser_init(struct gemini_parser *p, char *body);
// Reads the next token from a text/gemini file.
//
@@ -168,7 +151,7 @@ void gemini_parser_finish(struct gemini_parser *p);
// parameter.
int gemini_parser_next(struct gemini_parser *p, struct gemini_token *token);
-// Must be called after gemini_next to free up resources for the next token.
+// Must be called after gemini_parser_next to free up resources for the next token.
void gemini_token_finish(struct gemini_token *token);
#endif
diff --git a/include/util.h b/include/util.h
@@ -11,7 +11,8 @@ struct pathspec {
char *getpath(const struct pathspec *paths, size_t npaths);
void posix_dirname(char *path, char *dname);
int mkdirs(char *path, mode_t mode);
-int download_resp(FILE *out, struct gemini_response resp, const char *path,
- char *url);
+int print_resp_file(bool end_with_nl, struct gemini_response resp, FILE *out);
+int print_resp(bool end_with_nl, struct gemini_response resp, const char *path,
+ char *url);
#endif
diff --git a/src/client.c b/src/client.c
@@ -1,11 +1,13 @@
#include <assert.h>
#include <errno.h>
+#include <fcntl.h>
#include <netdb.h>
#include <bearssl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
+#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <gmni/certs.h>
@@ -121,13 +123,83 @@ sock_write(void *ctx, const unsigned char *buf, size_t len)
}
}
+static bool
+has_suffix(const char *str, char *suff)
+{
+ size_t suffl = strlen(suff);
+ size_t strl = strlen(str);
+ if (strl < suffl) {
+ return false;
+ }
+ return strcmp(&str[strl - suffl], suff) == 0;
+}
+
+static enum gemini_result
+file_request(const char *path, struct gemini_response *resp)
+{
+ assert(path);
+ assert(resp);
+ resp->meta = NULL;
+ resp->body = NULL;
+ enum gemini_result res = GEMINI_OK;
+
+ int fd = open(path, O_RDONLY);
+ if (fd < 0) {
+ resp->status = GEMINI_STATUS_NOT_FOUND;
+ resp->meta = strdup(strerror(errno));
+ goto out;
+ }
+
+ if (has_suffix(path, ".gmi") || has_suffix(path, ".gemini")) {
+ resp->meta = strdup("text/gemini");
+ } else if (has_suffix(path, ".txt")) {
+ resp->meta = strdup("text/plain");
+ } else {
+ // TODO: return x-octet-stream only if file contents aren't utf-8
+ resp->meta = strdup("application/x-octet-stream");
+ }
+
+ ssize_t nread = 0;
+ size_t sz = 1; // for NUL
+ size_t len = 0;
+ do {
+ len += nread;
+ sz += BUFSIZ;
+ resp->body = realloc(resp->body, sz);
+ } while ((nread = read(fd, &resp->body[len], BUFSIZ)) > 0);
+ resp->body[len] = '\0';
+
+ close(fd);
+ if (nread < 0) {
+ int e = errno;
+ free(resp->meta);
+ free(resp->body);
+ resp->meta = NULL;
+ resp->body = NULL;
+ if (e == EISDIR) {
+ resp->status = GEMINI_STATUS_BAD_REQUEST;
+ resp->meta = strdup(strerror(errno));
+ } else {
+ res = GEMINI_ERR_IO;
+ }
+ goto out;
+ }
+
+ resp->status = GEMINI_STATUS_SUCCESS;
+out:
+ return res;
+}
+
enum gemini_result
gemini_request(const char *url, struct gemini_options *options,
struct gemini_tofu *tofu, struct gemini_response *resp)
{
+ br_sslio_context sctx = {0};
+ br_ssl_client_context *sc = NULL;
+
assert(url);
assert(resp);
- memset(resp, 0, sizeof(*resp));
+ *resp = (struct gemini_response) {0};
if (strlen(url) > 1024) {
return GEMINI_ERR_INVALID_URL;
}
@@ -137,50 +209,51 @@ gemini_request(const char *url, struct gemini_options *options,
return GEMINI_ERR_OOM;
}
+ char *scheme = NULL, *host = NULL;
enum gemini_result res = GEMINI_OK;
if (curl_url_set(uri, CURLUPART_URL, url, 0) != CURLUE_OK) {
res = GEMINI_ERR_INVALID_URL;
goto cleanup;
}
- char *scheme, *host;
if (curl_url_get(uri, CURLUPART_SCHEME, &scheme, 0) != CURLUE_OK) {
res = GEMINI_ERR_INVALID_URL;
goto cleanup;
- } else {
- if (strcmp(scheme, "gemini") != 0) {
- res = GEMINI_ERR_NOT_GEMINI;
- free(scheme);
- goto cleanup;
- }
- free(scheme);
+ } else if (strcmp(scheme, "file") == 0) {
+ char *path = NULL;
+ curl_url_get(uri, CURLUPART_PATH, &path, 0);
+ res = file_request(path, resp);
+ free(path);
+ goto cleanup;
+ } else if (strcmp(scheme, "gemini") != 0) {
+ res = GEMINI_ERR_NOT_GEMINI;
+ goto cleanup;
}
+
if (curl_url_get(uri, CURLUPART_HOST, &host, 0) != CURLUE_OK) {
res = GEMINI_ERR_INVALID_URL;
- free(host);
goto cleanup;
}
- int r;
- res = gemini_connect(uri, options, resp, &resp->fd);
+ int r, sfd;
+ res = gemini_connect(uri, options, resp, &sfd);
if (res != GEMINI_OK) {
- free(host);
goto cleanup;
}
// TODO: session reuse
- resp->sc = &tofu->sc;
+ sc = &tofu->sc;
if (options->client_cert) {
struct gmni_client_certificate *cert = options->client_cert;
struct gmni_private_key *key = cert->key;
switch (key->type) {
case BR_KEYTYPE_RSA:
- br_ssl_client_set_single_rsa(resp->sc,
+ br_ssl_client_set_single_rsa(sc,
cert->chain, cert->nchain, &key->rsa,
br_rsa_pkcs1_sign_get_default());
break;
case BR_KEYTYPE_EC:
- br_ssl_client_set_single_ec(resp->sc,
+ br_ssl_client_set_single_ec(sc,
cert->chain, cert->nchain, &key->ec,
BR_KEYTYPE_SIGN, 0,
br_ec_get_default(),
@@ -188,19 +261,18 @@ gemini_request(const char *url, struct gemini_options *options,
break;
}
} else {
- br_ssl_client_set_client_certificate(resp->sc, NULL);
+ br_ssl_client_set_client_certificate(sc, NULL);
}
- br_ssl_client_reset(resp->sc, host, 0);
+ br_ssl_client_reset(sc, host, 0);
- br_sslio_init(&resp->body, &resp->sc->eng,
- sock_read, &resp->fd, sock_write, &resp->fd);
+ br_sslio_init(&sctx, &sc->eng, sock_read, &sfd, sock_write, &sfd);
char req[1024 + 3];
r = snprintf(req, sizeof(req), "%s\r\n", url);
assert(r > 0);
- br_sslio_write_all(&resp->body, req, r);
- br_sslio_flush(&resp->body);
+ br_sslio_write_all(&sctx, req, r);
+ br_sslio_flush(&sctx);
// The SSL engine maintains an internal buffer, so this shouldn't be as
// inefficient as it looks. It's necessary to do this one byte at a time
@@ -211,13 +283,25 @@ gemini_request(const char *url, struct gemini_options *options,
memset(buf, 0, sizeof(buf));
size_t l;
for (l = 0; l < 2 || memcmp(&buf[l-2], "\r\n", 2) != 0; ++l) {
- r = br_sslio_read(&resp->body, &buf[l], 1);
+ r = br_sslio_read(&sctx, &buf[l], 1);
if (r < 0) {
break;
}
}
- int err = br_ssl_engine_last_error(&resp->sc->eng);
+ int nread = 0;
+ size_t sz = 1; // for NUL
+ size_t len = 0;
+ do {
+ len += nread;
+ sz += BUFSIZ;
+ resp->body = realloc(resp->body, sz);
+ } while ((nread = br_sslio_read(&sctx, &resp->body[len], BUFSIZ)) > 0);
+ resp->body[len] = '\0';
+
+ close(sfd);
+
+ int err = br_ssl_engine_last_error(&sc->eng);
if (err != 0) {
// TODO: Bubble this up properly
fprintf(stderr, "SSL error %d\n", err);
@@ -242,7 +326,10 @@ gemini_request(const char *url, struct gemini_options *options,
resp->meta[l - 5] = '\0';
cleanup:
+ free(host);
+ free(scheme);
curl_url_cleanup(uri);
+ if (sc) br_sslio_close(&sctx);
return res;
ssl_error:
res = GEMINI_ERR_SSL;
@@ -257,19 +344,11 @@ gemini_response_finish(struct gemini_response *resp)
return;
}
- if (resp->fd != -1) {
- close(resp->fd);
- resp->fd = -1;
- }
-
free(resp->meta);
+ free(resp->body);
- if (resp->sc) {
- br_sslio_close(&resp->body);
- }
-
- resp->sc = NULL;
resp->meta = NULL;
+ resp->body = NULL;
}
const char *
diff --git a/src/gmni.c b/src/gmni.c
@@ -274,15 +274,13 @@ main(int argc, char *argv[])
int ret = 0, nredir = 0;
while (!exit) {
- char *buf;
+ char *buf = NULL;
curl_url_get(url, CURLUPART_URL, &buf, 0);
struct gemini_response resp;
enum gemini_result r = gemini_request(buf,
&opts, &cfg.tofu, &resp);
- free(buf);
-
if (r != GEMINI_OK) {
fprintf(stderr, "Error: %s\n", gemini_strerr(r, &resp));
ret = (int)r;
@@ -308,14 +306,10 @@ main(int argc, char *argv[])
break;
}
- char *buf;
- curl_url_get(url, CURLUPART_URL, &buf, 0);
-
char *new_url = gemini_input_url(buf, input);
assert(new_url);
free(input);
- free(buf);
curl_url_set(url, CURLUPART_URL, new_url, 0);
goto next;
@@ -362,42 +356,19 @@ main(int argc, char *argv[])
}
if (output_file != NULL) {
- char *buf;
- curl_url_get(url, CURLUPART_URL, &buf, 0);
-
- ret = download_resp(stderr, resp, output_file, buf);
- free(buf);
-
- break;
- }
-
- char last = 0;
- char buf[BUFSIZ];
- for (int n = 1; n > 0;) {
- n = br_sslio_read(&resp.body, buf, BUFSIZ);
- if (n > 0) {
- last = buf[n - 1];
- }
- ssize_t w = 0;
- while (w < (ssize_t)n) {
- ssize_t x = fwrite(&buf[w], 1, n - w, stdout);
- if (ferror(stdout)) {
- fprintf(stderr, "Error: write: %s\n",
- strerror(errno));
- return 1;
- }
- w += x;
+ fprintf(stderr, "Downloading %s to %s\n", buf,
+ output_file);
+ if (print_resp(linefeed, resp, output_file, buf)) {
+ fprintf(stderr, "Finished download\n");
}
- }
- if (strncmp(resp.meta, "text/", 5) == 0
- && linefeed && last != '\n'
- && isatty(STDOUT_FILENO)) {
- printf("\n");
+ } else {
+ print_resp_file(linefeed, resp, stdout);
}
break;
}
next:
+ free(buf);
gemini_response_finish(&resp);
}
diff --git a/src/gmnlm.c b/src/gmnlm.c
@@ -2,7 +2,6 @@
#include <bearssl.h>
#include <ctype.h>
#include <errno.h>
-#include <fcntl.h>
#include <getopt.h>
#include <gmni/certs.h>
#include <gmni/gmni.h>
@@ -23,6 +22,9 @@
#include <unistd.h>
#include "util.h"
+// not exported in gmni/gmni.h
+int plaintext_parser_next(struct gemini_parser *, struct gemini_token *);
+
struct link {
char *url;
struct link *next;
@@ -303,20 +305,8 @@ get_input(const struct gemini_response *resp, FILE *source)
return input;
}
-static bool
-has_suffix(char *str, char *suff)
-{
- size_t suffl = strlen(suff);
- size_t strl = strlen(str);
- if (strl < suffl) {
- return false;
- }
- return strcmp(&str[strl - suffl], suff) == 0;
-}
-
static void
pipe_resp(FILE *out, struct gemini_response resp, char *cmd) {
- char buf[BUFSIZ];
int pfd[2];
if (pipe(pfd) == -1) {
perror("pipe");
@@ -337,27 +327,7 @@ pipe_resp(FILE *out, struct gemini_response resp, char *cmd) {
}
close(pfd[0]);
FILE *f = fdopen(pfd[1], "w");
- // XXX: may affect history, do we care?
- for (int n = 1; n > 0;) {
- if (resp.sc) {
- n = br_sslio_read(&resp.body, buf, BUFSIZ);
- } else {
- n = read(resp.fd, buf, BUFSIZ);
- }
- if (n < 0) {
- n = 0;
- }
- ssize_t w = 0;
- while (w < (ssize_t)n) {
- ssize_t x = fwrite(&buf[w], 1, n - w, f);
- if (ferror(f)) {
- fprintf(stderr, "Error: write: %s\n",
- strerror(errno));
- return;
- }
- w += x;
- }
- }
+ print_resp_file(false, resp, f);
fclose(f);
int status;
waitpid(pid, &status, 0);
@@ -414,45 +384,6 @@ do_requests(struct browser *browser, struct gemini_response *resp)
}
while (requesting) {
- if (strcmp(scheme, "file") == 0) {
- requesting = false;
-
- char *path;
- uc = curl_url_get(browser->url,
- CURLUPART_PATH, &path, 0);
- if (uc != CURLUE_OK) {
- resp->status = GEMINI_STATUS_BAD_REQUEST;
- break;
- }
-
- int fd = open(path, O_RDONLY);
- if (fd < 0) {
- resp->status = GEMINI_STATUS_NOT_FOUND;
- // Make sure members of resp evaluate to false,
- // so that gemini_response_finish does not try
- // to free them.
- resp->sc = NULL;
- resp->meta = NULL;
- resp->fd = -1;
- free(path);
- break;
- }
-
- if (has_suffix(path, ".gmi") || has_suffix(path, ".gemini")) {
- resp->meta = strdup("text/gemini");
- } else if (has_suffix(path, ".txt")) {
- resp->meta = strdup("text/plain");
- } else {
- resp->meta = strdup("application/x-octet-stream");
- }
- free(path);
- resp->status = GEMINI_STATUS_SUCCESS;
- resp->fd = fd;
- resp->sc = NULL;
- res = GEMINI_OK;
- goto out;
- }
-
res = gemini_request(browser->plain_url, &browser->opts,
&browser->tofu, resp);
if (res != GEMINI_OK) {
@@ -788,7 +719,7 @@ do_prompts(const char *prompt, struct browser *browser)
set_url(browser, old_url, NULL);
goto exit;
}
- download_resp(browser->tty, resp, trim_ws(endptr), url);
+ print_resp(false, resp, trim_ws(endptr), url);
gemini_response_finish(&resp);
set_url(browser, old_url, NULL);
goto exit;
@@ -915,23 +846,13 @@ wrap(FILE *f, char *s, struct winsize *ws, int *row, int *col)
return fprintf(f, "%s\n", s) - 1;
}
-static int
-resp_read(void *state, void *buf, size_t nbyte)
-{
- struct gemini_response *resp = state;
- if (resp->sc) {
- return br_sslio_read(&resp->body, buf, nbyte);
- } else {
- return read(resp->fd, buf, nbyte);
- }
-}
-
static bool
-display_gemini(struct browser *browser, struct gemini_response *resp)
+display_document(struct browser *browser, struct gemini_response *resp,
+ int (*parser_next)(struct gemini_parser *, struct gemini_token *))
{
int nlinks = 0;
struct gemini_parser p;
- gemini_parser_init(&p, &resp_read, resp);
+ gemini_parser_init(&p, resp->body);
free(browser->page_title);
browser->page_title = NULL;
@@ -950,7 +871,7 @@ display_gemini(struct browser *browser, struct gemini_response *resp)
int info_rows = 0;
struct winsize ws;
bool first_screen = 1;
- while (text != NULL || gemini_parser_next(&p, &tok) == 0) {
+ while (text != NULL || parser_next(&p, &tok) == 0) {
repeat:
if (!row) {
ioctl(fileno(browser->tty), TIOCGWINSZ, &ws);
@@ -1106,13 +1027,11 @@ repeat:
if (text != NULL) {
gemini_token_finish(&tok);
}
- gemini_parser_finish(&p);
return true;
case PROMPT_ANSWERED:
if (text != NULL) {
gemini_token_finish(&tok);
}
- gemini_parser_finish(&p);
return true;
case PROMPT_NEXT:
searching = true;
@@ -1125,45 +1044,6 @@ repeat:
}
gemini_token_finish(&tok);
- gemini_parser_finish(&p);
- return false;
-}
-
-static bool
-display_plaintext(struct browser *browser, struct gemini_response *resp)
-{
- struct winsize ws;
- int row = 0, col = 0;
- ioctl(fileno(browser->tty), TIOCGWINSZ, &ws);
-
- char buf[BUFSIZ];
- for (int n = 1; n > 0;) {
- if (resp->sc) {
- n = br_sslio_read(&resp->body, buf, BUFSIZ);
- } else {
- n = read(resp->fd, buf, BUFSIZ);
- }
- if (n < 0) {
- n = 0;
- }
- for (int i = 0; i < n; i++) {
- if (iscntrl(buf[i]) && (buf[i] < '\t' || buf[i] > '\v')) {
- buf[i] = '.';
- }
- }
- ssize_t w = 0;
- while (w < (ssize_t)n) {
- ssize_t x = fwrite(&buf[w], 1, n - w, browser->tty);
- if (ferror(browser->tty)) {
- fprintf(stderr, "Error: write: %s\n",
- strerror(errno));
- return 1;
- }
- w += x;
- }
- }
-
- (void)row; (void)col; // TODO: generalize pagination
return false;
}
@@ -1175,10 +1055,10 @@ display_response(struct browser *browser, struct gemini_response *resp)
}
if (strcmp(resp->meta, "text/gemini") == 0
|| strncmp(resp->meta, "text/gemini;", 12) == 0) {
- return display_gemini(browser, resp);
+ return display_document(browser, resp, &gemini_parser_next);
}
if (strncmp(resp->meta, "text/", 5) == 0) {
- return display_plaintext(browser, resp);
+ return display_document(browser, resp, &plaintext_parser_next);
}
fprintf(stderr, "Media type %s is unsupported, use \"d [path]\" to download this page\n",
resp->meta);
diff --git a/src/parser.c b/src/parser.c
@@ -8,117 +8,109 @@
#include <gmni/gmni.h>
void
-gemini_parser_init(struct gemini_parser *p,
- int (*read)(void *state, void *buf, size_t nbyte),
- void *state)
+gemini_parser_init(struct gemini_parser *p, char *body)
{
- p->read = read;
- p->state = state;
- p->bufln = 0;
- p->bufsz = BUFSIZ;
- p->buf = malloc(p->bufsz + 1);
- p->buf[0] = 0;
+ p->bof = body;
+ p->bol = body;
p->preformatted = false;
}
-void
-gemini_parser_finish(struct gemini_parser *p)
+// XXX: the line itself is not NUL-terminated!
+// return: length of line, or -1 on EOF
+static int
+parser_next_line(struct gemini_parser *p, char **out)
{
- if (!p) {
- return;
+ if (!*p->bol) return -1;
+
+ char *eol = p->bol;
+ while (*eol && *eol != '\n') {
+ eol++;
}
- free(p->buf);
+ size_t n = eol - p->bol;
+ *out = p->bol;
+ p->bol = eol + (*eol != '\0');
+
+ return n;
}
int
-gemini_parser_next(struct gemini_parser *p, struct gemini_token *tok)
+plaintext_parser_next(struct gemini_parser *p, struct gemini_token *tok)
{
- memset(tok, 0, sizeof(*tok));
+ *tok = (struct gemini_token) {0};
- int eof = 0;
- while (!strchr(p->buf, '\n')) {
- while (p->bufln >= p->bufsz - 1) {
- p->bufsz *= 2;
- p->buf = realloc(p->buf, p->bufsz);
- assert(p->buf);
- }
-
- int n = p->read(p->state, &p->buf[p->bufln], p->bufsz - p->bufln - 1);
- if (n < 1) {
- eof = p->bufln == 0;
- break;
- }
- p->bufln += n;
- p->buf[p->bufln] = 0;
+ char *ln = NULL;
+ int len = parser_next_line(p, &ln);
+ if (len < 0) {
+ return 1;
}
+ tok->preformatted = strndup(ln, len);
+ tok->token = GEMINI_PREFORMATTED_TEXT;
+
+ return 0;
+}
+
+int
+gemini_parser_next(struct gemini_parser *p, struct gemini_token *tok)
+{
+ *tok = (struct gemini_token) {0};
- char *end;
- if ((end = strchr(p->buf, '\n')) != NULL) {
- *end = 0;
+ char *ln = NULL;
+ int len = parser_next_line(p, &ln);
+ if (len < 0) {
+ return 1;
}
+ char c = ln[len];
+ ln[len] = '\0';
if (p->preformatted) {
- if (strncmp(p->buf, "```", 3) == 0) {
+ if (strncmp(ln, "```", 3) == 0) {
tok->token = GEMINI_PREFORMATTED_END;
p->preformatted = false;
} else {
tok->token = GEMINI_PREFORMATTED_TEXT;
- tok->preformatted = strdup(p->buf);
+ tok->preformatted = strdup(ln);
}
- } else if (strncmp(p->buf, "=>", 2) == 0) {
+ } else if (strncmp(ln, "=>", 2) == 0) {
tok->token = GEMINI_LINK;
int i = 2;
- while (p->buf[i] && isspace(p->buf[i])) ++i;
- tok->link.url = &p->buf[i];
+ while (isspace(ln[i])) i++;
+ tok->link.url = strdup(&ln[i]);
- for (; p->buf[i]; ++i) {
- if (isspace(p->buf[i])) {
- p->buf[i++] = 0;
- while (isspace(p->buf[i])) ++i;
- if (p->buf[i]) {
- tok->link.text = strdup(&p->buf[i]);
- }
+ for (char *p = tok->link.url; *p; p++) {
+ if (isspace(*p)) {
+ *p = '\0';
+ do { p++; } while (isspace(*p));
+ if (*p) tok->link.text = strdup(p);
break;
}
}
-
- tok->link.url = strdup(tok->link.url);
- } else if (strncmp(p->buf, "```", 3) == 0) {
+ } else if (strncmp(ln, "```", 3) == 0) {
tok->token = GEMINI_PREFORMATTED_BEGIN;
- if (p->buf[3]) {
- tok->preformatted = strdup(&p->buf[3]);
+ if (ln[3]) {
+ tok->preformatted = strdup(&ln[3]);
}
p->preformatted = true;
- } else if (p->buf[0] == '#') {
+ } else if (ln[0] == '#') {
tok->token = GEMINI_HEADING;
int level = 1;
- while (p->buf[level] == '#' && level < 3) {
+ while (ln[level] == '#' && level < 3) {
++level;
}
tok->heading.level = level;
- tok->heading.title = strdup(&p->buf[level]);
- } else if (p->buf[0] == '*') {
+ tok->heading.title = strdup(&ln[level]);
+ } else if (ln[0] == '*') {
tok->token = GEMINI_LIST_ITEM;
- tok->list_item = strdup(&p->buf[1]);
- } else if (p->buf[0] == '>') {
+ tok->list_item = strdup(&ln[1]);
+ } else if (ln[0] == '>') {
tok->token = GEMINI_QUOTE;
- tok->quote_text = strdup(&p->buf[1]);
+ tok->quote_text = strdup(&ln[1]);
} else {
tok->token = GEMINI_TEXT;
- tok->text = strdup(p->buf);
- }
-
- if (end && end + 1 < p->buf + p->bufln) {
- size_t len = end - p->buf + 1;
- memmove(p->buf, end + 1, p->bufln - len);
- p->bufln -= len;
- p->buf[p->bufln] = 0;
- } else {
- p->buf[0] = 0;
- p->bufln = 0;
+ tok->text = strdup(ln);
}
- return eof;
+ ln[len] = c;
+ return 0;
}
void
diff --git a/src/util.c b/src/util.c
@@ -4,6 +4,7 @@
#include <gmni/gmni.h>
#include <libgen.h>
#include <limits.h>
+#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
@@ -61,75 +62,79 @@ getpath(const struct pathspec *paths, size_t npaths) {
return NULL;
}
-int
-download_resp(FILE *out, struct gemini_response resp, const char *path,
- char *url)
+static char *
+get_final_path(const char *path, char *url)
{
- char path_buf[PATH_MAX];
- int n = 0;
assert(path);
+ assert(url);
+
+ size_t len = strlen(path);
+ size_t hlen = *path == '~' ? strlen(getenv("HOME")) : 0;
+ size_t flen = path[len - 1] == '/' ? strlen(basename(url)) : 0;
+
+ if (len + hlen + flen >= PATH_MAX) { // PATH_MAX includes NUL
+ fprintf(stderr,
+ "Path %s exceeds limit of %d bytes%s\n", path, PATH_MAX,
+ hlen || flen ? " after necessary substitutions are made" : "");
+ return NULL;
+ }
+
+ char *buf = malloc(PATH_MAX);
switch (path[0]) {
case '\0':
- strcpy(path_buf, "./");
+ strcpy(buf, "./");
break;
- case '~':
- n = snprintf(path_buf, PATH_MAX, "%s/%s", getenv("HOME"), &path[1]);
- if (n > PATH_MAX) {
- fprintf(stderr,
- "Path %s exceeds limit of %d bytes and has been truncated\n",
- path_buf, PATH_MAX);
- return 1;
- }
+ case '~':;
+ int n = snprintf(buf, PATH_MAX, "%s/%s", getenv("HOME"), &path[1]);
+ assert(n < PATH_MAX);
break;
default:
- if (strlen(path) > PATH_MAX) {
- fprintf(stderr, "Path %s exceeds limit of %d bytes\n",
- path, PATH_MAX);
- return 1;
- }
- strcpy(path_buf, path);
+ strcpy(buf, path);
+ break;
}
- char path_res[PATH_MAX];
- if (path_buf[strlen(path_buf)-1] == '/') {
- n = snprintf(path_res, PATH_MAX, "%s%s", path_buf, basename(url));
- if (n > PATH_MAX) {
- fprintf(stderr,
- "Path %s exceeds limit of %d bytes and has been truncated\n",
- path_res, PATH_MAX);
- return 1;
+ if (flen > 0) strcat(buf, basename(url));
+
+ return buf;
+}
+
+int
+print_resp_file(bool end_with_nl, struct gemini_response resp, FILE *out)
+{
+ assert(out);
+ size_t i = 0;
+ while (resp.body[i] != '\0') {
+ if (fputc(resp.body[i], out) == EOF) {
+ fprintf(stderr, "Error: write: %s\n", strerror(errno));
+ return 0;
}
- } else {
- strcpy(path_res, path_buf);
+ i++;
}
- FILE *f = fopen(path_res, "w");
- if (f == NULL) {
- fprintf(stderr, "Could not open %s for writing: %s\n",
- path_res, strerror(errno));
- return 1;
+ if (end_with_nl && i > 0 && resp.body[i - 1] != '\n') {
+ fputc('\n', out);
}
- fprintf(out, "Downloading %s to %s\n", url, path_res);
- char buf[BUFSIZ];
- for (int n = 1; n > 0;) {
- if (resp.sc) {
- n = br_sslio_read(&resp.body, buf, BUFSIZ);
- } else {
- n = read(resp.fd, buf, BUFSIZ);
- }
- if (n < 0) {
- break;
- }
- ssize_t w = 0;
- while (w < (ssize_t)n) {
- ssize_t x = fwrite(&buf[w], 1, n - w, f);
- if (ferror(f)) {
- fprintf(stderr, "Error: write: %s\n",
- strerror(errno));
- return 1;
- }
- w += x;
- }
+ return 1;
+}
+
+int
+print_resp(bool end_with_nl, struct gemini_response resp, const char *pth, char *url)
+{
+ assert(pth);
+ assert(url);
+
+ int ret = 0;
+ FILE *out = NULL;
+
+ char *path = get_final_path(pth, url);
+ if (!path) goto cleanup;
+ out = fopen(path, "w");
+ if (!out) {
+ fprintf(stderr, "Could not open %s for writing: %s\n",
+ path, strerror(errno));
+ goto cleanup;
}
- fprintf(out, "Finished download\n");
- fclose(f);
- return 0;
+ ret = print_resp_file(end_with_nl, resp, out);
+cleanup:
+ if (out) fclose(out);
+ free(path);
+ return ret;
}