gmni

a gemini line mode client
git clone https://git.clttr.info/gmni.git
Log (Feed) | Files | Refs (Tags) | README | LICENSE

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:
Minclude/gmni/gmni.h | 29++++++-----------------------
Minclude/util.h | 5+++--
Msrc/client.c | 147++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Msrc/gmni.c | 45++++++++-------------------------------------
Msrc/gmnlm.c | 142+++++++------------------------------------------------------------------------
Msrc/parser.c | 132+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/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; }