commit 25092bbd3b288f40af3bb76ad5c1092b998a8b35 parent 22d763f48fdb27f11dc0d5346e5c851ea103d19e Author: René Wagner <rwa@clttr.info> Date: Sat, 24 Jul 2021 08:05:02 +0200 remove git repos from repo Diffstat:
288 files changed, 1 insertion(+), 30974 deletions(-)
diff --git a/.gitignore b/.gitignore @@ -1 +1,2 @@ /transit/index.gmi +/sources/*.git diff --git a/sources/cgmnlm.git/commits/01567e578c9632960903e1f56dd2086547806da3.patch b/sources/cgmnlm.git/commits/01567e578c9632960903e1f56dd2086547806da3.patch @@ -1,285 +0,0 @@ -diff --git a/Makefile b/Makefile -index 69a241a8825a6a7aa979eb2ae95a26faaf3a0532..3d4df602cd7f7ab4f5a45b47dee0d47729f0739c 100644 ---- a/Makefile -+++ b/Makefile -@@ -8,6 +8,10 @@ gmni: $(gmni_objects) - @printf 'CCLD\t$@\n' - @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmni_objects) - -+gmnlm: $(gmnlm_objects) -+ @printf 'CCLD\t$@\n' -+ @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmnlm_objects) -+ - doc/gmni.1: doc/gmni.scd - - .SUFFIXES: .c .o .scd .1 -diff --git a/config.sh b/config.sh -index b93815ada4a25ec508e7a86cc79b9e9be3eba428..52931ab241c3177285b56258bf9b67ac4b63a7ea 100644 ---- a/config.sh -+++ b/config.sh -@@ -134,6 +134,7 @@ - all: ${all} - EOF - gmni >>"$outdir"/config.mk -+ gmnlm >>"$outdir"/config.mk - echo done - - touch $outdir/cppcache -diff --git a/configure b/configure -index aca6e8a1eaa9a6271f03eb8863640d0d93cdf435..151bdae8c12d9ad07d3a5240d7c554f4c62664c8 100755 ---- a/configure -+++ b/configure -@@ -7,10 +7,18 @@ genrules gmni \ - src/client.c \ - src/escape.c \ - src/gmni.c \ -+ src/url.c -+} -+ -+gmnlm() { -+ genrules gmnlm \ -+ src/client.c \ -+ src/escape.c \ -+ src/gmnlm.c \ - src/parser.c \ - src/url.c - } - --all="gmni" -+all="gmni gmnlm" - - run_configure -diff --git a/src/gmni.c b/src/gmni.c -index 75c6c5afb6e7f1f286d314d93f2b44ef8414afb3..dc0c5c7f61679369fe4fadafd0508147868cc3c3 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -15,7 +15,7 @@ #include <unistd.h> - #include "gmni.h" - - static void --usage(char *argv_0) -+usage(const char *argv_0) - { - fprintf(stderr, - "usage: %s [-46lLiIN] [-E cert] [-d input] [-D path] gemini://...\n", -diff --git a/src/gmnlm.c b/src/gmnlm.c -new file mode 100644 -index 0000000000000000000000000000000000000000..bc3f647d42372678b4180f539df8637d0ba69a12 ---- /dev/null -+++ b/src/gmnlm.c -@@ -0,0 +1,215 @@ -+#include <assert.h> -+#include <ctype.h> -+#include <getopt.h> -+#include <openssl/bio.h> -+#include <openssl/err.h> -+#include <stdbool.h> -+#include <stdio.h> -+#include <string.h> -+#include <sys/ioctl.h> -+#include <termios.h> -+#include <unistd.h> -+#include "gmni.h" -+#include "url.h" -+ -+struct link { -+ char *url; -+ struct link *next; -+}; -+ -+static void -+usage(const char *argv_0) -+{ -+ fprintf(stderr, "usage: %s [gemini://...]\n", argv_0); -+} -+ -+static bool -+set_url(struct Curl_URL *url, char *new_url) -+{ -+ if (curl_url_set(url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { -+ fprintf(stderr, "Error: invalid URL\n"); -+ return false; -+ } -+ return true; -+} -+ -+static char * -+trim_ws(char *in) -+{ -+ for (int i = strlen(in) - 1; in[i] && isspace(in[i]); --i) { -+ in[i] = 0; -+ } -+ for (; *in && isspace(*in); ++in); -+ return in; -+} -+ -+static void -+display_gemini(FILE *tty, struct gemini_response *resp, -+ struct link **next, bool pagination) -+{ -+ int nlinks = 0; -+ struct gemini_parser p; -+ gemini_parser_init(&p, resp->bio); -+ -+ struct winsize ws; -+ ioctl(fileno(tty), TIOCGWINSZ, &ws); -+ -+ int row = 0, col = 0; -+ struct gemini_token tok; -+ while (gemini_parser_next(&p, &tok) == 0) { -+ switch (tok.token) { -+ case GEMINI_TEXT: -+ // TODO: word wrap -+ col += fprintf(tty, " %s\n", trim_ws(tok.text)); -+ break; -+ case GEMINI_LINK: -+ (void)next; // TODO: Record links -+ col += fprintf(tty, "[%d] %s\n", nlinks++, trim_ws( -+ tok.link.text ? tok.link.text : tok.link.url)); -+ break; -+ case GEMINI_PREFORMATTED: -+ continue; // TODO -+ case GEMINI_HEADING: -+ for (int n = tok.heading.level; n; --n) { -+ col += fprintf(tty, "#"); -+ } -+ col += fprintf(tty, " %s\n", trim_ws(tok.heading.title)); -+ break; -+ case GEMINI_LIST_ITEM: -+ // TODO: Option to disable Unicode -+ col += fprintf(tty, " • %s\n", trim_ws(tok.list_item)); -+ break; -+ case GEMINI_QUOTE: -+ // TODO: Option to disable Unicode -+ col += fprintf(tty, " | %s\n", trim_ws(tok.quote_text)); -+ break; -+ } -+ -+ while (col >= ws.ws_col) { -+ col -= ws.ws_col; -+ ++row; -+ } -+ ++row; -+ col = 0; -+ -+ if (pagination && row >= ws.ws_row - 1) { -+ fprintf(tty, "[Enter for more, or q to stop] "); -+ -+ size_t n = 0; -+ char *l = NULL; -+ if (getline(&l, &n, tty) == -1) { -+ return; -+ } -+ if (strcmp(l, "q\n") == 0) { -+ return; -+ } -+ -+ free(l); -+ row = col = 0; -+ } -+ } -+ -+ gemini_parser_finish(&p); -+} -+ -+int -+main(int argc, char *argv[]) -+{ -+ bool pagination = true; -+ -+ bool have_url = false; -+ struct Curl_URL *url = curl_url(); -+ -+ FILE *tty = fopen("/dev/tty", "w+"); -+ -+ int c; -+ while ((c = getopt(argc, argv, "hP")) != -1) { -+ switch (c) { -+ case 'P': -+ pagination = false; -+ break; -+ case 'h': -+ usage(argv[0]); -+ return 0; -+ default: -+ fprintf(stderr, "fatal: unknown flag %c\n", c); -+ return 1; -+ } -+ } -+ -+ if (optind == argc - 1) { -+ set_url(url, argv[optind]); -+ have_url = true; -+ } else if (optind < argc - 1) { -+ usage(argv[0]); -+ return 1; -+ } -+ -+ SSL_load_error_strings(); -+ ERR_load_crypto_strings(); -+ struct gemini_options opts = { -+ .ssl_ctx = SSL_CTX_new(TLS_method()), -+ }; -+ -+ bool run = true; -+ struct gemini_response resp; -+ while (run) { -+ assert(have_url); // TODO -+ -+ struct link *links; -+ static char prompt[4096]; -+ -+ char *plain_url; -+ CURLUcode uc = curl_url_get(url, CURLUPART_URL, &plain_url, 0); -+ assert(uc == CURLUE_OK); // Invariant -+ -+ snprintf(prompt, sizeof(prompt), "\n\t%s\n" -+ "\tWhere to? [n]: follow Nth link; [o <url>]: open URL; [q]: quit\n" -+ "=> ", plain_url); -+ -+ enum gemini_result res = gemini_request(plain_url, &opts, &resp); -+ if (res != GEMINI_OK) { -+ fprintf(stderr, "Error: %s\n", gemini_strerr(res, &resp)); -+ assert(0); // TODO: Prompt -+ } -+ -+ switch (gemini_response_class(resp.status)) { -+ case GEMINI_STATUS_CLASS_INPUT: -+ assert(0); // TODO -+ case GEMINI_STATUS_CLASS_REDIRECT: -+ assert(0); // TODO -+ case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: -+ assert(0); // TODO -+ case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: -+ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: -+ fprintf(stderr, "Server returned %s %d %s\n", -+ resp.status / 10 == 4 ? -+ "TEMPORARY FAILURE" : "PERMANENT FALIURE", -+ resp.status, resp.meta); -+ break; -+ case GEMINI_STATUS_CLASS_SUCCESS: -+ display_gemini(tty, &resp, &links, pagination); -+ break; -+ } -+ -+ gemini_response_finish(&resp); -+ -+ fprintf(tty, "%s", prompt); -+ size_t l = 0; -+ char *in = NULL; -+ ssize_t n = getline(&in, &l, tty); -+ if (n == -1 && feof(tty)) { -+ break; -+ } -+ -+ if (strcmp(in, "q\n") == 0) { -+ run = false; -+ } -+ -+ free(in); -+ } -+ -+ SSL_CTX_free(opts.ssl_ctx); -+ curl_url_cleanup(url); -+ return 0; -+} diff --git a/sources/cgmnlm.git/commits/021d8f8fdfcb9be636a73d7c59d540d8255cc0df.patch b/sources/cgmnlm.git/commits/021d8f8fdfcb9be636a73d7c59d540d8255cc0df.patch @@ -1,22 +0,0 @@ -diff --git a/src/gmni.c b/src/gmni.c -index e8b25f9060c021bda099aa97fc2ec5657aeb2672..4f563ed19e0edc52086f55e4057a91f0802a1f93 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -20,7 +20,7 @@ static void - usage(const char *argv_0) - { - fprintf(stderr, -- "usage: %s [-46lLiIN] [-E cert] [-d input] [-D path] gemini://...\n", -+ "usage: %s [-46lLiIN] [-j mode] [-E cert] [-d input] [-D path] gemini://...\n", - argv_0); - } - -@@ -86,7 +86,7 @@ fprintf(stderr, - "The certificate offered by this server is of unknown trust. " - "Its fingerprint is: \n" - "%s\n\n" -- "Use -j once to trust temporarily, or -j always to add to the trust store.\n", fingerprint); -+ "Use '-j once' to trust temporarily, or '-j always' to add to the trust store.\n", fingerprint); - break; - case TOFU_FINGERPRINT_MISMATCH: - fprintf(stderr, diff --git a/sources/cgmnlm.git/commits/02c2be62daceb04e4891d415c997dd64db84b9d9.patch b/sources/cgmnlm.git/commits/02c2be62daceb04e4891d415c997dd64db84b9d9.patch @@ -1,12 +0,0 @@ -diff --git a/src/gmni.c b/src/gmni.c -index ab0908de5ec9682865504e940a5489a43c41b12e..bdaa9baaca75e5f167add9a0ebed73cc60eaa647 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -147,6 +147,7 @@ - new_url = gemini_input_url(url, input); - free(url); - url = new_url; -+ assert(url); - goto next; - case 3: // REDIRECT - free(url); diff --git a/sources/cgmnlm.git/commits/02f6af661513683f0c6c1465c5ff1dd8f03a30c9.patch b/sources/cgmnlm.git/commits/02f6af661513683f0c6c1465c5ff1dd8f03a30c9.patch @@ -1,751 +0,0 @@ -diff --git a/configure b/configure -index 7412cf545fa96e6be169c0055778b83a06b557f5..44db11c4765077677a1f506f034c846cc736ab8c 100755 ---- a/configure -+++ b/configure -@@ -7,7 +7,9 @@ genrules gmni \ - src/client.c \ - src/escape.c \ - src/gmni.c \ -- src/url.c -+ src/tofu.c \ -+ src/url.c \ -+ src/util.c - } - - gmnlm() { -@@ -16,6 +18,7 @@ src/client.c \ - src/escape.c \ - src/gmnlm.c \ - src/parser.c \ -+ src/tofu.c \ - src/url.c \ - src/util.c - } -diff --git a/doc/gmni.scd b/doc/gmni.scd -index 0d84d4d974f2c38b426f6ae85d228cdd4847cbda..2c1dc54272aaf3a4cbd6b1c13e29323f418f199b 100644 ---- a/doc/gmni.scd -+++ b/doc/gmni.scd -@@ -6,7 +6,7 @@ gmni - Gemini client - - # SYNPOSIS - --*gmni* [-46lLiIN] [-E _path_] [-d _input_] [-D _path_] _gemini://..._ -+*gmni* [-46lLiIN] [-j _mode_] [-E _path_] [-d _input_] [-D _path_] _gemini://..._ - - # DESCRIPTION - -@@ -51,6 +51,11 @@ this behavior. - - *-L* - Follow redirects. -+ -+*-j* _mode_ -+ Sets the TOFU (trust on first use) configuration, which controls if the -+ client shall trust new certificates. _mode_ can be one of *always*, -+ *once*, or *fail*. - - *-i* - Print the response status and meta text to stdout. -diff --git a/doc/gmnlm.scd b/doc/gmnlm.scd -index c5e7bf7f6b189f984e01ea2a942f47acb993f7e7..b11f3612e044ad0667d8c4d306445ae86e9f73d4 100644 ---- a/doc/gmnlm.scd -+++ b/doc/gmnlm.scd -@@ -6,13 +6,18 @@ gmnlm - Gemini line-mode browser - - # SYNPOSIS - --*gmnlm* [-PU] _gemini://..._ -+*gmnlm* [-PU] [-j _mode_] _gemini://..._ - - # DESCRIPTION - - *gmnlm* is an interactive line-mode Gemini browser. - - # OPTIONS -+ -+*-j* _mode_ -+ Sets the TOFU (trust on first use) configuration, which controls if the -+ client shall trust new certificates. _mode_ can be one of *always*, -+ *once*, or *fail*. - - *-P* - Disable pagination. -diff --git a/include/gmni.h b/include/gmni.h -index 4240c6231010ebb86aead2d49700fe2e3d00b65c..7e27b489d71fd3a43ca60292b17d56cab3caa5f8 100644 ---- a/include/gmni.h -+++ b/include/gmni.h -@@ -13,6 +13,7 @@ GEMINI_ERR_NOT_GEMINI, - GEMINI_ERR_RESOLVE, - GEMINI_ERR_CONNECT, - GEMINI_ERR_SSL, -+ GEMINI_ERR_SSL_VERIFY, - GEMINI_ERR_IO, - GEMINI_ERR_PROTOCOL, - }; -@@ -64,10 +65,6 @@ struct gemini_options { - // If NULL, an SSL context will be created. If unset, the ssl field - // must also be NULL. - SSL_CTX *ssl_ctx; -- -- // If NULL, an SSL connection will be established. If set, it is -- // presumed that the caller pre-established the SSL connection. -- SSL *ssl; - - // If ai_family != AF_UNSPEC (the default value on most systems), the - // client will connect to this address and skip name resolution. -diff --git a/include/tofu.h b/include/tofu.h -new file mode 100644 -index 0000000000000000000000000000000000000000..29aa9bc21567868cafb25a09dbc25ea0685ab01c ---- /dev/null -+++ b/include/tofu.h -@@ -0,0 +1,48 @@ -+#ifndef GEMINI_TOFU_H -+#define GEMINI_TOFU_H -+#include <limits.h> -+#include <openssl/ssl.h> -+#include <openssl/x509.h> -+#include <time.h> -+ -+enum tofu_error { -+ TOFU_VALID, -+ // Expired, wrong CN, etc. -+ TOFU_INVALID_CERT, -+ // Cert is valid but we haven't seen it before -+ TOFU_UNTRUSTED_CERT, -+ // Cert is valid but we already trust another cert for this host -+ TOFU_FINGERPRINT_MISMATCH, -+}; -+ -+enum tofu_action { -+ TOFU_ASK, -+ TOFU_FAIL, -+ TOFU_TRUST_ONCE, -+ TOFU_TRUST_ALWAYS, -+}; -+ -+struct known_host { -+ char *host, *fingerprint; -+ time_t expires; -+ int lineno; -+ struct known_host *next; -+}; -+ -+// Called when the user needs to be prompted to agree to trust an unknown -+// certificate. Return true to trust this certificate. -+typedef enum tofu_action (tofu_callback_t)(enum tofu_error error, -+ const char *fingerprint, struct known_host *host, void *data); -+ -+struct gemini_tofu { -+ char known_hosts_path[PATH_MAX+1]; -+ struct known_host *known_hosts; -+ int lineno; -+ tofu_callback_t *callback; -+ void *cb_data; -+}; -+ -+void gemini_tofu_init(struct gemini_tofu *tofu, -+ SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *data); -+ -+#endif -diff --git a/src/client.c b/src/client.c -index d8b67d7b9f47b4473ba6eb278853ea0565739f09..07460f917b153b0d368eb2a98f368aa6a5df56a4 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -95,6 +95,7 @@ { - assert(url); - assert(resp); - resp->meta = NULL; -+ resp->bio = NULL; - if (strlen(url) > 1024) { - return GEMINI_ERR_INVALID_URL; - } -@@ -110,7 +111,7 @@ res = GEMINI_ERR_INVALID_URL; - goto cleanup; - } - -- char *scheme; -+ char *scheme, *host; - if (curl_url_get(uri, CURLUPART_SCHEME, &scheme, 0) != CURLUE_OK) { - res = GEMINI_ERR_INVALID_URL; - goto cleanup; -@@ -120,6 +121,10 @@ res = GEMINI_ERR_NOT_GEMINI; - goto cleanup; - } - } -+ if (curl_url_get(uri, CURLUPART_HOST, &host, 0) != CURLUE_OK) { -+ res = GEMINI_ERR_INVALID_URL; -+ goto cleanup; -+ } - - if (options && options->ssl_ctx) { - resp->ssl_ctx = options->ssl_ctx; -@@ -127,42 +132,54 @@ SSL_CTX_up_ref(options->ssl_ctx); - } else { - resp->ssl_ctx = SSL_CTX_new(TLS_method()); - assert(resp->ssl_ctx); -+ SSL_CTX_set_verify(resp->ssl_ctx, SSL_VERIFY_PEER, NULL); - } - -+ int r; - BIO *sbio = BIO_new(BIO_f_ssl()); -- if (options && options->ssl) { -- resp->ssl = options->ssl; -- SSL_up_ref(resp->ssl); -- BIO_set_ssl(sbio, resp->ssl, 0); -- resp->fd = -1; -- } else { -- res = gemini_connect(uri, options, resp, &resp->fd); -- if (res != GEMINI_OK) { -- goto cleanup; -- } -+ res = gemini_connect(uri, options, resp, &resp->fd); -+ if (res != GEMINI_OK) { -+ goto cleanup; -+ } -+ -+ resp->ssl = SSL_new(resp->ssl_ctx); -+ assert(resp->ssl); -+ SSL_set_connect_state(resp->ssl); -+ if ((r = SSL_set1_host(resp->ssl, host)) != 1) { -+ goto ssl_error; -+ } -+ if ((r = SSL_set_tlsext_host_name(resp->ssl, host)) != 1) { -+ goto ssl_error; -+ } -+ if ((r = SSL_set_fd(resp->ssl, resp->fd)) != 1) { -+ goto ssl_error; -+ } -+ if ((r = SSL_connect(resp->ssl)) != 1) { -+ goto ssl_error; -+ } -+ -+ X509 *cert = SSL_get_peer_certificate(resp->ssl); -+ if (!cert) { -+ resp->status = X509_V_ERR_UNSPECIFIED; -+ res = GEMINI_ERR_SSL_VERIFY; -+ goto cleanup; -+ } -+ X509_free(cert); - -- resp->ssl = SSL_new(resp->ssl_ctx); -- assert(resp->ssl); -- int r = SSL_set_fd(resp->ssl, resp->fd); -- if (r != 1) { -- resp->status = r; -- res = GEMINI_ERR_SSL; -- goto cleanup; -- } -- r = SSL_connect(resp->ssl); -- if (r != 1) { -- resp->status = r; -- res = GEMINI_ERR_SSL; -- goto cleanup; -- } -- BIO_set_ssl(sbio, resp->ssl, 0); -+ long vr = SSL_get_verify_result(resp->ssl); -+ if (vr != X509_V_OK) { -+ resp->status = vr; -+ res = GEMINI_ERR_SSL_VERIFY; -+ goto cleanup; - } - -+ BIO_set_ssl(sbio, resp->ssl, 0); -+ - resp->bio = BIO_new(BIO_f_buffer()); - BIO_push(resp->bio, sbio); - - char req[1024 + 3]; -- int r = snprintf(req, sizeof(req), "%s\r\n", url); -+ r = snprintf(req, sizeof(req), "%s\r\n", url); - assert(r > 0); - - r = BIO_puts(sbio, req); -@@ -199,6 +216,10 @@ - cleanup: - curl_url_cleanup(uri); - return res; -+ssl_error: -+ res = GEMINI_ERR_SSL; -+ resp->status = r; -+ goto cleanup; - } - - void -@@ -248,6 +269,8 @@ case GEMINI_ERR_SSL: - return ERR_error_string( - SSL_get_error(resp->ssl, resp->status), - NULL); -+ case GEMINI_ERR_SSL_VERIFY: -+ return X509_verify_cert_error_string(resp->status); - case GEMINI_ERR_IO: - return "I/O error"; - case GEMINI_ERR_PROTOCOL: -diff --git a/src/gmni.c b/src/gmni.c -index dc0c5c7f61679369fe4fadafd0508147868cc3c3..c13e0cd55623f557a8dc676d69aea54661b11faf 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -13,6 +13,7 @@ #include <sys/types.h> - #include <termios.h> - #include <unistd.h> - #include "gmni.h" -+#include "tofu.h" - - static void - usage(const char *argv_0) -@@ -57,6 +58,55 @@ } - return input; - } - -+struct tofu_config { -+ struct gemini_tofu tofu; -+ enum tofu_action action; -+}; -+ -+static enum tofu_action -+tofu_callback(enum tofu_error error, const char *fingerprint, -+ struct known_host *host, void *data) -+{ -+ struct tofu_config *cfg = (struct tofu_config *)data; -+ enum tofu_action action = cfg->action; -+ switch (error) { -+ case TOFU_VALID: -+ assert(0); // Invariant -+ case TOFU_INVALID_CERT: -+ fprintf(stderr, -+ "The server presented an invalid certificate with fingerprint %s.\n", -+ fingerprint); -+ if (action == TOFU_TRUST_ALWAYS) { -+ action = TOFU_TRUST_ONCE; -+ } -+ break; -+ case TOFU_UNTRUSTED_CERT: -+ fprintf(stderr, -+ "The certificate offered by this server is of unknown trust. " -+ "Its fingerprint is: \n" -+ "%s\n\n", fingerprint); -+ break; -+ case TOFU_FINGERPRINT_MISMATCH: -+ fprintf(stderr, -+ "The certificate offered by this server DOES NOT MATCH the one we have on file.\n" -+ "/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n" -+ "The unknown certificate's fingerprint is:\n" -+ "%s\n\n" -+ "The expected fingerprint is:\n" -+ "%s\n\n" -+ "If you're certain that this is correct, edit %s:%d\n", -+ fingerprint, host->fingerprint, -+ cfg->tofu.known_hosts_path, host->lineno); -+ return TOFU_FAIL; -+ } -+ -+ if (action == TOFU_ASK) { -+ return TOFU_FAIL; -+ } -+ -+ return action; -+} -+ - int - main(int argc, char *argv[]) - { -@@ -71,7 +121,6 @@ enum input_mode { - INPUT_READ, - INPUT_SUPPRESS, - }; -- - enum input_mode input_mode = INPUT_READ; - FILE *input_source = stdin; - -@@ -82,9 +131,11 @@ struct addrinfo hints = {0}; - struct gemini_options opts = { - .hints = &hints, - }; -+ struct tofu_config cfg; -+ cfg.action = TOFU_ASK; - - int c; -- while ((c = getopt(argc, argv, "46d:D:E:hlLiINR:")) != -1) { -+ while ((c = getopt(argc, argv, "46d:D:E:hj:lLiINR:")) != -1) { - switch (c) { - case '4': - hints.ai_family = AF_INET; -@@ -115,6 +166,18 @@ break; - case 'h': - usage(argv[0]); - return 0; -+ case 'j': -+ if (strcmp(optarg, "fail") == 0) { -+ cfg.action = TOFU_FAIL; -+ } else if (strcmp(optarg, "once") == 0) { -+ cfg.action = TOFU_TRUST_ONCE; -+ } else if (strcmp(optarg, "always") == 0) { -+ cfg.action = TOFU_TRUST_ALWAYS; -+ } else { -+ usage(argv[0]); -+ return 1; -+ } -+ break; - case 'l': - linefeed = false; - break; -@@ -153,6 +216,8 @@ } - - SSL_load_error_strings(); - ERR_load_crypto_strings(); -+ opts.ssl_ctx = SSL_CTX_new(TLS_method()); -+ gemini_tofu_init(&cfg.tofu, opts.ssl_ctx, &tofu_callback, &cfg); - - bool exit = false; - char *url = strdup(argv[optind]); -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 69b9a75ca1caab6f7e5f74685e550539be149ab6..41284df2235e869f49e542f5e1f2dcc240deb684 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -13,6 +13,7 @@ #include <sys/ioctl.h> - #include <termios.h> - #include <unistd.h> - #include "gmni.h" -+#include "tofu.h" - #include "url.h" - #include "util.h" - -@@ -29,6 +30,8 @@ - struct browser { - bool pagination, unicode; - struct gemini_options opts; -+ struct gemini_tofu tofu; -+ enum tofu_action tofu_mode; - - FILE *tty; - char *plain_url; -@@ -657,22 +660,113 @@ - return false; - } - -+static enum tofu_action -+tofu_callback(enum tofu_error error, const char *fingerprint, -+ struct known_host *host, void *data) -+{ -+ struct browser *browser = data; -+ if (browser->tofu_mode != TOFU_ASK) { -+ return browser->tofu_mode; -+ } -+ -+ static char prompt[8192]; -+ switch (error) { -+ case TOFU_VALID: -+ assert(0); // Invariant -+ case TOFU_INVALID_CERT: -+ snprintf(prompt, sizeof(prompt), -+ "The server presented an invalid certificate. If you choose to proceed, " -+ "you should not disclose personal information or trust the contents of the page.\n" -+ "trust [o]nce; [a]bort\n" -+ "=> "); -+ break; -+ case TOFU_UNTRUSTED_CERT: -+ snprintf(prompt, sizeof(prompt), -+ "The certificate offered by this server is of unknown trust. " -+ "Its fingerprint is: \n" -+ "%s\n\n" -+ "If you knew the fingerprint to expect in advance, verify that this matches.\n" -+ "Otherwise, it should be safe to trust this certificate.\n\n" -+ "[t]rust always; trust [o]nce; [a]bort\n" -+ "=> ", fingerprint); -+ break; -+ case TOFU_FINGERPRINT_MISMATCH: -+ snprintf(prompt, sizeof(prompt), -+ "The certificate offered by this server DOES NOT MATCH the one we have on file.\n" -+ "/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n" -+ "The unknown certificate's fingerprint is:\n" -+ "%s\n\n" -+ "The expected fingerprint is:\n" -+ "%s\n\n" -+ "If you're certain that this is correct, edit %s:%d\n", -+ fingerprint, host->fingerprint, -+ browser->tofu.known_hosts_path, host->lineno); -+ return TOFU_FAIL; -+ } -+ -+ bool prompting = true; -+ while (prompting) { -+ fprintf(browser->tty, "%s", prompt); -+ -+ size_t sz = 0; -+ char *line = NULL; -+ if (getline(&line, &sz, browser->tty) == -1) { -+ free(line); -+ return TOFU_FAIL; -+ } -+ if (line[1] != '\n') { -+ free(line); -+ continue; -+ } -+ -+ char c = line[0]; -+ free(line); -+ -+ switch (c) { -+ case 't': -+ if (error == TOFU_INVALID_CERT) { -+ break; -+ } -+ return TOFU_TRUST_ALWAYS; -+ case 'o': -+ return TOFU_TRUST_ONCE; -+ case 'a': -+ return TOFU_FAIL; -+ } -+ } -+ -+ return TOFU_FAIL; -+} -+ - int - main(int argc, char *argv[]) - { - struct browser browser = { - .pagination = true, -+ .tofu_mode = TOFU_ASK, - .unicode = true, - .url = curl_url(), - .tty = fopen("/dev/tty", "w+"), - }; - - int c; -- while ((c = getopt(argc, argv, "hPU")) != -1) { -+ while ((c = getopt(argc, argv, "hj:PU")) != -1) { - switch (c) { - case 'h': - usage(argv[0]); - return 0; -+ case 'j': -+ if (strcmp(optarg, "fail") == 0) { -+ browser.tofu_mode = TOFU_FAIL; -+ } else if (strcmp(optarg, "once") == 0) { -+ browser.tofu_mode = TOFU_TRUST_ONCE; -+ } else if (strcmp(optarg, "always") == 0) { -+ browser.tofu_mode = TOFU_TRUST_ALWAYS; -+ } else { -+ usage(argv[0]); -+ return 1; -+ } -+ break; - case 'P': - browser.pagination = false; - break; -@@ -695,6 +789,8 @@ - SSL_load_error_strings(); - ERR_load_crypto_strings(); - browser.opts.ssl_ctx = SSL_CTX_new(TLS_method()); -+ gemini_tofu_init(&browser.tofu, browser.opts.ssl_ctx, -+ &tofu_callback, &browser); - - struct gemini_response resp; - browser.running = true; -diff --git a/src/tofu.c b/src/tofu.c -new file mode 100644 -index 0000000000000000000000000000000000000000..e8efeaf69fcb9bd890711f76959e86efa75cfec6 ---- /dev/null -+++ b/src/tofu.c -@@ -0,0 +1,201 @@ -+#include <assert.h> -+#include <errno.h> -+#include <libgen.h> -+#include <limits.h> -+#include <openssl/asn1.h> -+#include <openssl/evp.h> -+#include <openssl/ssl.h> -+#include <openssl/x509.h> -+#include <stdio.h> -+#include <string.h> -+#include <time.h> -+#include "tofu.h" -+#include "util.h" -+ -+static int -+verify_callback(X509_STORE_CTX *ctx, void *data) -+{ -+ // Gemini clients handle TLS verification differently from the rest of -+ // the internet. We use a TOFU system, so trust is based on two factors: -+ // -+ // - Is the certificate valid at the time of the request? -+ // - Has the user trusted this certificate yet? -+ // -+ // If the answer to the latter is "no", then we give the user an -+ // opportunity to explicitly agree to trust the certificate before -+ // rejecting it. -+ // -+ // If you're reading this code with the intent to re-use it, think -+ // twice. -+ // -+ // TODO: Check that the subject name is valid for the requested URL. -+ struct gemini_tofu *tofu = (struct gemini_tofu *)data; -+ X509 *cert = X509_STORE_CTX_get0_cert(ctx); -+ -+ int rc; -+ int day, sec; -+ const ASN1_TIME *notBefore = X509_get0_notBefore(cert); -+ const ASN1_TIME *notAfter = X509_get0_notAfter(cert); -+ if (!ASN1_TIME_diff(&day, &sec, NULL, notBefore)) { -+ rc = X509_V_ERR_UNSPECIFIED; -+ goto invalid_cert; -+ } -+ if (day > 0 || sec > 0) { -+ rc = X509_V_ERR_CERT_NOT_YET_VALID; -+ goto invalid_cert; -+ } -+ if (!ASN1_TIME_diff(&day, &sec, NULL, notAfter)) { -+ rc = X509_V_ERR_UNSPECIFIED; -+ goto invalid_cert; -+ } -+ if (day < 0 || sec < 0) { -+ rc = X509_V_ERR_CERT_HAS_EXPIRED; -+ goto invalid_cert; -+ } -+ -+ unsigned char md[256 / 8]; -+ const EVP_MD *sha512 = EVP_sha512(); -+ unsigned int len = sizeof(md); -+ rc = X509_digest(cert, sha512, md, &len); -+ assert(rc == 1); -+ -+ char fingerprint[256 / 8 * 3]; -+ for (size_t i = 0; i < sizeof(md); ++i) { -+ snprintf(&fingerprint[i * 3], 4, "%02X%s", -+ md[i], i + 1 == sizeof(md) ? "" : ":"); -+ } -+ -+ SSL *ssl = X509_STORE_CTX_get_ex_data(ctx, -+ SSL_get_ex_data_X509_STORE_CTX_idx()); -+ const char *servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); -+ if (!servername) { -+ rc = X509_V_ERR_HOSTNAME_MISMATCH; -+ goto invalid_cert; -+ } -+ -+ time_t now; -+ time(&now); -+ -+ enum tofu_error error = TOFU_UNTRUSTED_CERT; -+ struct known_host *host = tofu->known_hosts; -+ while (host) { -+ if (host->expires < now) { -+ goto next; -+ } -+ if (strcmp(host->host, servername) != 0) { -+ goto next; -+ } -+ if (strcmp(host->fingerprint, fingerprint) == 0) { -+ // Valid match in known hosts -+ return 0; -+ } -+ error = TOFU_FINGERPRINT_MISMATCH; -+ break; -+next: -+ host = host->next; -+ } -+ -+ rc = X509_V_ERR_CERT_UNTRUSTED; -+ -+callback: -+ switch (tofu->callback(error, fingerprint, host, tofu->cb_data)) { -+ case TOFU_ASK: -+ assert(0); // Invariant -+ case TOFU_FAIL: -+ X509_STORE_CTX_set_error(ctx, rc); -+ break; -+ case TOFU_TRUST_ONCE: -+ // No further action necessary -+ return 0; -+ case TOFU_TRUST_ALWAYS:; -+ FILE *f = fopen(tofu->known_hosts_path, "a"); -+ if (!f) { -+ fprintf(stderr, "Error opening %s for writing: %s\n", -+ tofu->known_hosts_path, strerror(errno)); -+ break; -+ }; -+ struct tm expires_tm; -+ ASN1_TIME_to_tm(notAfter, &expires_tm); -+ time_t expires = mktime(&expires_tm); -+ fprintf(f, "%s %s %s %ld\n", servername, -+ "SHA-512", fingerprint, expires); -+ fclose(f); -+ -+ host = calloc(1, sizeof(struct known_host)); -+ host->host = strdup(servername); -+ host->fingerprint = strdup(fingerprint); -+ host->expires = expires; -+ host->lineno = ++tofu->lineno; -+ host->next = tofu->known_hosts; -+ tofu->known_hosts = host; -+ return 0; -+ } -+ -+ X509_STORE_CTX_set_error(ctx, rc); -+ return 0; -+ -+invalid_cert: -+ error = TOFU_INVALID_CERT; -+ goto callback; -+} -+ -+ -+void -+gemini_tofu_init(struct gemini_tofu *tofu, -+ SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *cb_data) -+{ -+ const struct pathspec paths[] = { -+ {.var = "GMNIDATA", .path = "/%s"}, -+ {.var = "XDG_DATA_HOME", .path = "/gmni/%s"}, -+ {.var = "HOME", .path = "/.local/share/gmni/%s"} -+ }; -+ const char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); -+ snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), -+ path_fmt, "known_hosts"); -+ -+ if (mkdirs(dirname(tofu->known_hosts_path), 0755) != 0) { -+ snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), -+ path_fmt, "known_hosts"); -+ fprintf(stderr, "Error creating directory %s: %s\n", -+ dirname(tofu->known_hosts_path), strerror(errno)); -+ return; -+ } -+ -+ snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), -+ path_fmt, "known_hosts"); -+ -+ tofu->callback = cb; -+ tofu->cb_data = cb_data; -+ SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_callback, tofu); -+ -+ FILE *f = fopen(tofu->known_hosts_path, "r"); -+ if (!f) { -+ return; -+ } -+ size_t n = 0; -+ char *line = NULL; -+ while (getline(&line, &n, f) != -1) { -+ struct known_host *host = calloc(1, sizeof(struct known_host)); -+ char *tok = strtok(line, " "); -+ assert(tok); -+ host->host = strdup(tok); -+ -+ tok = strtok(NULL, " "); -+ assert(tok); -+ if (strcmp(tok, "SHA-512") != 0) { -+ free(host); -+ continue; -+ } -+ -+ tok = strtok(NULL, " "); -+ assert(tok); -+ host->fingerprint = strdup(tok); -+ -+ tok = strtok(NULL, " "); -+ assert(tok); -+ host->expires = strtoul(tok, NULL, 10); -+ -+ host->next = tofu->known_hosts; -+ tofu->known_hosts = host; -+ } -+} diff --git a/sources/cgmnlm.git/commits/0513b91be1173b1ed43a0f1d28cf502a81267185.patch b/sources/cgmnlm.git/commits/0513b91be1173b1ed43a0f1d28cf502a81267185.patch @@ -1,14 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index c70a4126acc5337a768c3a70513aecc1b0e16668..95b85fedd9fb84b50bf1c8a89be94daaeb99a4a6 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -730,6 +730,9 @@ case '\r': - if (!s[i+1]) break; - /* fallthrough */ - default: -+ // skip unicode continuation bytes -+ if ((s[i] & 0xc0) == 0x80) break; -+ - if (iscntrl(s[i])) { - s[i] = '.'; - } diff --git a/sources/cgmnlm.git/commits/05cc8b85cdf731ea3a664b6099aad04f22bbca6c.patch b/sources/cgmnlm.git/commits/05cc8b85cdf731ea3a664b6099aad04f22bbca6c.patch @@ -1,49 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 129324718d13f8311f38cc3c80b7f0f3ccb3bd09..b7cc12dbbb391dd3e72e5e30d21a7e7957872f60 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -41,6 +41,14 @@ PROMPT_QUIT, - PROMPT_ANSWERED, - }; - -+const char *help_msg = -+ "The following commands are available:\n\n" -+ "q: Quit\n" -+ "N: Follow Nth link (where N is a number)\n" -+ "b: Back (in the page history)\n" -+ "f: Forward (in the page history)\n" -+ ; -+ - static void - usage(const char *argv_0) - { -@@ -100,6 +108,11 @@ if (strcmp(in, "q\n") == 0) { - result = PROMPT_QUIT; - goto exit; - } -+ if (strcmp(in, "?\n") == 0) { -+ fprintf(browser->tty, "%s", help_msg); -+ result = PROMPT_AGAIN; -+ goto exit; -+ } - if (strcmp(in, "b\n") == 0) { - if (!browser->history->prev) { - fprintf(stderr, "At beginning of history\n"); -@@ -291,7 +304,7 @@ - if (browser->pagination && row >= ws.ws_row - 4) { - char prompt[4096]; - snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -- "[Enter]: read more; [N]: follow Nth link; %s%s[q]uit; or type a URL\n" -+ "[Enter]: read more; [N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" - "(more) => ", resp->meta, browser->plain_url, - browser->history->prev ? "[b]ack; " : "", - browser->history->next ? "[f]orward; " : ""); -@@ -500,7 +513,7 @@ goto next; - } - - snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -- "[N]: follow Nth link; %s%s[q]uit; or type a URL\n" -+ "[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" - "=> ", - resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", - browser.plain_url, diff --git a/sources/cgmnlm.git/commits/05d112b7d347b737bdac503ea05292db3347f2a8.patch b/sources/cgmnlm.git/commits/05d112b7d347b737bdac503ea05292db3347f2a8.patch @@ -1,66 +0,0 @@ -diff --git a/src/gmni.c b/src/gmni.c -index 5b1f37522e93da360e74ba92ccb00c530bd41463..6e27b2f859c692f37fb483184cda245a7e979cbe 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -71,16 +71,20 @@ enum input_mode { - INPUT_READ, - INPUT_SUPPRESS, - }; -+ - enum input_mode input_mode = INPUT_READ; - FILE *input_source = stdin; -+ - bool follow_redirects = false, linefeed = true; -+ int max_redirect = 5; -+ - struct addrinfo hints = {0}; - struct gemini_options opts = { - .hints = &hints, - }; - - int c; -- while ((c = getopt(argc, argv, "46d:D:E:hlLiIN")) != -1) { -+ while ((c = getopt(argc, argv, "46d:D:E:hlLiINR:")) != -1) { - switch (c) { - case '4': - hints.ai_family = AF_INET; -@@ -127,6 +131,15 @@ break; - case 'N': - input_mode = INPUT_SUPPRESS; - break; -+ case 'R':; -+ char *endptr; -+ errno = 0; -+ max_redirect = strtoul(optarg, &endptr, 10); -+ if (*endptr || errno != 0) { -+ fprintf(stderr, "Error: -R expects numeric argument\n"); -+ return 1; -+ } -+ break; - default: - fprintf(stderr, "fatal: unknown flag %c\n", c); - return 1; -@@ -144,7 +157,7 @@ - bool exit = false; - char *url = strdup(argv[optind]); - -- int ret = 0; -+ int ret = 0, nredir = 0; - while (!exit) { - struct gemini_response resp; - enum gemini_result r = gemini_request(url, &opts, &resp); -@@ -177,6 +190,14 @@ free(url); - url = new_url; - goto next; - case GEMINI_STATUS_CLASS_REDIRECT: -+ if (++nredir >= max_redirect) { -+ fprintf(stderr, -+ "Error: maximum redirects (%d) exceeded", -+ max_redirect); -+ exit = true; -+ goto next; -+ } -+ - free(url); - url = strdup(resp.meta); - if (!follow_redirects) { diff --git a/sources/cgmnlm.git/commits/0976b0e44655163a34d1b53e62a348cbf4335940.patch b/sources/cgmnlm.git/commits/0976b0e44655163a34d1b53e62a348cbf4335940.patch @@ -1,13 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 199572c363f9e50acdd052111350f3926d6a350d..5d45ffe7372f71ab32461bf71a590cc494269e96 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -246,7 +246,7 @@ open_bookmarks(browser); - result = PROMPT_ANSWERED; - goto exit; - case '/': -- if (in[1]) break; -+ if (!in[1]) break; - if ((r = regcomp(&browser->regex, &in[1], REG_EXTENDED)) != 0) { - static char buf[1024]; - r = regerror(r, &browser->regex, buf, sizeof(buf)); diff --git a/sources/cgmnlm.git/commits/0a03e6dadf7c30cea1fb388a9e5386a00c853dbb.patch b/sources/cgmnlm.git/commits/0a03e6dadf7c30cea1fb388a9e5386a00c853dbb.patch @@ -1,12 +0,0 @@ -diff --git a/src/parser.c b/src/parser.c -index 2f78e4641c08b86f5dc50cc9776ea80ec7f9aead..04501b6b644af28abe7843076ae593b7165912b2 100644 ---- a/src/parser.c -+++ b/src/parser.c -@@ -113,6 +113,7 @@ 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; diff --git a/sources/cgmnlm.git/commits/0b5c37d2e65a46fe8e4a49c2f00cb6228fad59e3.patch b/sources/cgmnlm.git/commits/0b5c37d2e65a46fe8e4a49c2f00cb6228fad59e3.patch @@ -1,13 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index da99a84580894312ba86b92e6abd02cfdfc80fd4..26654324b4fb845633f7db2b2ac01e5e3b4635a1 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -493,7 +493,7 @@ col += fprintf(out, " "); - } - break; - case GEMINI_QUOTE: -- col += fprintf(out, "%s ", -+ col += fprintf(out, " %s ", - browser->unicode ? "┃" : ">"); - if (text == NULL) { - text = trim_ws(tok.quote_text); diff --git a/sources/cgmnlm.git/commits/0eaf9cc109a99d6efb0d9c763291f6a5d9e74391.patch b/sources/cgmnlm.git/commits/0eaf9cc109a99d6efb0d9c763291f6a5d9e74391.patch @@ -1,15 +0,0 @@ -diff --git a/src/tofu.c b/src/tofu.c -index 45e1275d568cfcb4aacfe7f86788b76fff97916b..4eeb7fd0eb8df7423fe8fd8f106e2fe433fa8716 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -147,8 +147,8 @@ SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *cb_data) - { - const struct pathspec paths[] = { - {.var = "GMNIDATA", .path = "/%s"}, -- {.var = "XDG_DATA_HOME", .path = "/gmni/%s"}, -- {.var = "HOME", .path = "/.local/share/gmni/%s"} -+ {.var = "XDG_DATA_HOME", .path = "/gemini/%s"}, -+ {.var = "HOME", .path = "/.local/share/gemini/%s"} - }; - char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); - snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), diff --git a/sources/cgmnlm.git/commits/0ed7a4527c967ce3f14909923277cf62624f0900.patch b/sources/cgmnlm.git/commits/0ed7a4527c967ce3f14909923277cf62624f0900.patch @@ -1,28 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index a021a97298bb41bff8cdbbceca172f561b64c7a5..8e85d091e54e3c3c11dbd6980ae660be6838c7c8 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -349,7 +349,7 @@ case 0: - close(pfd[1]); - dup2(pfd[0], STDIN_FILENO); - close(pfd[0]); -- execlp("sh", "sh", "-c", cmd); -+ execlp("sh", "sh", "-c", cmd, NULL); - perror("exec"); - _exit(1); - } -diff --git a/src/tofu.c b/src/tofu.c -index b9100c77fd61c71ce561f2194b991eee7130e689..ba5493352968b563ad76e0ecd25bab6370d2f6e5 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -124,8 +124,8 @@ }; - struct tm expires_tm; - ASN1_TIME_to_tm(notAfter, &expires_tm); - time_t expires = mktime(&expires_tm); -- fprintf(f, "%s %s %s %ld\n", servername, -- "SHA-512", fingerprint, expires); -+ fprintf(f, "%s %s %s %jd\n", servername, -+ "SHA-512", fingerprint, (intmax_t)expires); - fclose(f); - - host = calloc(1, sizeof(struct known_host)); diff --git a/sources/cgmnlm.git/commits/100759a7d796f4e486a89b65b3ca491c1141056f.patch b/sources/cgmnlm.git/commits/100759a7d796f4e486a89b65b3ca491c1141056f.patch @@ -1,34 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 930ab19abe717b186070151a184b2a8e98542f77..1292bb6b8f526f923e9783ba8c0a014c2743af05 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -1106,7 +1106,7 @@ "=> ", host, fingerprint); - free(host); - break; - case TOFU_FINGERPRINT_MISMATCH: -- snprintf(prompt, sizeof(prompt), -+ fprintf(browser->tty, - "The certificate offered by this server DOES NOT MATCH the one we have on file.\n" - "/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n" - "The unknown certificate's fingerprint is:\n" -diff --git a/src/tofu.c b/src/tofu.c -index ba5493352968b563ad76e0ecd25bab6370d2f6e5..48395c08cdbf68b31c5defd15b360f1eb2897a3f 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -188,6 +188,7 @@ if (!f) { - return; - } - n = 0; -+ int lineno = 1; - char *line = NULL; - while (getline(&line, &n, f) != -1) { - struct known_host *host = calloc(1, sizeof(struct known_host)); -@@ -210,6 +211,8 @@ - tok = strtok(NULL, " "); - assert(tok); - host->expires = strtoul(tok, NULL, 10); -+ -+ host->lineno = lineno++; - - host->next = tofu->known_hosts; - tofu->known_hosts = host; diff --git a/sources/cgmnlm.git/commits/122fb0a9fd5456e3b1fd9f084130df85c859394b.patch b/sources/cgmnlm.git/commits/122fb0a9fd5456e3b1fd9f084130df85c859394b.patch @@ -1,562 +0,0 @@ -diff --git a/.gitignore b/.gitignore -index 6ec8d5188d32b997f3a91f8a11f64f4e79b43770..4a40b62507e740b789fdb6d2d4a63c41e3b102c8 100644 ---- a/.gitignore -+++ b/.gitignore -@@ -4,3 +4,5 @@ gmni - gmnlm - *.1 - *.o -+*.a -+*.pc -diff --git a/Makefile b/Makefile -index 5ac44dbda9d629823f99124f67ace8125e9bc697..22fed5d19ec4f989ead26d22d3d1887614e4927c 100644 ---- a/Makefile -+++ b/Makefile -@@ -1,6 +1,7 @@ - .POSIX: - .SUFFIXES: - OUTDIR=.build -+VERSION=0.0.0 - include $(OUTDIR)/config.mk - include $(OUTDIR)/cppcache - -@@ -12,9 +13,26 @@ gmnlm: $(gmnlm_objects) - @printf 'CCLD\t$@\n' - @$(CC) $(LDFLAGS) -o $@ $(gmnlm_objects) $(LIBS) - -+libgmni.a: $(libgmni.a_objects) -+ @printf 'AR\t$@\n' -+ @$(AR) -rcs $@ $(libgmni.a_objects) -+ - doc/gmni.1: doc/gmni.scd - doc/gmnlm.1: doc/gmnlm.scd - -+libgmni.pc: -+ @printf 'GEN\t$@\n' -+ @printf 'prefix=%s\n' "$(PREFIX)" > $@ -+ @printf 'exec_prefix=$${prefix}\n' >> $@ -+ @printf 'includedir=$${prefix}/include\n' >> $@ -+ @printf 'libdir=$${prefix}/lib\n' >> $@ -+ @printf 'Name: libgmni\n' >> $@ -+ @printf 'Version: %s\n' "$(VERSION)" >> $@ -+ @printf 'Description: The gmni client library\n' >> $@ -+ @printf 'Requires: libssl libcrypto\n' >> $@ -+ @printf 'Cflags: -I$${includedir}/gmni\n' >> $@ -+ @printf 'Libs: -L$${libdir} -lgmni\n' >> $@ -+ - .SUFFIXES: .c .o .scd .1 - - .c.o: -@@ -22,7 +40,7 @@ @printf 'CC\t$@\n' - @touch $(OUTDIR)/cppcache - @grep $< $(OUTDIR)/cppcache >/dev/null || \ - $(CPP) $(CFLAGS) -MM -MT $@ $< >> $(OUTDIR)/cppcache -- @$(CC) -c $(CFLAGS) -o $@ $< -+ @$(CC) -c -fPIC $(CFLAGS) -o $@ $< - - .scd.1: - @printf 'SCDOC\t$@\n' -@@ -31,7 +49,7 @@ - docs: doc/gmni.1 doc/gmnlm.1 - - clean: -- @rm -f gmni gmnlm doc/gmni.1 doc/gmnlm.1 $(gmnlm_objects) $(gmni_objects) -+ @rm -f gmni gmnlm libgmni.a libgmni.pc doc/gmni.1 doc/gmnlm.1 $(gmnlm_objects) $(gmni_objects) - - distclean: clean - @rm -rf "$(OUTDIR)" -@@ -41,6 +59,11 @@ mkdir -p $(BINDIR) - mkdir -p $(MANDIR)/man1 - install -Dm755 gmni $(BINDIR)/gmni - install -Dm755 gmnlm $(BINDIR)/gmnlm -+ install -Dm755 libgmni.a $(LIBDIR)/libgmni.a -+ install -Dm644 include/gmni/gmni.h $(INCLUDEDIR)/gmni/gmni.h -+ install -Dm644 include/gmni/tofu.h $(INCLUDEDIR)/gmni/tofu.h -+ install -Dm644 include/gmni/url.h $(INCLUDEDIR)/gmni/url.h -+ install -Dm644 libgmni.pc $(LIBDIR)/pkgconfig - install -Dm644 doc/gmni.1 $(MANDIR)/man1/gmni.1 - install -Dm644 doc/gmnlm.1 $(MANDIR)/man1/gmnlm.1 - -diff --git a/config.sh b/config.sh -index b03dfffbc5976e69d2d878dbf3b6c545ebcad377..a6963622d4b41d6e25bb4fe230c2513849c077d3 100644 ---- a/config.sh -+++ b/config.sh -@@ -134,6 +134,7 @@ OUTDIR=${outdir} - _INSTDIR=\$(DESTDIR)\$(PREFIX) - BINDIR?=${BINDIR:-\$(_INSTDIR)/bin} - LIBDIR?=${LIBDIR:-\$(_INSTDIR)/lib} -+ INCLUDEDIR?=${INCLUDEDIR:-\$(_INSTDIR)/include} - MANDIR?=${MANDIR:-\$(_INSTDIR)/share/man} - CACHE=\$(OUTDIR)/cache - CFLAGS=${CFLAGS} -@@ -146,7 +147,7 @@ EOF - - for target in $all - do -- $target >>"$outdir"/config.mk -+ ${target//./_} >>"$outdir"/config.mk - done - echo done - -diff --git a/configure b/configure -index 44db11c4765077677a1f506f034c846cc736ab8c..e82a0e27e27c9873c8dac92fd3385e65bf511648 100755 ---- a/configure -+++ b/configure -@@ -23,6 +23,21 @@ src/url.c \ - src/util.c - } - --all="gmni gmnlm" -+libgmni_a() { -+ genrules libgmni.a \ -+ src/client.c \ -+ src/escape.c \ -+ src/tofu.c \ -+ src/url.c \ -+ src/util.c \ -+ src/parser.c -+} -+ -+libgmni_pc() { -+ : -+} -+ -+all="gmni gmnlm libgmni.a libgmni.pc" -+ - - run_configure -diff --git a/include/gmni.h b/include/gmni.h -deleted file mode 100644 -index 7e27b489d71fd3a43ca60292b17d56cab3caa5f8..0000000000000000000000000000000000000000 ---- a/include/gmni.h -+++ /dev/null -@@ -1,164 +0,0 @@ --#ifndef GEMINI_CLIENT_H --#define GEMINI_CLIENT_H --#include <netdb.h> --#include <openssl/ssl.h> --#include <stdbool.h> --#include <sys/socket.h> -- --enum gemini_result { -- GEMINI_OK, -- GEMINI_ERR_OOM, -- GEMINI_ERR_INVALID_URL, -- GEMINI_ERR_NOT_GEMINI, -- GEMINI_ERR_RESOLVE, -- GEMINI_ERR_CONNECT, -- GEMINI_ERR_SSL, -- GEMINI_ERR_SSL_VERIFY, -- GEMINI_ERR_IO, -- GEMINI_ERR_PROTOCOL, --}; -- --enum gemini_status { -- GEMINI_STATUS_INPUT = 10, -- GEMINI_STATUS_SENSITIVE_INPUT = 11, -- GEMINI_STATUS_SUCCESS = 20, -- GEMINI_STATUS_REDIRECT_TEMPORARY = 30, -- GEMINI_STATUS_REDIRECT_PERMANENT = 31, -- GEMINI_STATUS_TEMPORARY_FAILURE = 40, -- GEMINI_STATUS_SERVER_UNAVAILABLE = 41, -- GEMINI_STATUS_CGI_ERROR = 42, -- GEMINI_STATUS_PROXY_ERROR = 43, -- GEMINI_STATUS_SLOW_DOWN = 44, -- GEMINI_STATUS_PERMANENT_FAILURE = 50, -- GEMINI_STATUS_NOT_FOUND = 51, -- GEMINI_STATUS_GONE = 52, -- GEMINI_STATUS_PROXY_REQUEST_REFUSED = 53, -- GEMINI_STATUS_BAD_REQUEST = 59, -- GEMINI_STATUS_CLIENT_CERTIFICATE_REQUIRED = 60, -- GEMINI_STATUS_CERTIFICATE_NOT_AUTHORIZED = 61, -- GEMINI_STATUS_CERTIFICATE_NOT_VALID = 62, --}; -- --enum gemini_status_class { -- GEMINI_STATUS_CLASS_INPUT = 10, -- GEMINI_STATUS_CLASS_SUCCESS = 20, -- GEMINI_STATUS_CLASS_REDIRECT = 30, -- GEMINI_STATUS_CLASS_TEMPORARY_FAILURE = 40, -- GEMINI_STATUS_CLASS_PERMANENT_FAILURE = 50, -- GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED = 60, --}; -- --struct gemini_response { -- enum gemini_status status; -- char *meta; -- -- // Response body may be read from here if appropriate: -- BIO *bio; -- -- // Connection state -- SSL_CTX *ssl_ctx; -- SSL *ssl; -- int fd; --}; -- --struct gemini_options { -- // If NULL, an SSL context will be created. If unset, the ssl field -- // must also be NULL. -- SSL_CTX *ssl_ctx; -- -- // If ai_family != AF_UNSPEC (the default value on most systems), the -- // client will connect to this address and skip name resolution. -- struct addrinfo *addr; -- -- // If non-NULL, these hints are provided to getaddrinfo. Useful, for -- // example, to force IPv4/IPv6. -- struct addrinfo *hints; --}; -- --// Requests the specified URL via the gemini protocol. If options is non-NULL, --// it may specify some additional configuration to adjust client behavior. --// --// Returns a value indicating the success of the request. --// --// Caller must call gemini_response_finish afterwards to clean up resources --// before exiting or re-using it for another request. --enum gemini_result gemini_request(const char *url, -- struct gemini_options *options, -- struct gemini_response *resp); -- --// Must be called after gemini_request in order to free up the resources --// allocated during the request. --void gemini_response_finish(struct gemini_response *resp); -- --// Returns a user-friendly string describing an error. --const char *gemini_strerr(enum gemini_result r, struct gemini_response *resp); -- --// Returns the given URL with the input response set to the specified value. --// The caller must free the string. --char *gemini_input_url(const char *url, const char *input); -- --// Returns the general response class (i.e. with the second digit set to zero) --// of the given Gemini status code. --enum gemini_status_class gemini_response_class(enum gemini_status status); -- --enum gemini_tok { -- GEMINI_TEXT, -- GEMINI_LINK, -- GEMINI_PREFORMATTED_BEGIN, -- GEMINI_PREFORMATTED_END, -- GEMINI_PREFORMATTED_TEXT, -- GEMINI_HEADING, -- GEMINI_LIST_ITEM, -- GEMINI_QUOTE, --}; -- --struct gemini_token { -- enum gemini_tok token; -- -- // The token field determines which of the union members is valid. -- union { -- char *text; -- -- struct { -- char *text; -- char *url; // May be NULL -- } link; -- -- char *preformatted; -- -- struct { -- char *title; -- int level; // 1, 2, or 3 -- } heading; -- -- char *list_item; -- char *quote_text; -- }; --}; -- --struct gemini_parser { -- BIO *f; -- char *buf; -- size_t bufsz; -- size_t bufln; -- bool preformatted; --}; -- --// Initializes a text/gemini parser which reads from the specified BIO. --void gemini_parser_init(struct gemini_parser *p, BIO *f); -- --// Finishes this text/gemini parser and frees up its resources. --void gemini_parser_finish(struct gemini_parser *p); -- --// Reads the next token from a text/gemini file. --// --// Returns 0 on success, 1 on EOF, and -1 on failure. --// --// Caller must call gemini_token_finish before exiting or re-using the token --// 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. --void gemini_token_finish(struct gemini_token *token); -- --#endif -diff --git a/include/tofu.h b/include/tofu.h -deleted file mode 100644 -index a88167ba0fb6606b2b170e5005c55131f1861972..0000000000000000000000000000000000000000 ---- a/include/tofu.h -+++ /dev/null -@@ -1,49 +0,0 @@ --#ifndef GEMINI_TOFU_H --#define GEMINI_TOFU_H --#include <limits.h> --#include <openssl/ssl.h> --#include <openssl/x509.h> --#include <time.h> -- --enum tofu_error { -- TOFU_VALID, -- // Expired, wrong CN, etc. -- TOFU_INVALID_CERT, -- // Cert is valid but we haven't seen it before -- TOFU_UNTRUSTED_CERT, -- // Cert is valid but we already trust another cert for this host -- TOFU_FINGERPRINT_MISMATCH, --}; -- --enum tofu_action { -- TOFU_ASK, -- TOFU_FAIL, -- TOFU_TRUST_ONCE, -- TOFU_TRUST_ALWAYS, --}; -- --struct known_host { -- char *host, *fingerprint; -- time_t expires; -- int lineno; -- struct known_host *next; --}; -- --// Called when the user needs to be prompted to agree to trust an unknown --// certificate. Return true to trust this certificate. --typedef enum tofu_action (tofu_callback_t)(enum tofu_error error, -- const char *fingerprint, struct known_host *host, void *data); -- --struct gemini_tofu { -- char known_hosts_path[PATH_MAX+1]; -- struct known_host *known_hosts; -- int lineno; -- tofu_callback_t *callback; -- void *cb_data; --}; -- --void gemini_tofu_init(struct gemini_tofu *tofu, -- SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *data); --void gemini_tofu_finish(struct gemini_tofu *tofu); -- --#endif -diff --git a/include/url.h b/include/url.h -deleted file mode 100644 -index 155fd55740dbe47a498062713aca217ab259734f..0000000000000000000000000000000000000000 ---- a/include/url.h -+++ /dev/null -@@ -1,103 +0,0 @@ --#ifndef URLAPI_H --#define URLAPI_H --/*************************************************************************** -- * _ _ ____ _ -- * Project ___| | | | _ \| | -- * / __| | | | |_) | | -- * | (__| |_| | _ <| |___ -- * \___|\___/|_| \_\_____| -- * -- * Copyright (C) 2018, Daniel Stenberg, <daniel@haxx.se>, et al. -- * -- * This software is licensed as described in the file COPYING, which -- * you should have received as part of this distribution. The terms -- * are also available at https://curl.haxx.se/docs/copyright.html. -- * -- * You may opt to use, copy, modify, merge, publish, distribute and/or sell -- * copies of the Software, and permit persons to whom the Software is -- * furnished to do so, under the terms of the COPYING file. -- * -- * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY -- * KIND, either express or implied. -- * -- ***************************************************************************/ -- --/* the error codes for the URL API */ --typedef enum { -- CURLUE_OK, -- CURLUE_BAD_HANDLE, /* 1 */ -- CURLUE_BAD_PARTPOINTER, /* 2 */ -- CURLUE_MALFORMED_INPUT, /* 3 */ -- CURLUE_BAD_PORT_NUMBER, /* 4 */ -- CURLUE_UNSUPPORTED_SCHEME, /* 5 */ -- CURLUE_URLDECODE, /* 6 */ -- CURLUE_OUT_OF_MEMORY, /* 7 */ -- CURLUE_USER_NOT_ALLOWED, /* 8 */ -- CURLUE_UNKNOWN_PART, /* 9 */ -- CURLUE_NO_SCHEME, /* 10 */ -- CURLUE_NO_USER, /* 11 */ -- CURLUE_NO_PASSWORD, /* 12 */ -- CURLUE_NO_OPTIONS, /* 13 */ -- CURLUE_NO_HOST, /* 14 */ -- CURLUE_NO_PORT, /* 15 */ -- CURLUE_NO_QUERY, /* 16 */ -- CURLUE_NO_FRAGMENT /* 17 */ --} CURLUcode; -- --typedef enum { -- CURLUPART_URL, -- CURLUPART_SCHEME, -- CURLUPART_USER, -- CURLUPART_PASSWORD, -- CURLUPART_OPTIONS, -- CURLUPART_HOST, -- CURLUPART_PORT, -- CURLUPART_PATH, -- CURLUPART_QUERY, -- CURLUPART_FRAGMENT --} CURLUPart; -- --#define CURLU_PATH_AS_IS (1<<4) /* leave dot sequences */ --#define CURLU_DISALLOW_USER (1<<5) /* no user+password allowed */ --#define CURLU_URLDECODE (1<<6) /* URL decode on get */ --#define CURLU_URLENCODE (1<<7) /* URL encode on set */ --#define CURLU_APPENDQUERY (1<<8) /* append a form style part */ -- --typedef struct Curl_URL CURLU; -- --/* -- * curl_url() creates a new CURLU handle and returns a pointer to it. -- * Must be freed with curl_url_cleanup(). -- */ --struct Curl_URL *curl_url(void); -- --/* -- * curl_url_cleanup() frees the CURLU handle and related resources used for -- * the URL parsing. It will not free strings previously returned with the URL -- * API. -- */ --void curl_url_cleanup(struct Curl_URL *handle); -- --/* -- * curl_url_dup() duplicates a CURLU handle and returns a new copy. The new -- * handle must also be freed with curl_url_cleanup(). -- */ --struct Curl_URL *curl_url_dup(struct Curl_URL *in); -- --/* -- * curl_url_get() extracts a specific part of the URL from a CURLU -- * handle. Returns error code. The returned pointer MUST be freed with -- * free() afterwards. -- */ --CURLUcode curl_url_get(struct Curl_URL *handle, CURLUPart what, -- char **part, unsigned int flags); -- --/* -- * curl_url_set() sets a specific part of the URL in a CURLU handle. Returns -- * error code. The passed in string will be copied. Passing a NULL instead of -- * a part string, clears that part. -- */ --CURLUcode curl_url_set(struct Curl_URL *handle, CURLUPart what, -- const char *part, unsigned int flags); -- --#endif -diff --git a/src/client.c b/src/client.c -index 398e133b182af7f2fbe907be83cd5ecdc1b3a671..8b6b9e7a3c09656a5d59ed86977cb8b5c39541d4 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -9,8 +9,8 @@ #include <string.h> - #include <sys/socket.h> - #include <sys/types.h> - #include <unistd.h> --#include "gmni.h" --#include "url.h" -+#include <gmni/gmni.h> -+#include <gmni/url.h> - - static enum gemini_result - gemini_get_addrinfo(struct Curl_URL *uri, struct gemini_options *options, -diff --git a/src/gmni.c b/src/gmni.c -index 61f41e29c324d980bd3a1f270e78649cdcec88e0..e8b25f9060c021bda099aa97fc2ec5657aeb2672 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -12,8 +12,8 @@ #include <sys/socket.h> - #include <sys/types.h> - #include <termios.h> - #include <unistd.h> --#include "gmni.h" --#include "tofu.h" -+#include <gmni/gmni.h> -+#include <gmni/tofu.h> - #include "util.h" - - static void -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 245ec85fa59fbcabde7e49c7d4d6e978aefa5f9a..2fba84e1f3c9e312ac53417d1b873158ceaf8c73 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -14,9 +14,9 @@ #include <sys/stat.h> - #include <sys/wait.h> - #include <termios.h> - #include <unistd.h> --#include "gmni.h" --#include "tofu.h" --#include "url.h" -+#include <gmni/gmni.h> -+#include <gmni/tofu.h> -+#include <gmni/url.h> - #include "util.h" - - struct link { -diff --git a/src/parser.c b/src/parser.c -index 579415150f842557b6faf4097a63aac9324928fb..ad2c0e6306872f8dde450063ba8815e2ee7e314a 100644 ---- a/src/parser.c -+++ b/src/parser.c -@@ -5,7 +5,7 @@ #include <stdbool.h> - #include <stddef.h> - #include <stdlib.h> - #include <string.h> --#include "gmni.h" -+#include <gmni/gmni.h> - - void - gemini_parser_init(struct gemini_parser *p, BIO *f) -diff --git a/src/tofu.c b/src/tofu.c -index 48a627fe131996070d991bcc54bb7bcb40fddce4..863efc644a691370ee58ef0fb92291e9471adeb9 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -10,8 +10,8 @@ #include <openssl/x509v3.h> - #include <stdio.h> - #include <string.h> - #include <time.h> --#include "gmni.h" --#include "tofu.h" -+#include <gmni/gmni.h> -+#include <gmni/tofu.h> - #include "util.h" - - static int -diff --git a/src/url.c b/src/url.c -index dabf45f234e1e0957007b94ef12ef1125250dfea..47741e4ab1f91d336e5aba8117bde01094e370c3 100644 ---- a/src/url.c -+++ b/src/url.c -@@ -31,7 +31,7 @@ #include <stdlib.h> - #include <string.h> - #include <strings.h> - #include "escape.h" --#include "url.h" -+#include <gmni/url.h> - - /* Provided by gmni */ - static char * -diff --git a/src/util.c b/src/util.c -index 360c99ac95de9dcfce860610aeef85e8986e99c2..0a479af3ea734ce25d0806aa086e61bef6b8953b 100644 ---- a/src/util.c -+++ b/src/util.c -@@ -7,7 +7,7 @@ #include <stdio.h> - #include <stdlib.h> - #include <string.h> - #include <sys/stat.h> --#include "gmni.h" -+#include <gmni/gmni.h> - #include "util.h" - - static void diff --git a/sources/cgmnlm.git/commits/144693a3d001a436abaa37f11b1c1c2bdf88c813.patch b/sources/cgmnlm.git/commits/144693a3d001a436abaa37f11b1c1c2bdf88c813.patch @@ -1,47 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 8841c4613e208e379c6cd768a65a9a3b631d3146..8fc92928ba0d8532a5f94f8e637d0b0aa025bc09 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -78,9 +78,13 @@ - static bool - set_url(struct browser *browser, char *new_url, struct history **history) - { -+ if (curl_url_set(browser->url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { -+ fprintf(stderr, "Error: invalid URL\n"); -+ return false; -+ } - if (history) { - struct history *next = calloc(1, sizeof(struct history)); -- next->url = strdup(new_url); -+ curl_url_get(browser->url, CURLUPART_URL, &next->url, 0); - next->prev = *history; - if (*history) { - if ((*history)->next) { -@@ -89,10 +93,6 @@ } - (*history)->next = next; - } - *history = next; -- } -- if (curl_url_set(browser->url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { -- fprintf(stderr, "Error: invalid URL\n"); -- return false; - } - return true; - } -@@ -153,8 +153,14 @@ result = PROMPT_ANSWERED; - } - goto exit_re; - case 'n': -- result = PROMPT_NEXT; -- goto exit_re; -+ if (browser->searching) { -+ result = PROMPT_NEXT; -+ goto exit_re; -+ } else { -+ fprintf(stderr, "Cannot move to next result; we are not searching for anything\n"); -+ result = PROMPT_AGAIN; -+ goto exit; -+ } - case '?': - fprintf(browser->tty, "%s", help_msg); - result = PROMPT_AGAIN; diff --git a/sources/cgmnlm.git/commits/174fbd5d09bc13212fc1edc0cd1d3fa2400a8b7e.patch b/sources/cgmnlm.git/commits/174fbd5d09bc13212fc1edc0cd1d3fa2400a8b7e.patch @@ -1,365 +0,0 @@ -diff --git a/include/tofu.h b/include/tofu.h -index 29aa9bc21567868cafb25a09dbc25ea0685ab01c..a88167ba0fb6606b2b170e5005c55131f1861972 100644 ---- a/include/tofu.h -+++ b/include/tofu.h -@@ -44,5 +44,6 @@ }; - - void gemini_tofu_init(struct gemini_tofu *tofu, - SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *data); -+void gemini_tofu_finish(struct gemini_tofu *tofu); - - #endif -diff --git a/src/client.c b/src/client.c -index 2d39f56cb8e45f7b708e5a5dea1ec6fa84e188c9..bb508e04c8d41fe12aa99ae42b8fa30d4f3a9a2e 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -118,11 +118,14 @@ goto cleanup; - } else { - if (strcmp(scheme, "gemini") != 0) { - res = GEMINI_ERR_NOT_GEMINI; -+ free(scheme); - goto cleanup; - } -+ free(scheme); - } - if (curl_url_get(uri, CURLUPART_HOST, &host, 0) != CURLUE_OK) { - res = GEMINI_ERR_INVALID_URL; -+ free(host); - goto cleanup; - } - -@@ -139,6 +142,7 @@ int r; - BIO *sbio = BIO_new(BIO_f_ssl()); - res = gemini_connect(uri, options, resp, &resp->fd); - if (res != GEMINI_OK) { -+ free(host); - goto cleanup; - } - -@@ -146,11 +150,14 @@ resp->ssl = SSL_new(resp->ssl_ctx); - assert(resp->ssl); - SSL_set_connect_state(resp->ssl); - if ((r = SSL_set1_host(resp->ssl, host)) != 1) { -+ free(host); - goto ssl_error; - } - if ((r = SSL_set_tlsext_host_name(resp->ssl, host)) != 1) { -+ free(host); - goto ssl_error; - } -+ free(host); - if ((r = SSL_set_fd(resp->ssl, resp->fd)) != 1) { - goto ssl_error; - } -@@ -235,15 +242,16 @@ resp->fd = -1; - } - - if (resp->bio) { -- BIO_free(BIO_pop(resp->bio)); // ssl bio -- BIO_free(resp->bio); // buffered bio -+ BIO_free_all(resp->bio); - resp->bio = NULL; - } - - if (resp->ssl) { - SSL_free(resp->ssl); - } -- SSL_CTX_free(resp->ssl_ctx); -+ if (resp->ssl_ctx) { -+ SSL_CTX_free(resp->ssl_ctx); -+ } - free(resp->meta); - - resp->ssl = NULL; -diff --git a/src/gmni.c b/src/gmni.c -index c13e0cd55623f557a8dc676d69aea54661b11faf..4af98fbbe70b09ce5a7fce46863438b0487d8738 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -336,6 +336,8 @@ next: - gemini_response_finish(&resp); - } - -+ SSL_CTX_free(opts.ssl_ctx); - free(url); -+ gemini_tofu_finish(&cfg.tofu); - return ret; - } -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 5d45ffe7372f71ab32461bf71a590cc494269e96..a2717fb84c76fd50a000ceda540af54c60396252 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -83,6 +83,7 @@ if (!history) { - return; - } - history_free(history->next); -+ free(history->url); - free(history); - } - -@@ -92,6 +93,9 @@ { - if (curl_url_set(browser->url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { - fprintf(stderr, "Error: invalid URL\n"); - return false; -+ } -+ if (browser->plain_url != NULL) { -+ free(browser->plain_url); - } - curl_url_get(browser->url, CURLUPART_URL, &browser->plain_url, 0); - if (history) { -@@ -130,17 +134,19 @@ - static void - save_bookmark(struct browser *browser) - { -- const char *path_fmt = get_data_pathfmt(); -+ char *path_fmt = get_data_pathfmt(); - static char path[PATH_MAX+1]; - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - if (mkdirs(dirname(path), 0755) != 0) { - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ free(path_fmt); - fprintf(stderr, "Error creating directory %s: %s\n", - dirname(path), strerror(errno)); - return; - } - - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ free(path_fmt); - FILE *f = fopen(path, "a"); - if (!f) { - fprintf(stderr, "Error opening %s for writing: %s\n", -@@ -150,7 +156,7 @@ } - - char *title = browser->page_title; - if (title) { -- title = trim_ws(browser->page_title); -+ title = trim_ws(strdup(browser->page_title)); - } - - fprintf(f, "=> %s%s%s\n", browser->plain_url, -@@ -159,6 +165,9 @@ fclose(f); - - fprintf(browser->tty, "Bookmark saved: %s\n", - title ? title : browser->plain_url); -+ if (title != NULL) { -+ free(title); -+ } - } - - static void -@@ -411,12 +420,14 @@ col += fprintf(out, " "); - } - break; - case GEMINI_PREFORMATTED_BEGIN: -+ gemini_token_finish(&tok); -+ /* fallthrough */ - case GEMINI_PREFORMATTED_END: - continue; // Not used - case GEMINI_PREFORMATTED_TEXT: - col += fprintf(out, "` "); - if (text == NULL) { -- text = tok.text; -+ text = tok.preformatted; - } - break; - case GEMINI_HEADING: -@@ -484,6 +495,9 @@ if (!text[0]) { - text = NULL; - } - } -+ if (text == NULL) { -+ gemini_token_finish(&tok); -+ } - - while (col >= ws.ws_col) { - col -= ws.ws_col; -@@ -510,8 +524,16 @@ case PROMPT_MORE: - break; - case PROMPT_QUIT: - browser->running = false; -+ 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; -@@ -523,6 +545,7 @@ row = col = 0; - } - } - -+ gemini_token_finish(&tok); - gemini_parser_finish(&p); - return false; - } -@@ -617,6 +640,7 @@ CURLUcode uc = curl_url_get(browser->url, - CURLUPART_SCHEME, &scheme, 0); - assert(uc == CURLUE_OK); // Invariant - if (strcmp(scheme, "file") == 0) { -+ free(scheme); - requesting = false; - - char *path; -@@ -630,6 +654,7 @@ - FILE *fp = fopen(path, "r"); - if (!fp) { - resp->status = GEMINI_STATUS_NOT_FOUND; -+ free(path); - break; - } - -@@ -643,9 +668,14 @@ resp->meta = strdup("text/plain"); - } else { - resp->meta = strdup("application/x-octet-stream"); - } -+ free(path); - resp->status = GEMINI_STATUS_SUCCESS; -+ resp->fd = -1; -+ resp->ssl = NULL; -+ resp->ssl_ctx = NULL; - return display_response(browser, resp); - } -+ free(scheme); - - enum gemini_result res = gemini_request(browser->plain_url, - &browser->opts, resp); -@@ -672,7 +702,7 @@ break; - case GEMINI_STATUS_CLASS_REDIRECT: - if (++nredir >= 5) { - requesting = false; -- fprintf(stderr, "Error: maximum redirects (5) exceeded"); -+ fprintf(stderr, "Error: maximum redirects (5) exceeded\n"); - break; - } - fprintf(stderr, "Following redirect to %s\n", resp->meta); -@@ -816,6 +846,7 @@ browser.unicode = false; - break; - default: - fprintf(stderr, "fatal: unknown flag %c\n", c); -+ curl_url_cleanup(browser.url); - return 1; - } - } -@@ -841,6 +872,7 @@ while (browser.running) { - static char prompt[4096]; - if (do_requests(&browser, &resp)) { - // Skip prompts -+ gemini_response_finish(&resp); - goto next; - } - -@@ -880,8 +912,16 @@ } - browser.links = NULL; - } - -- history_free(browser.history); -+ gemini_tofu_finish(&browser.tofu); -+ struct history *hist = browser.history; -+ while (hist && hist->prev) { -+ hist = hist->prev; -+ } -+ history_free(hist); - SSL_CTX_free(browser.opts.ssl_ctx); - curl_url_cleanup(browser.url); -+ free(browser.page_title); -+ free(browser.plain_url); -+ fclose(browser.tty); - return 0; - } -diff --git a/src/parser.c b/src/parser.c -index 5b0f01399d934f593cdf987e096dd2cfb134d114..2f78e4641c08b86f5dc50cc9776ea80ec7f9aead 100644 ---- a/src/parser.c -+++ b/src/parser.c -@@ -35,15 +35,14 @@ { - memset(tok, 0, sizeof(*tok)); - - int eof = 0; -- while (!strstr(p->buf, "\n")) { -- if (p->bufln == p->bufsz) { -+ while (!strchr(p->buf, '\n')) { -+ while (p->bufln >= p->bufsz - 1) { - p->bufsz *= 2; -- char *buf = realloc(p->buf, p->bufsz); -- assert(buf); -- p->buf = buf; -+ p->buf = realloc(p->buf, p->bufsz); -+ assert(p->buf); - } - -- int n = BIO_read(p->f, &p->buf[p->bufln], p->bufsz - p->bufln); -+ ssize_t n = BIO_read(p->f, &p->buf[p->bufln], p->bufsz - p->bufln - 1); - if (n == -1) { - return -1; - } else if (n == 0) { -@@ -55,7 +54,7 @@ p->buf[p->bufln] = 0; - } - - char *end; -- if ((end = strstr(p->buf, "\n")) != NULL) { -+ if ((end = strchr(p->buf, '\n')) != NULL) { - *end = 0; - } - -diff --git a/src/tofu.c b/src/tofu.c -index 354e211c856451bdea120bbb1aed5917099eb285..45e1275d568cfcb4aacfe7f86788b76fff97916b 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -150,7 +150,7 @@ {.var = "GMNIDATA", .path = "/%s"}, - {.var = "XDG_DATA_HOME", .path = "/gmni/%s"}, - {.var = "HOME", .path = "/.local/share/gmni/%s"} - }; -- const char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); -+ char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0])); - snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), - path_fmt, "known_hosts"); - -@@ -164,6 +164,7 @@ } - - snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), - path_fmt, "known_hosts"); -+ free(path_fmt); - - tofu->callback = cb; - tofu->cb_data = cb_data; -@@ -175,6 +176,7 @@ return; - } - size_t n = 0; - char *line = NULL; -+ tofu->known_hosts = NULL; - while (getline(&line, &n, f) != -1) { - struct known_host *host = calloc(1, sizeof(struct known_host)); - char *tok = strtok(line, " "); -@@ -184,6 +186,7 @@ - tok = strtok(NULL, " "); - assert(tok); - if (strcmp(tok, "SHA-512") != 0) { -+ free(host->host); - free(host); - continue; - } -@@ -198,5 +201,20 @@ host->expires = strtoul(tok, NULL, 10); - - host->next = tofu->known_hosts; - tofu->known_hosts = host; -+ } -+ free(line); -+ fclose(f); -+} -+ -+void -+gemini_tofu_finish(struct gemini_tofu *tofu) -+{ -+ struct known_host *host = tofu->known_hosts; -+ while (host) { -+ struct known_host *tmp = host; -+ host = host->next; -+ free(tmp->host); -+ free(tmp->fingerprint); -+ free(tmp); - } - } diff --git a/sources/cgmnlm.git/commits/1808e6cd1880d3c08abb0ddfa19044afada925dd.patch b/sources/cgmnlm.git/commits/1808e6cd1880d3c08abb0ddfa19044afada925dd.patch @@ -1,24 +0,0 @@ -diff --git a/src/url.c b/src/url.c -index 47e31b5fcbeeecb5bc3962585311b20e3518bb73..dabf45f234e1e0957007b94ef12ef1125250dfea 100644 ---- a/src/url.c -+++ b/src/url.c -@@ -1361,19 +1361,6 @@ bool free_part = false; - char *enc = malloc(nalloc * 3 + 1); /* for worst case! */ - if(!enc) - return CURLUE_OUT_OF_MEMORY; -- if(plusencode) { -- /* space to plus */ -- i = part; -- for(o = enc; *i; ++o, ++i) -- *o = (*i == ' ') ? '+' : *i; -- *o = 0; /* zero terminate */ -- part = strdup(enc); -- if(!part) { -- free(enc); -- return CURLUE_OUT_OF_MEMORY; -- } -- free_part = true; -- } - for(i = part, o = enc; *i; i++) { - if(Curl_isunreserved(*i) || - ((*i == '/') && urlskipslash) || diff --git a/sources/cgmnlm.git/commits/18ead2644a8c525d1d3bbc729d9ccd9aa7e0d63c.patch b/sources/cgmnlm.git/commits/18ead2644a8c525d1d3bbc729d9ccd9aa7e0d63c.patch @@ -1,93 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 9efe3e231d445d55ea43d444a72eaa79f595ac76..37b4db277d8fcd961eb1746294724783e638109c 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -87,11 +87,11 @@ "<Enter>\t\tread more lines (if available)\n" - "<url>\t\tgo to url\n" - "[N]\t\tFollow Nth link (where N is a number)\n" - "p[N]\t\tPrint URL of Nth link (where N is a number)\n" -- "e\t\tSend current URL of browser to external default program\n" -- "e[N]\t\tSend URL of Nth link in external default program\n" -+ "e[N]\t\tSend URL of Nth link or current URL when N is ommited to external default program\n" - "t[N]\t\tDownload content of Nth link to a temporary file\n" - "b[N]\t\tJump back N entries in history, N is optional, default 1\n" - "f[N]\t\tJump forward N entries in history, N is optional, default 1\n" -+ "u\t\tone path element up\n" - "H\t\tView all page history\n" - "m\t\tSave bookmark\n" - "M\t\tBrowse bookmarks\n" -@@ -623,6 +623,7 @@ goto exit; - } - in[n - 1] = 0; // Remove LF - -+ char url[1024] = {0}; - int r; - switch (in[0]) { - case '\0': -@@ -664,6 +665,22 @@ } - set_url(browser, browser->history->url, NULL); - result = PROMPT_ANSWERED; - goto exit; -+ case 'u':; -+ int keep = 0; -+ int len = strlen(browser->plain_url); -+ for (int i=0; i<len; i++) -+ { -+ // ignore trailing / on uri path -+ if (browser->plain_url[i] == '/' && i != len-1) { -+ keep = i; -+ } -+ } -+ if (keep > 9) { -+ strncpy(url , browser->plain_url, keep+1); -+ set_url(browser, url, &browser->history); -+ } -+ result = PROMPT_ANSWERED; -+ goto exit; - case 'H': - if (in[1]) break; - struct history *cur = browser->history; -@@ -723,9 +740,8 @@ case 'p': - case 't': - if (!in[1]) { - if (in[0] == 'e') { -- char xdgopen[4096]; -- snprintf(xdgopen, sizeof(xdgopen), "xdg-open %s", browser->plain_url); -- if ( !system(xdgopen) ) fprintf(browser->tty, "Link send to xdg-open\n"); -+ snprintf(url, sizeof(url), "xdg-open %s", browser->plain_url); -+ if ( !system(url) ) fprintf(browser->tty, "Link send to xdg-open\n"); - goto exit; - } else { - break; -@@ -743,13 +759,11 @@ fprintf(stderr, "Error: no such link.\n"); - } else { - fprintf(browser->tty, "=> %s\n", link->url); - if (in[0] == 'e') { -- char xdgopen[4096]; -- snprintf(xdgopen, sizeof(xdgopen), "xdg-open %s", link->url); -- if ( !system(xdgopen) ) fprintf(browser->tty, "Link send to xdg-open\n"); -+ snprintf(url, sizeof(url), "xdg-open %s", link->url); -+ if ( !system(url) ) fprintf(browser->tty, "Link send to xdg-open\n"); - } - if (in[0] == 't') { - struct gemini_response resp; -- char url[1024] = {0}; - strncpy(&url[0], browser->plain_url, sizeof(link->url)-1); - set_url(browser, link->url, &browser->history); - // XXX: may affect history, do we care? -@@ -785,7 +799,6 @@ goto exit; - case 'd': - if (in[1] != '\0' && !isspace(in[1])) break; - struct gemini_response resp; -- char url[1024] = {0}; - strncpy(&url[0], browser->plain_url, sizeof(url)-1); - // XXX: may affect history, do we care? - enum gemini_result res = do_requests(browser, &resp); -@@ -831,7 +844,6 @@ - if (!link) { - fprintf(stderr, "Error: no such link.\n"); - } else if (endptr[0] == '|') { -- char url[1024] = {0}; - struct gemini_response resp; - strncpy(url, browser->plain_url, sizeof(url) - 1); - set_url(browser, link->url, &browser->history); diff --git a/sources/cgmnlm.git/commits/1a747cb6c2765ee818506c886bad4ed36b2b9d51.patch b/sources/cgmnlm.git/commits/1a747cb6c2765ee818506c886bad4ed36b2b9d51.patch @@ -1,47 +0,0 @@ -diff --git a/Makefile b/Makefile -index c5bc1ed76a0a61d70c2bfe3506a868f0d7eace5e..9d17d35eb67ee4b86a2d9219c3f1fce98695a521 100644 ---- a/Makefile -+++ b/Makefile -@@ -54,9 +54,8 @@ - distclean: clean - @rm -rf "$(OUTDIR)" - --install: all -+install: all install_docs - mkdir -p $(BINDIR) -- mkdir -p $(MANDIR)/man1 - install -Dm755 gmni $(BINDIR)/gmni - install -Dm755 gmnlm $(BINDIR)/gmnlm - install -Dm755 libgmni.a $(LIBDIR)/libgmni.a -@@ -64,8 +63,6 @@ install -Dm644 include/gmni/gmni.h $(INCLUDEDIR)/gmni/gmni.h - install -Dm644 include/gmni/tofu.h $(INCLUDEDIR)/gmni/tofu.h - install -Dm644 include/gmni/url.h $(INCLUDEDIR)/gmni/url.h - install -Dm644 libgmni.pc $(LIBDIR)/pkgconfig/libgmni.pc -- install -Dm644 doc/gmni.1 $(MANDIR)/man1/gmni.1 -- install -Dm644 doc/gmnlm.1 $(MANDIR)/man1/gmnlm.1 - - uninstall: - rm -f $(BINDIR)/gmni -diff --git a/config.sh b/config.sh -index 5bd34bb30dec02422855edbf37817f8276840211..ca5444c73b47daa204d9dd17a3949147385dedec 100644 ---- a/config.sh -+++ b/config.sh -@@ -125,6 +125,10 @@ if scdoc -v >/dev/null 2>&1 - then - echo yes - all="$all docs" -+ install_docs=" -+ mkdir -p \$(MANDIR)/man1 -+ install -Dm644 doc/gmni.1 \$(MANDIR)/man1/gmni.1 -+ install -Dm644 doc/gmnlm.1 \$(MANDIR)/man1/gmnlm.1" - else - echo no - fi -@@ -148,6 +152,7 @@ CFLAGS+=-DPREFIX='"\$(PREFIX)"' - CFLAGS+=-DLIBDIR='"\$(LIBDIR)"' - - all: ${all} -+ install_docs: ${install_docs} - EOF - - for target in $(printf '%s\n' $all | tr '.' '_') diff --git a/sources/cgmnlm.git/commits/1c9a6e6a35448b76063f16b0f6aaaf8d43ebee9a.patch b/sources/cgmnlm.git/commits/1c9a6e6a35448b76063f16b0f6aaaf8d43ebee9a.patch @@ -1,32 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index cbc04be66573d4980e24f00761c54d4ac8a1a88f..e80b38b6521ef0eccd5629a1f0b16d2406d8cac1 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -278,8 +278,10 @@ - if (browser->pagination && row >= ws.ws_row - 4) { - char prompt[4096]; - snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -- "[Enter]: read more; [N]: follow Nth link; [b]ack; [f]orward; [q]uit\n" -- "(more) => ", resp->meta, browser->plain_url); -+ "[Enter]: read more; [N]: follow Nth link; %s%s[q]uit\n" -+ "(more) => ", resp->meta, browser->plain_url, -+ browser->history->prev ? "[b]ack; " : "", -+ browser->history->next ? "[f]orward; " : ""); - enum prompt_result result = PROMPT_AGAIN; - while (result == PROMPT_AGAIN) { - result = do_prompts(prompt, browser); -@@ -485,10 +487,12 @@ goto next; - } - - snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -- "[N]: follow Nth link; [b]ack; [f]orward; [q]uit\n" -+ "[N]: follow Nth link; %s%s[q]uit\n" - "=> ", - resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", -- browser.plain_url); -+ browser.plain_url, -+ browser.history->prev ? "[b]ack; " : "", -+ browser.history->next ? "[f]orward; " : ""); - gemini_response_finish(&resp); - - enum prompt_result result = PROMPT_AGAIN; diff --git a/sources/cgmnlm.git/commits/1cfe0e794936cc51b9306327634a35f1c443643f.patch b/sources/cgmnlm.git/commits/1cfe0e794936cc51b9306327634a35f1c443643f.patch @@ -1,13 +0,0 @@ -diff --git a/config.sh b/config.sh -index 8d586cfde54fa3c3bc369ff0156ad5d55a79f383..424cbda346869c7476859d55a600c414c86bbb46 100644 ---- a/config.sh -+++ b/config.sh -@@ -73,7 +73,7 @@ *-Werror*) - werror="-Werror" - ;; - esac -- if $CC $werror "$@" -o /dev/null "$outdir"/check.c >/dev/null 2>&1 -+ if $CC $werror "$@" -o "$outdir"/check "$outdir"/check.c >/dev/null 2>&1 - then - append_cflags "$@" - else diff --git a/sources/cgmnlm.git/commits/1da4ff928a44f590e2c72cda1dcb4b097845cbc3.patch b/sources/cgmnlm.git/commits/1da4ff928a44f590e2c72cda1dcb4b097845cbc3.patch @@ -1,12 +0,0 @@ -diff --git a/README.md b/README.md -index e1b424367e49daad233793f663f6340849eb4286..9564df898ce60d9895b818f95ae020c39edb83af 100644 ---- a/README.md -+++ b/README.md -@@ -18,6 +18,7 @@ - It includes the following modifications: - - default 4 char indenting - - e[N] command to open a link in default external program (requires `xdg-open`) -+- t[N] command to download the content behind a link to a temporary file - - colored headings & links - - The actual colors used depend on your terminal palette: diff --git a/sources/cgmnlm.git/commits/207a72012ef69de654a78e18d28182ecde1326e2.patch b/sources/cgmnlm.git/commits/207a72012ef69de654a78e18d28182ecde1326e2.patch @@ -1,92 +0,0 @@ -diff --git a/Makefile b/Makefile -index 0070b703ea0af36743878d2264259bc04edb6b70..d6b576c49053adcc21557a8d88137101deeb5846 100644 ---- a/Makefile -+++ b/Makefile -@@ -4,11 +4,11 @@ OUTDIR=.build - include $(OUTDIR)/config.mk - include $(OUTDIR)/cppcache - --gmnic: $(gmnic_objects) -+gmni: $(gmni_objects) - @printf 'CCLD\t$@\n' -- @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmnic_objects) -+ @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmni_objects) - --doc/gmnic.1: doc/gmnic.scd -+doc/gmni.1: doc/gmni.scd - - .SUFFIXES: .c .o .scd .1 - -@@ -23,10 +23,10 @@ .scd.1: - @printf 'SCDOC\t$@\n' - @$(SCDOC) < $< > $@ - --docs: doc/gmnic.1 -+docs: doc/gmni.1 - - clean: -- @rm -f gmnic -+ @rm -f gmni doc/gmni.1 - - distclean: clean - @rm -rf "$(OUTDIR)" -diff --git a/configure b/configure -index 680b57fc9e319c2707e0cc2ded0bc22597544d78..407886859d8ca0b9cbcb085ecef89d004c68cde4 100755 ---- a/configure -+++ b/configure -@@ -3,13 +3,13 @@ srcdir=${SRCDIR:-$(dirname "$0")} - eval ". $srcdir/config.sh" - - gmni() { -- genrules gmnic \ -+ genrules gmni \ - src/client.c \ - src/escape.c \ -- src/gmnic.c \ -+ src/gmni.c \ - src/url.c - } - --all="gmnic" -+all="gmni" - - run_configure -diff --git a/doc/gmnic.scd b/doc/gmni.scd -rename from doc/gmnic.scd -rename to doc/gmni.scd -index 9eec29cfddf50c2678ef7ae080275fbfbe3fcaa1..0d84d4d974f2c38b426f6ae85d228cdd4847cbda 100644 ---- a/doc/gmnic.scd -+++ b/doc/gmni.scd -@@ -1,16 +1,16 @@ --gmnic(1) -+gmni(1) - - # NAME - --gmnic - Gemini client -+gmni - Gemini client - - # SYNPOSIS - --*gmnic* [-46lLiIN] [-E _path_] [-d _input_] [-D _path_] _gemini://..._ -+*gmni* [-46lLiIN] [-E _path_] [-d _input_] [-D _path_] _gemini://..._ - - # DESCRIPTION - --*gmnic* executes a gemini request and, if successful, prints the response body -+*gmni* executes a gemini request and, if successful, prints the response body - to stdout. - - If an error is returned, information is printed to stderr and the process exits -@@ -45,7 +45,7 @@ accept a password, append ":" to the path and it will be intepreted as - an empty password. - - *-l* -- For *text/\** responses, gmnic normally adds a line feed if stdout is a -+ For *text/\** responses, *gmni* normally adds a line feed if stdout is a - TTY and the response body does not include one. This flag suppresses - this behavior. - -diff --git a/src/gmnic.c b/src/gmni.c -rename from src/gmnic.c -rename to src/gmni.c diff --git a/sources/cgmnlm.git/commits/211b8c3dd36a950132efa5ba4e2f0172a42bb6bc.patch b/sources/cgmnlm.git/commits/211b8c3dd36a950132efa5ba4e2f0172a42bb6bc.patch @@ -1,66 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 15256bbd0dcd1c6899e8e91ba5228baf93ce289f..2c2b67e0e56d2e6be04bba6cee96cb4a24ff3b51 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -192,6 +192,40 @@ } - } - - static char * -+get_input(const struct gemini_response *resp, FILE *source) -+{ -+ int r = 0; -+ struct termios attrs; -+ bool tty = fileno(source) != -1 && isatty(fileno(source)); -+ char *input = NULL; -+ if (tty) { -+ fprintf(stderr, "%s: ", resp->meta); -+ if (resp->status == GEMINI_STATUS_SENSITIVE_INPUT) { -+ r = tcgetattr(fileno(source), &attrs); -+ struct termios new_attrs; -+ r = tcgetattr(fileno(source), &new_attrs); -+ if (r != -1) { -+ new_attrs.c_lflag &= ~ECHO; -+ tcsetattr(fileno(source), TCSANOW, &new_attrs); -+ } -+ } -+ } -+ size_t s = 0; -+ ssize_t n = getline(&input, &s, source); -+ if (n == -1) { -+ fprintf(stderr, "Error reading input: %s\n", -+ feof(source) ? "EOF" : strerror(ferror(source))); -+ return NULL; -+ } -+ input[n - 1] = '\0'; // Drop LF -+ if (tty && resp->status == GEMINI_STATUS_SENSITIVE_INPUT && r != -1) { -+ attrs.c_lflag &= ~ECHO; -+ tcsetattr(fileno(source), TCSANOW, &attrs); -+ } -+ return input; -+} -+ -+static char * - do_requests(struct browser *browser, struct gemini_response *resp) - { - char *plain_url; -@@ -210,9 +244,19 @@ requesting = false; - break; - } - -+ char *input; - switch (gemini_response_class(resp->status)) { - case GEMINI_STATUS_CLASS_INPUT: -- assert(0); // TODO -+ input = get_input(resp, browser->tty); -+ if (!input) { -+ requesting = false; -+ break; -+ } -+ -+ char *new_url = gemini_input_url(plain_url, input); -+ assert(new_url); -+ set_url(browser, new_url, NULL); -+ break; - case GEMINI_STATUS_CLASS_REDIRECT: - if (++nredir >= 5) { - requesting = false; diff --git a/sources/cgmnlm.git/commits/22a28fa92755b49254cac144894be1fdb917a6a3.patch b/sources/cgmnlm.git/commits/22a28fa92755b49254cac144894be1fdb917a6a3.patch @@ -1,114 +0,0 @@ -diff --git a/README.md b/README.md -index d68381569be5736bac9887345b5c053e69393b5a..3cb5f020cab021c3b4ffd7eb98908c76c352e90d 100644 ---- a/README.md -+++ b/README.md -@@ -27,6 +27,8 @@ - default 4 char indenting - - k command to remove the bookmark for the current page - - e[N] command to open a link in default external program (requires `xdg-open`) - - t[N] command to download the content behind a link to a temporary file -+- b & f commands to navigate history can jump multiple entries at once -+- colored headings & links - - The actual colors used depend on your terminal palette: - - heading 1: light red -@@ -39,11 +41,13 @@ - preformatted text: light gray - - Besides this rendering adjustments i'll try to keep track of upstream changes or send patches to upstream. - --## Dependencies: -+## Usage - --- A POSIX-like system and a C11 compiler --- OpenSSL --- [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional, build only) -+See `gmni(1)`, `cgmnlm(1)`. -+ -+# Installation -+ -+* ArchLinux and derivates: https://aur.archlinux.org/packages/cgmnlm-git/ - - ## Compiling - -@@ -54,6 +58,9 @@ $ make - # make install - ``` - --## Usage -+### Dependencies: -+ -+- A POSIX-like system and a C11 compiler -+- OpenSSL -+- [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) - --See `gmni(1)`, `cgmnlm(1)`. -diff --git a/src/gmnlm.c b/src/gmnlm.c -index cc429cdfe6d482e549afee2bd9b05520985e92f9..eaa34c055cbb92564ebd279fcf06189dc577360d 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -84,8 +84,8 @@ "[N]\t\tFollow Nth link (where N is a number)\n" - "p[N]\t\tPrint URL of Nth link (where N is a number)\n" - "e[N]\t\tSend URL of Nth link in external default program\n" - "t[N]\t\tDownload content of Nth link to a temporary file\n" -- "b\t\tBack (in the page history)\n" -- "f\t\tForward (in the page history)\n" -+ "b[N]\t\tJump back N entries in history, N is optional, default 1\n" -+ "f[N]\t\tJump forward N entries in history, N is optional, default 1\n" - "H\t\tView all page history\n" - "m\t\tSave bookmark\n" - "M\t\tBrowse bookmarks\n" -@@ -546,6 +546,8 @@ - struct link *link = browser->links; - char *endptr = NULL; - int linksel = 0; -+ int historyhops = 1; -+ - char *in = NULL; - size_t l = 0; - ssize_t n = getline(&in, &l, browser->tty); -@@ -554,7 +556,7 @@ result = PROMPT_QUIT; - goto exit; - } - in[n - 1] = 0; // Remove LF -- -+ - int r; - switch (in[0]) { - case '\0': -@@ -565,25 +567,24 @@ if (in[1]) break; - result = PROMPT_QUIT; - goto exit; - case 'b': -- if (in[1]) break; -- if (!browser->history->prev) { -- fprintf(stderr, "At beginning of history\n"); -- result = PROMPT_AGAIN; -- goto exit; -+ if (in[1]) historyhops =(int)strtol(in+1, &endptr, 10); -+ while (historyhops > 0) { -+ if (browser->history->prev) { -+ browser->history = browser->history->prev; -+ } -+ historyhops--; - } -- if (in[1]) break; -- browser->history = browser->history->prev; - set_url(browser, browser->history->url, NULL); - result = PROMPT_ANSWERED; - goto exit; - case 'f': -- if (in[1]) break; -- if (!browser->history->next) { -- fprintf(stderr, "At end of history\n"); -- result = PROMPT_AGAIN; -- goto exit; -+ if (in[1]) historyhops =(int)strtol(in+1, &endptr, 10); -+ while (historyhops > 0) { -+ if (browser->history->next) { -+ browser->history = browser->history->next; -+ } -+ historyhops--; - } -- browser->history = browser->history->next; - set_url(browser, browser->history->url, NULL); - result = PROMPT_ANSWERED; - goto exit; diff --git a/sources/cgmnlm.git/commits/262ccc2005617eb633f5b3ba434ee26f8000e0a8.patch b/sources/cgmnlm.git/commits/262ccc2005617eb633f5b3ba434ee26f8000e0a8.patch @@ -1,23 +0,0 @@ -diff --git a/src/gmnic.c b/src/gmnic.c -index fd6add8a61546a662a3fe7bb4f3beb9848a19452..2a06a180ebd5478e25b97cacea8468c86d11df00 100644 ---- a/src/gmnic.c -+++ b/src/gmnic.c -@@ -17,7 +17,7 @@ static void - usage(char *argv_0) - { - fprintf(stderr, -- "usage: %s [-LI] [-C cert] [-d input] gemini://...\n", -+ "usage: %s [-46lLiIN] [-C cert] [-d input] [-D path] gemini://...\n", - argv_0); - } - -@@ -209,7 +209,8 @@ w += x; - } - } - if (strncmp(resp.meta, "text/", 5) == 0 -- && linefeed && last != '\n') { -+ && linefeed && last != '\n' -+ && isatty(STDOUT_FILENO)) { - printf("\n"); - } - break; diff --git a/sources/cgmnlm.git/commits/26666e7838fd40ca7d6f20af7e0cb554ff8bb0f0.patch b/sources/cgmnlm.git/commits/26666e7838fd40ca7d6f20af7e0cb554ff8bb0f0.patch @@ -1,12 +0,0 @@ -diff --git a/README.md b/README.md -index 3cb5f020cab021c3b4ffd7eb98908c76c352e90d..3c999cee8bbcb5b0bfbc6732f9ecea0ba231d043 100644 ---- a/README.md -+++ b/README.md -@@ -28,7 +28,6 @@ - k command to remove the bookmark for the current page - - e[N] command to open a link in default external program (requires `xdg-open`) - - t[N] command to download the content behind a link to a temporary file - - b & f commands to navigate history can jump multiple entries at once --- colored headings & links - - The actual colors used depend on your terminal palette: - - heading 1: light red diff --git a/sources/cgmnlm.git/commits/28283bda98accf122b6424ac611fd4ff25dedbc9.patch b/sources/cgmnlm.git/commits/28283bda98accf122b6424ac611fd4ff25dedbc9.patch @@ -1,12 +0,0 @@ -diff --git a/src/parser.c b/src/parser.c -index eb9aa5ec3d1da4a222e9ec0c76ad19ba004b9a2c..b9db3d2000f1176df4a21300f7b806ed6a5ded75 100644 ---- a/src/parser.c -+++ b/src/parser.c -@@ -52,7 +52,6 @@ p->bufln += n; - p->buf[p->bufln] = 0; - } - -- // TODO: Collapse multi-line text for the user-agent to wrap - char *end; - if ((end = strstr(p->buf, "\n")) != NULL) { - *end = 0; diff --git a/sources/cgmnlm.git/commits/2e593cd48bd5e1f90fcbb54b83955752cd392466.patch b/sources/cgmnlm.git/commits/2e593cd48bd5e1f90fcbb54b83955752cd392466.patch @@ -1,22 +0,0 @@ -diff --git a/config.sh b/config.sh -index 70c4489bf5fd48fc0eb636bf739a5d82dcff8663..ef533ec79238744458fb2e69b95e3d34333c0e5a 100644 ---- a/config.sh -+++ b/config.sh -@@ -5,7 +5,6 @@ AS=${AS:-as} - CC=${CC:-cc} - CFLAGS=${CFLAGS:-} - LD=${LD:-ld} --LIBSSL= - - for arg - do -@@ -13,9 +12,6 @@ # TODO: Add args for install directories - case "$arg" in - --prefix=*) - PREFIX=${arg#*=} -- ;; -- --with-libssl=*) -- LIBSSL=${arg#*=} - ;; - esac - done diff --git a/sources/cgmnlm.git/commits/2e9d3c0bab8e7df635a8f0968f04fe9b1e2d979c.patch b/sources/cgmnlm.git/commits/2e9d3c0bab8e7df635a8f0968f04fe9b1e2d979c.patch @@ -1,26 +0,0 @@ -diff --git a/README.md b/README.md -index ce71012d402a0c975c73f6d4acd2922f32c16cbc..c269ec775d39d0c26445cfd39358829aae06b575 100644 ---- a/README.md -+++ b/README.md -@@ -5,13 +5,19 @@ - - A CLI utility (like curl): gmni - - A [line-mode browser](https://en.wikipedia.org/wiki/Line_Mode_Browser): gmnlm - --[](https://asciinema.org/a/Y7viodM01e0AXYyf40CwSLAVA) -- - Dependencies: - - - A POSIX-like system and a C11 compiler - - OpenSSL - - [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) -+ -+Features: -+ -+- Page history -+- Regex searches -+- Bookmarks -+ -+[](https://asciinema.org/a/Y7viodM01e0AXYyf40CwSLAVA) - - ## Compiling - diff --git a/sources/cgmnlm.git/commits/30660fc160a15504274d40d4a5ec1b31539f8c2f.patch b/sources/cgmnlm.git/commits/30660fc160a15504274d40d4a5ec1b31539f8c2f.patch @@ -1,18 +0,0 @@ -diff --git a/Makefile b/Makefile -index 12ff4b45e4b043ae5c93629b502e42e8396b2b24..4dc668c893cfeb03020f992bf92c7ae46953d299 100644 ---- a/Makefile -+++ b/Makefile -@@ -6,11 +6,11 @@ include $(OUTDIR)/cppcache - - gmni: $(gmni_objects) - @printf 'CCLD\t$@\n' -- @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmni_objects) -+ $(CC) $(LDFLAGS) -o $@ $(gmni_objects) $(LIBS) - - gmnlm: $(gmnlm_objects) - @printf 'CCLD\t$@\n' -- @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmnlm_objects) -+ @$(CC) $(LDFLAGS) -o $@ $(gmnlm_objects) $(LIBS) - - doc/gmni.1: doc/gmni.scd - doc/gmnlm.1: doc/gmnlm.scd diff --git a/sources/cgmnlm.git/commits/320676ca5bc1980c96f5e4bc14240a741be8f3be.patch b/sources/cgmnlm.git/commits/320676ca5bc1980c96f5e4bc14240a741be8f3be.patch @@ -1,30 +0,0 @@ -diff --git a/.gitignore b/.gitignore -index d622c7ceba259e04d4a2e8c2ee4c91de45e13187..66d40426a4b715ec6bceb934219dff612ea2f011 100644 ---- a/.gitignore -+++ b/.gitignore -@@ -1,7 +1,7 @@ - .build - build - /gmni --gmnlm -+cgmnlm - *.1 - *.o - *.a -diff --git a/cgmnlm b/cgmnlm -deleted file mode 100755 -index a7237df4a6c75847b8bfaee4dc4d1df687975936..0000000000000000000000000000000000000000 -Binary files a/cgmnlm and /dev/null differ -diff --git a/src/cgmnlm.c b/src/cgmnlm.c -index cb0bc10b924eb6afa057d41158815b133fd9372f..60b275d1f8b8425206dd7000a221d60c7473ddff 100644 ---- a/src/cgmnlm.c -+++ b/src/cgmnlm.c -@@ -897,7 +897,7 @@ char *end = NULL; - if (browser->meta && (end = strchr(resp->meta, ';')) != NULL) { - *end = 0; - } -- snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -+ snprintf(prompt, sizeof(prompt), "%s at %s\n" - "[Enter]: read more; %s[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" - "(more) => ", resp->meta, browser->plain_url, - browser->searching ? "[n]ext result; " : "", diff --git a/sources/cgmnlm.git/commits/3270a74590d4bfbbc9ae1fdc4c1d36eed943844f.patch b/sources/cgmnlm.git/commits/3270a74590d4bfbbc9ae1fdc4c1d36eed943844f.patch @@ -1,30 +0,0 @@ -diff --git a/src/cgmnlm.c b/src/cgmnlm.c -index 60b275d1f8b8425206dd7000a221d60c7473ddff..38ac821cff06602aba88c5e1c414aeccf3eb1e1f 100644 ---- a/src/cgmnlm.c -+++ b/src/cgmnlm.c -@@ -733,9 +733,7 @@ default: - // skip unicode continuation bytes - if ((s[i] & 0xc0) == 0x80) break; - -- if (iscntrl(s[i])) { -- s[i] = '.'; -- } -+ if (iscntrl(s[i])) s[i] = '.'; - *col += 1; - break; - } -@@ -743,12 +741,10 @@ - if (*col >= ws->ws_col) { - int j = i--; - while (&s[i] != s && !isspace(s[i])) --i; -- if (&s[i] == s) { -- i = j; -- } -+ if (&s[i] == s) i = j; - char c = s[i]; - s[i] = 0; -- int n = fprintf(f, "%s\n", s); -+ int n = fprintf(f, "%s\n", s) - (isspace(c) ? 0 : 1); - s[i] = c; - *row += 1; - *col = 0; diff --git a/sources/cgmnlm.git/commits/33495e8dd86139cafade2888227e37b1572d18ea.patch b/sources/cgmnlm.git/commits/33495e8dd86139cafade2888227e37b1572d18ea.patch @@ -1,52 +0,0 @@ -diff --git a/include/gmni.h b/include/gmni.h -index 42cfdac95530c9d6a5f4e6e6c3b85635908cc1e6..4d46380f2ef13db72ebf2e1a3434865142b6bcbc 100644 ---- a/include/gmni.h -+++ b/include/gmni.h -@@ -8,6 +8,7 @@ enum gemini_result { - GEMINI_OK, - GEMINI_ERR_OOM, - GEMINI_ERR_INVALID_URL, -+ GEMINI_ERR_NOT_GEMINI, - GEMINI_ERR_RESOLVE, - GEMINI_ERR_CONNECT, - GEMINI_ERR_SSL, -diff --git a/src/client.c b/src/client.c -index f1674d534a9a5f2edcb6c7be678e17ad5724a9a7..34d25f3794f8069b15a688ebd89b7bf82fdd01e2 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -104,11 +104,24 @@ struct Curl_URL *uri = curl_url(); - if (!uri) { - return GEMINI_ERR_OOM; - } -+ -+ enum gemini_result res = GEMINI_OK; - if (curl_url_set(uri, CURLUPART_URL, url, 0) != CURLUE_OK) { -- return GEMINI_ERR_INVALID_URL; -+ res = GEMINI_ERR_INVALID_URL; -+ goto cleanup; -+ } -+ -+ char *scheme; -+ 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; -+ goto cleanup; -+ } - } - -- enum gemini_result res = GEMINI_OK; - if (options && options->ssl_ctx) { - resp->ssl_ctx = options->ssl_ctx; - SSL_CTX_up_ref(options->ssl_ctx); -@@ -226,6 +239,8 @@ case GEMINI_ERR_OOM: - return "Out of memory"; - case GEMINI_ERR_INVALID_URL: - return "Invalid URL"; -+ case GEMINI_ERR_NOT_GEMINI: -+ return "Not a gemini URL"; - case GEMINI_ERR_RESOLVE: - return gai_strerror(resp->status); - case GEMINI_ERR_CONNECT: diff --git a/sources/cgmnlm.git/commits/3547fd11d57da5c7aa610366d54eef3b47d0b1a4.patch b/sources/cgmnlm.git/commits/3547fd11d57da5c7aa610366d54eef3b47d0b1a4.patch @@ -1,141 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index dd8c8d200a949a24d030ce3429a4d20f1430fe97..94e9933e514438c633663ae8871d4fba3e64c349 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -17,15 +17,42 @@ char *url; - struct link *next; - }; - -+struct history { -+ char *url; -+ struct history *prev, *next; -+}; -+ - static void - usage(const char *argv_0) - { - fprintf(stderr, "usage: %s [gemini://...]\n", argv_0); -+} -+ -+static void -+history_free(struct history *history) -+{ -+ if (!history) { -+ return; -+ } -+ history_free(history->next); -+ free(history); - } - - static bool --set_url(struct Curl_URL *url, char *new_url) -+set_url(struct Curl_URL *url, char *new_url, struct history **history) - { -+ if (history) { -+ struct history *next = calloc(1, sizeof(struct history)); -+ next->url = strdup(new_url); -+ next->prev = *history; -+ if (*history) { -+ if ((*history)->next) { -+ history_free((*history)->next); -+ } -+ (*history)->next = next; -+ } -+ *history = next; -+ } - if (curl_url_set(url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { - fprintf(stderr, "Error: invalid URL\n"); - return false; -@@ -138,8 +165,9 @@ return 1; - } - } - -+ struct history *history; - if (optind == argc - 1) { -- set_url(url, argv[optind]); -+ set_url(url, argv[optind], &history); - } else { - usage(argv[0]); - return 1; -@@ -161,9 +189,10 @@ char *plain_url; - CURLUcode uc = curl_url_get(url, CURLUPART_URL, &plain_url, 0); - assert(uc == CURLUE_OK); // Invariant - -- snprintf(prompt, sizeof(prompt), "\n\t%s\n" -- "\tWhere to? [n]: follow Nth link; [o <url>]: open URL; [q]: quit " -- "[b]ack; [f]orward\n" -+ snprintf(prompt, sizeof(prompt), "\nat %s\n" -+ "[n]: follow Nth link; [o <url>]: open URL; " -+ "[b]ack; [f]orward; " -+ "[q]uit\n" - "=> ", plain_url); - - enum gemini_result res = gemini_request(plain_url, &opts, &resp); -@@ -201,11 +230,29 @@ size_t l = 0; - char *in = NULL; - ssize_t n = getline(&in, &l, tty); - if (n == -1 && feof(tty)) { -- prompting = run = false; -+ run = false; - break; - } - if (strcmp(in, "q\n") == 0) { -- prompting = run = false; -+ run = false; -+ break; -+ } -+ if (strcmp(in, "b\n") == 0) { -+ if (!history->prev) { -+ fprintf(stderr, "At beginning of history\n"); -+ continue; -+ } -+ history = history->prev; -+ set_url(url, history->url, NULL); -+ break; -+ } -+ if (strcmp(in, "f\n") == 0) { -+ if (!history->next) { -+ fprintf(stderr, "At end of history\n"); -+ continue; -+ } -+ history = history->next; -+ set_url(url, history->url, NULL); - break; - } - -@@ -221,23 +268,24 @@ - if (!link) { - fprintf(stderr, "Error: no such link.\n"); - } else { -- prompting = false; -- set_url(url, link->url); -+ set_url(url, link->url, &history); -+ break; - } - } -+ free(in); -+ } - -- link = links; -- while (link) { -- struct link *next = link->next; -- free(link->url); -- free(link); -- link = next; -- } -- -- free(in); -+ struct link *link = links; -+ while (link) { -+ struct link *next = link->next; -+ free(link->url); -+ free(link); -+ link = next; - } -+ links = NULL; - } - -+ history_free(history); - SSL_CTX_free(opts.ssl_ctx); - curl_url_cleanup(url); - return 0; diff --git a/sources/cgmnlm.git/commits/37396a375a68868490342e16140e67287445be17.patch b/sources/cgmnlm.git/commits/37396a375a68868490342e16140e67287445be17.patch @@ -1,16 +0,0 @@ -diff --git a/Makefile b/Makefile -index d6b576c49053adcc21557a8d88137101deeb5846..69a241a8825a6a7aa979eb2ae95a26faaf3a0532 100644 ---- a/Makefile -+++ b/Makefile -@@ -31,4 +31,10 @@ - distclean: clean - @rm -rf "$(OUTDIR)" - --.PHONY: clean distclean docs -+install: all -+ mkdir -p $(BINDIR) -+ mkdir -p $(MANDIR)/man1 -+ install -Dm755 gmni $(BINDIR)/gmni -+ install -Dm644 doc/gmni.1 $(MANDIR)/man1/gmni.1 -+ -+.PHONY: clean distclean docs install diff --git a/sources/cgmnlm.git/commits/39339c348f593bda9ca0a556affa38cd5e15138c.patch b/sources/cgmnlm.git/commits/39339c348f593bda9ca0a556affa38cd5e15138c.patch @@ -1,21 +0,0 @@ -diff --git a/src/tofu.c b/src/tofu.c -index e8efeaf69fcb9bd890711f76959e86efa75cfec6..354e211c856451bdea120bbb1aed5917099eb285 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -31,6 +31,7 @@ // - // TODO: Check that the subject name is valid for the requested URL. - struct gemini_tofu *tofu = (struct gemini_tofu *)data; - X509 *cert = X509_STORE_CTX_get0_cert(ctx); -+ struct known_host *host = NULL; - - int rc; - int day, sec; -@@ -77,7 +78,7 @@ time_t now; - time(&now); - - enum tofu_error error = TOFU_UNTRUSTED_CERT; -- struct known_host *host = tofu->known_hosts; -+ host = tofu->known_hosts; - while (host) { - if (host->expires < now) { - goto next; diff --git a/sources/cgmnlm.git/commits/3c63a64288f665a272974698d547bbca79769d5a.patch b/sources/cgmnlm.git/commits/3c63a64288f665a272974698d547bbca79769d5a.patch @@ -1,13 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 3bd2ea9a9b70cc33f6fc5b330234c17a974f80f5..c5b778095021a7592fce5c7e4a83c70be822790e 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -246,7 +246,7 @@ fclose(fi); - fclose(fo); - free(line); - if ( rename(tempfile, path) != 0) { -- fprintf(browser->tty, "Failed to udpate bookmarks: %s\n", strerror(errno)); -+ fprintf(browser->tty, "Failed to update bookmarks: %s\n", strerror(errno)); - } - } - diff --git a/sources/cgmnlm.git/commits/3ce02e5183da68e017b572265d68f19fef59043c.patch b/sources/cgmnlm.git/commits/3ce02e5183da68e017b572265d68f19fef59043c.patch @@ -1,183 +0,0 @@ -diff --git a/Makefile b/Makefile -index 5e468e26f12fef2c066826e4e5662e51ca4dde42..00e31d3fcee294d181c736b75e8e2bc36bdc7053 100644 ---- a/Makefile -+++ b/Makefile -@@ -9,16 +9,16 @@ gmni: $(gmni_objects) - @printf 'CCLD\t$@\n' - @$(CC) $(LDFLAGS) -o $@ $(gmni_objects) $(LIBS) - --gmnlm: $(gmnlm_objects) -+cgmnlm: $(cgmnlm_objects) - @printf 'CCLD\t$@\n' -- @$(CC) $(LDFLAGS) -o $@ $(gmnlm_objects) $(LIBS) -+ @$(CC) $(LDFLAGS) -o $@ $(cgmnlm_objects) $(LIBS) - - libgmni.a: $(libgmni.a_objects) - @printf 'AR\t$@\n' - @$(AR) -rcs $@ $(libgmni.a_objects) - - doc/gmni.1: doc/gmni.scd --doc/gmnlm.1: doc/gmnlm.scd -+doc/cgmnlm.1: doc/cgmnlm.scd - - libgmni.pc: - @printf 'GEN\t$@\n' -@@ -46,10 +46,10 @@ .scd.1: - @printf 'SCDOC\t$@\n' - @$(SCDOC) < $< > $@ - --docs: doc/gmni.1 doc/gmnlm.1 -+docs: doc/gmni.1 doc/cgmnlm.1 - - clean: -- @rm -f gmni gmnlm libgmni.a libgmni.pc doc/gmni.1 doc/gmnlm.1 $(gmnlm_objects) $(gmni_objects) -+ @rm -f gmni cgmnlm libgmni.a libgmni.pc doc/gmni.1 doc/cgmnlm.1 $(cgmnlm_objects) $(gmni_objects) - - distclean: clean - @rm -rf "$(OUTDIR)" -@@ -60,7 +60,7 @@ mkdir -p $(LIBDIR) - mkdir -p $(INCLUDEDIR)/gmni - mkdir -p $(LIBDIR)/pkgconfig - install -m755 gmni $(BINDIR)/gmni -- install -m755 gmnlm $(BINDIR)/gmnlm -+ install -m755 cgmnlm $(BINDIR)/cgmnlm - install -m755 libgmni.a $(LIBDIR)/libgmni.a - install -m644 include/gmni/gmni.h $(INCLUDEDIR)/gmni/gmni.h - install -m644 include/gmni/tofu.h $(INCLUDEDIR)/gmni/tofu.h -@@ -69,11 +69,11 @@ install -m644 libgmni.pc $(LIBDIR)/pkgconfig/libgmni.pc - - uninstall: - rm -f $(BINDIR)/gmni -- rm -f $(BINDIR)/gmnlm -+ rm -f $(BINDIR)/cgmnlm - rm -f $(LIBDIR)/libgmni.a - rm -rf $(INCLUDEDIR)/gmni - rm -f $(LIBDIR)/pkgconfig/libgmni.pc - rm -f $(MANDIR)/man1/gmni.1 -- rm -f $(MANDIR)/man1/gmnlm.1 -+ rm -f $(MANDIR)/man1/cgmnlm.1 - - .PHONY: clean distclean docs install -diff --git a/README.md b/README.md -index c269ec775d39d0c26445cfd39358829aae06b575..9e1c9831127d8395d3fa8173f4ecb775c27e31b1 100644 ---- a/README.md -+++ b/README.md -@@ -1,23 +1,29 @@ --# gmni - A Gemini client -+# cgmnlm - A colorful Gemini line mode client - - This is a [Gemini](https://gemini.circumlunar.space/) client. Included are: - - - A CLI utility (like curl): gmni --- A [line-mode browser](https://en.wikipedia.org/wiki/Line_Mode_Browser): gmnlm -- --Dependencies: -- --- A POSIX-like system and a C11 compiler --- OpenSSL --- [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) -+- A [line-mode browser](https://en.wikipedia.org/wiki/Line_Mode_Browser): cgmnlm - --Features: -+## Features: - - - Page history - - Regex searches - - Bookmarks - --[](https://asciinema.org/a/Y7viodM01e0AXYyf40CwSLAVA) -+### Modifications compared to upstream -+ -+This project is of fork of https://git.sr.ht/~sircmpwn/gmni -+ -+It includes the following modifications: -+- default 4 char indenting -+- colored headings & links -+ -+## Dependencies: -+ -+- A POSIX-like system and a C11 compiler -+- OpenSSL -+- [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) - - ## Compiling - -@@ -30,4 +36,4 @@ ``` - - ## Usage - --See `gmni(1)`, `gmnlm(1)`. -+See `gmni(1)`, `cgmnlm(1)`. -diff --git a/cgmnlm b/cgmnlm -new file mode 100755 -index 0000000000000000000000000000000000000000..5c05cd3ecca44b577a75544d7e2119421a5da102 -Binary files /dev/null and b/cgmnlm differ -diff --git a/config.sh b/config.sh -index fd6a325bb21807ed6b6dc871b64479ebfe89a68d..8d586cfde54fa3c3bc369ff0156ad5d55a79f383 100644 ---- a/config.sh -+++ b/config.sh -@@ -128,7 +128,7 @@ all="$all docs" - install_docs=" - mkdir -p \$(MANDIR)/man1 - install -m644 doc/gmni.1 \$(MANDIR)/man1/gmni.1 -- install -m644 doc/gmnlm.1 \$(MANDIR)/man1/gmnlm.1" -+ install -m644 doc/cgmnlm.1 \$(MANDIR)/man1/cgmnlm.1" - else - echo no - fi -diff --git a/configure b/configure -index e82a0e27e27c9873c8dac92fd3385e65bf511648..aa761971fe95d70ed242977bed8dfd6d55a9f794 100755 ---- a/configure -+++ b/configure -@@ -12,11 +12,11 @@ src/url.c \ - src/util.c - } - --gmnlm() { -- genrules gmnlm \ -+cgmnlm() { -+ genrules cgmnlm \ - src/client.c \ - src/escape.c \ -- src/gmnlm.c \ -+ src/cgmnlm.c \ - src/parser.c \ - src/tofu.c \ - src/url.c \ -@@ -37,7 +37,7 @@ libgmni_pc() { - : - } - --all="gmni gmnlm libgmni.a libgmni.pc" -+all="gmni cgmnlm libgmni.a libgmni.pc" - - - run_configure -diff --git a/doc/gmnlm.scd b/doc/cgmnlm.scd -rename from doc/gmnlm.scd -rename to doc/cgmnlm.scd -index e0a368f8921253c76219790d35380848e486720d..1ed6e620cb39e04ade8b98fa9bab7e7c0bacc5e9 100644 ---- a/doc/gmnlm.scd -+++ b/doc/cgmnlm.scd -@@ -2,15 +2,15 @@ gmnlm(1) - - # NAME - --gmnlm - Gemini line-mode browser -+cgmnlm - colored Gemini line-mode browser - - # SYNPOSIS - --*gmnlm* [-PU] [-j _mode_] [-W _width_] _gemini://..._ -+*cgmnlm* [-PU] [-j _mode_] [-W _width_] _gemini://..._ - - # DESCRIPTION - --*gmnlm* is an interactive line-mode Gemini browser. -+*cgmnlm* is an interactive line-mode Gemini browser. - - # OPTIONS - -diff --git a/src/gmnlm.c b/src/cgmnlm.c -rename from src/gmnlm.c -rename to src/cgmnlm.c diff --git a/sources/cgmnlm.git/commits/3dd06ab4813b6c8f4992e19fce9d4b094fd3a1a9.patch b/sources/cgmnlm.git/commits/3dd06ab4813b6c8f4992e19fce9d4b094fd3a1a9.patch @@ -1,43 +0,0 @@ -diff --git a/include/util.h b/include/util.h -index 8ec9ac5d94092a75813a3e083140fbb88079c29d..6c241f41a175c3d27390ecb6ca5b041bd637cb2b 100644 ---- a/include/util.h -+++ b/include/util.h -@@ -9,6 +9,7 @@ const char *path; - }; - - 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); -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 12a6c971b7ae7d18bcdafc3b2b81f55539340095..0ab21849e186a99c84d0f94c59ef6c821e91b97d 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -562,6 +562,13 @@ assert(host); - n = snprintf(certpath, sizeof(certpath), path_fmt, host, "crt"); - assert(n < sizeof(certpath)); - n = snprintf(keypath, sizeof(keypath), path_fmt, host, "key"); -+ char dname[PATH_MAX + 1]; -+ posix_dirname(certpath, dname); -+ if (mkdirs(dname, 0755) != 0) { -+ fprintf(stderr, "Error creating directory %s: %s\n", -+ dname, strerror(errno)); -+ break; -+ } - assert(n < sizeof(keypath)); - fprintf(stderr, "The server requested a client certificate.\n" - "Presently, this process is not automated.\n" -diff --git a/src/util.c b/src/util.c -index 1cb0bf42b6319e6bd1b0cf0cc94e28627e4f9c68..8441b584ff199ffd81472539a59acce5d0a18f42 100644 ---- a/src/util.c -+++ b/src/util.c -@@ -11,7 +11,7 @@ #include <string.h> - #include <sys/stat.h> - #include "util.h" - --static void -+void - posix_dirname(char *path, char *dname) - { - char p[PATH_MAX+1]; diff --git a/sources/cgmnlm.git/commits/40308b8b0bd7e15d0f6e2971b901a9c09a4bc681.patch b/sources/cgmnlm.git/commits/40308b8b0bd7e15d0f6e2971b901a9c09a4bc681.patch @@ -1,18 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 0546486340b7299be3c97266dd534a1354bf31a3..d05180c7a74c88bd7ed8ba551a4a522bb8b74009 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -759,6 +759,13 @@ - FILE *fp = fopen(path, "r"); - if (!fp) { - 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->bio = NULL; -+ resp->ssl = NULL; -+ resp->ssl_ctx = NULL; -+ resp->meta = NULL; -+ resp->fd = -1; - free(path); - break; - } diff --git a/sources/cgmnlm.git/commits/4134dc1b4a37b65f8176d799f03342583b49d932.patch b/sources/cgmnlm.git/commits/4134dc1b4a37b65f8176d799f03342583b49d932.patch @@ -1,16 +0,0 @@ -diff --git a/config.sh b/config.sh -index 55bc9a2ab6b15f91c655a4ab033fcbd6107b83fe..5bd34bb30dec02422855edbf37817f8276840211 100644 ---- a/config.sh -+++ b/config.sh -@@ -85,6 +85,11 @@ find_library() { - name="$1" - pc="$2" - printf "Checking for %s... " "$name" -+ if ! command -v pkg-config >/dev/null -+ then -+ printf "ERROR: pkg-config not found\n" -+ return 1 -+ fi - if ! pkg-config "$pc" 2>/dev/null - then - printf "NOT FOUND\n" diff --git a/sources/cgmnlm.git/commits/4274b06fe4b2702af297cd0cee3d7871741899ec.patch b/sources/cgmnlm.git/commits/4274b06fe4b2702af297cd0cee3d7871741899ec.patch @@ -1,30 +0,0 @@ -diff --git a/README.md b/README.md -index 9aa989fe358ecccd9b984b1aff0df97c6c2ceec0..4b03a4782f0b97d8687b3360f20d9c8748f88ca3 100644 ---- a/README.md -+++ b/README.md -@@ -14,7 +14,7 @@ - basic Client Certificate support (no autocreation of client certs currently) - - ## Non-Features: - --- no inlinig of any link type -+- no inlining of any link type - - no caching of page content - - no persistent history across sessions - -@@ -25,11 +25,11 @@ - It includes the following modifications: - - colored headings & links - - default 4 char indenting --- s command to directly search in geminispace (via geminispace.info) --- k command to remove the bookmark for the current page --- e[N] command to open a link in default external program (requires `xdg-open`) --- t[N] command to download the content behind a link to a temporary file --- a command to switch between display of preformatted blocks and alt text (if available) -+- `s` command to directly search in geminispace (via geminispace.info) -+- `k` command to remove the bookmark for the current page -+- `e[N]` command to open a link in default external program (requires `xdg-open`) -+- `t[N]` command to download the content behind a link to a temporary file -+- `a` command to switch between display of preformatted blocks and alt text (if available) - - The actual colors used depend on your terminal palette: - - heading 1: light red diff --git a/sources/cgmnlm.git/commits/46b5d74576ffce397c83ac53ebfacb25e1cdc851.patch b/sources/cgmnlm.git/commits/46b5d74576ffce397c83ac53ebfacb25e1cdc851.patch @@ -1,131 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 391d2b4a2a2dddd7e688a950bb852dfa0687cc32..cbc04be66573d4980e24f00761c54d4ac8a1a88f 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -139,10 +139,7 @@ - static char * - trim_ws(char *in) - { -- for (int i = strlen(in) - 1; in[i] && isspace(in[i]); --i) { -- in[i] = 0; -- } -- for (; *in && isspace(*in); ++in); -+ while (*in && isspace(*in)) ++in; - return in; - } - -@@ -203,61 +200,80 @@ struct link **next = &browser->links; - while (text != NULL || gemini_parser_next(&p, &tok) == 0) { - switch (tok.token) { - case GEMINI_TEXT: -- // TODO: Run other stuff through wrap() -+ col += fprintf(browser->tty, " "); - if (text == NULL) { - text = tok.text; - } -- -- do { -+ break; -+ case GEMINI_LINK: -+ if (text == NULL) { -+ col += fprintf(browser->tty, "%d) ", nlinks++); -+ text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); -+ *next = calloc(1, sizeof(struct link)); -+ (*next)->url = strdup(trim_ws(tok.link.url)); -+ next = &(*next)->next; -+ } else { - col += fprintf(browser->tty, " "); -- int w = wrap(browser->tty, text, &ws, &row, &col); -- text += w; -- if (row >= ws.ws_row - 4) { -- break; -- } -- } while (text[0]); -- -- if (!text[0]) { -- text = NULL; - } - break; -- case GEMINI_LINK: -- col += fprintf(browser->tty, "%d) %s\n", nlinks++, -- trim_ws(tok.link.text ? tok.link.text : tok.link.url)); -- *next = calloc(1, sizeof(struct link)); -- (*next)->url = strdup(trim_ws(tok.link.url)); -- next = &(*next)->next; -- break; - case GEMINI_PREFORMATTED: - continue; // TODO - case GEMINI_HEADING: -- for (int n = tok.heading.level; n; --n) { -- col += fprintf(browser->tty, "#"); -- } -- for (int n = 3 - tok.heading.level; n > 1; --n) { -- col += fprintf(browser->tty, " "); -+ if (text == NULL) { -+ for (int n = tok.heading.level; n; --n) { -+ col += fprintf(browser->tty, "#"); -+ } -+ switch (tok.heading.level) { -+ case 1: -+ col += fprintf(browser->tty, " "); -+ break; -+ case 2: -+ case 3: -+ col += fprintf(browser->tty, " "); -+ break; -+ } -+ text = trim_ws(tok.heading.title); -+ } else { -+ col += fprintf(browser->tty, " "); - } -- col += fprintf(browser->tty, " %s\n", -- trim_ws(tok.heading.title)); - break; - case GEMINI_LIST_ITEM: -- col += fprintf(browser->tty, " %s %s\n", -- browser->unicode ? "•" : "*", -- trim_ws(tok.list_item)); -+ if (text == NULL) { -+ col += fprintf(browser->tty, " %s ", -+ browser->unicode ? "•" : "*"); -+ text = trim_ws(tok.list_item); -+ } else { -+ col += fprintf(browser->tty, " "); -+ } - break; - case GEMINI_QUOTE: -- col += fprintf(browser->tty, " %s %s\n", -- browser->unicode ? "|" : "|", -- trim_ws(tok.quote_text)); -+ if (text == NULL) { -+ col += fprintf(browser->tty, " %s ", -+ browser->unicode ? "|" : "|"); -+ text = trim_ws(tok.quote_text); -+ } else { -+ col += fprintf(browser->tty, " "); -+ } - break; - } - -+ if (text) { -+ int w = wrap(browser->tty, text, &ws, &row, &col); -+ text += w; -+ if (text[0] && row < ws.ws_row - 4) { -+ continue; -+ } -+ -+ if (!text[0]) { -+ text = NULL; -+ } -+ } -+ - while (col >= ws.ws_col) { - col -= ws.ws_col; - ++row; - } -- ++row; -- col = 0; -+ ++row; col = 0; - - if (browser->pagination && row >= ws.ws_row - 4) { - char prompt[4096]; diff --git a/sources/cgmnlm.git/commits/479ea9e74f4b66645c0d7b51d99adf420d831f23.patch b/sources/cgmnlm.git/commits/479ea9e74f4b66645c0d7b51d99adf420d831f23.patch @@ -1,36 +0,0 @@ -diff --git a/README.md b/README.md -index 3c999cee8bbcb5b0bfbc6732f9ecea0ba231d043..aa9f811a9bfd9cfb7b6654954e33fc62e868f8c4 100644 ---- a/README.md -+++ b/README.md -@@ -24,6 +24,7 @@ - It includes the following modifications: - - colored headings & links - - default 4 char indenting -+- s command to directly search in geminispace (via geminispace.info) - - k command to remove the bookmark for the current page - - e[N] command to open a link in default external program (requires `xdg-open`) - - t[N] command to download the content behind a link to a temporary file -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 2c35f76f8ee4b6dfa6af5bbe8af790a3e98afbbc..e28bf70afc5d6fd4c5c6eff85d85198d62a127e8 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -93,6 +93,7 @@ "m\t\tSave bookmark\n" - "M\t\tBrowse bookmarks\n" - "k\t\tRemove bookmark for current page\n" - "r\t\tReload the page\n" -+ "s\t\tSearch via geminispace.info\n" - "/<text>\t\tsearch for text (POSIX regular expression)\n" - "n\t\tjump to next search match\n" - "d <path>\tDownload page to <path>\n" -@@ -567,6 +568,11 @@ goto exit; - case 'q': - if (in[1]) break; - result = PROMPT_QUIT; -+ goto exit; -+ case 's': -+ if (in[1]) break; -+ set_url(browser, "gemini://geminispace.info/search", &browser->history); -+ result = PROMPT_ANSWERED; - goto exit; - case 'b': - if (in[1]) historyhops =(int)strtol(in+1, &endptr, 10); diff --git a/sources/cgmnlm.git/commits/48d0feed6d097c54662a7f231c7bc4704837f023.patch b/sources/cgmnlm.git/commits/48d0feed6d097c54662a7f231c7bc4704837f023.patch @@ -1,248 +0,0 @@ -diff --git a/configure b/configure -index 407886859d8ca0b9cbcb085ecef89d004c68cde4..aca6e8a1eaa9a6271f03eb8863640d0d93cdf435 100755 ---- a/configure -+++ b/configure -@@ -7,6 +7,7 @@ genrules gmni \ - src/client.c \ - src/escape.c \ - src/gmni.c \ -+ src/parser.c \ - src/url.c - } - -diff --git a/include/gmni.h b/include/gmni.h -index 4d46380f2ef13db72ebf2e1a3434865142b6bcbc..d78d1c8eb4e94507c363219573691e60d40e8ea8 100644 ---- a/include/gmni.h -+++ b/include/gmni.h -@@ -103,4 +103,64 @@ // Returns the general response class (i.e. with the second digit set to zero) - // of the given Gemini status code. - enum gemini_status_class gemini_response_class(enum gemini_status status); - -+enum gemini_tok { -+ GEMINI_TEXT, -+ GEMINI_LINK, -+ GEMINI_PREFORMATTED, -+ GEMINI_HEADING, -+ GEMINI_LIST_ITEM, -+ GEMINI_QUOTE, -+}; -+ -+struct gemini_token { -+ enum gemini_tok token; -+ -+ // The token field determines which of the union members is valid. -+ union { -+ char *text; -+ -+ struct { -+ char *text; -+ char *url; // May be NULL -+ } link; -+ -+ struct { -+ char *text; -+ char *alt_text; // May be NULL -+ } preformatted; -+ -+ struct { -+ char *title; -+ int level; // 1, 2, or 3 -+ } heading; -+ -+ char *list_item; -+ char *quote_text; -+ }; -+}; -+ -+struct gemini_parser { -+ BIO *f; -+ char *buf; -+ size_t bufsz; -+ size_t bufln; -+}; -+ -+// Initializes a text/gemini parser which reads from the specified BIO. -+void gemini_parser_init(struct gemini_parser *p, BIO *f); -+ -+// Finishes this text/gemini parser and frees up its resources. -+void gemini_parser_finish(struct gemini_parser *p); -+ -+// Reads the next token from a text/gemini file. -+// -+// Returns 0 on success, 1 on EOF, and -1 on failure. -+// -+// Caller must call gemini_token_finish before exiting or re-using the token -+// 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. -+void gemini_token_finish(struct gemini_token *token); -+ - #endif -diff --git a/src/gmni.c b/src/gmni.c -index 6e27b2f859c692f37fb483184cda245a7e979cbe..75c6c5afb6e7f1f286d314d93f2b44ef8414afb3 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -233,9 +233,11 @@ case SHOW_HEADERS: - printf("%d %s\n", resp.status, resp.meta); - /* fallthrough */ - case OMIT_HEADERS: -- if (resp.status / 10 != 2) { -+ if (gemini_response_class(resp.status) != -+ GEMINI_STATUS_CLASS_SUCCESS) { - break; - } -+ - char last; - char buf[BUFSIZ]; - for (int n = 1; n > 0;) { -diff --git a/src/parser.c b/src/parser.c -new file mode 100644 -index 0000000000000000000000000000000000000000..ffcc28767be7d638d18609764999d004790aa9a2 ---- /dev/null -+++ b/src/parser.c -@@ -0,0 +1,144 @@ -+#include <assert.h> -+#include <ctype.h> -+#include <openssl/bio.h> -+#include <stddef.h> -+#include <stdlib.h> -+#include <string.h> -+#include "gmni.h" -+ -+void -+gemini_parser_init(struct gemini_parser *p, BIO *f) -+{ -+ p->f = f; -+ p->bufln = 0; -+ p->bufsz = BUFSIZ; -+ p->buf = malloc(p->bufsz + 1); -+ p->buf[0] = 0; -+ BIO_up_ref(p->f); -+} -+ -+void -+gemini_parser_finish(struct gemini_parser *p) -+{ -+ if (!p) { -+ return; -+ } -+ BIO_free(p->f); -+ free(p->buf); -+} -+ -+int -+gemini_parser_next(struct gemini_parser *p, struct gemini_token *tok) -+{ -+ memset(tok, 0, sizeof(*tok)); -+ -+ int eof = 0; -+ while (!strstr(p->buf, "\n")) { -+ if (p->bufln == p->bufsz) { -+ p->bufsz *= 2; -+ char *buf = realloc(p->buf, p->bufsz); -+ assert(buf); -+ p->buf = buf; -+ } -+ -+ int n = BIO_read(p->f, &p->buf[p->bufln], p->bufsz - p->bufln); -+ if (n == -1) { -+ return -1; -+ } else if (n == 0) { -+ eof = 1; -+ break; -+ } -+ p->bufln += n; -+ p->buf[p->bufln] = 0; -+ } -+ -+ // TODO: Collapse multi-line text for the user-agent to wrap -+ char *end; -+ if ((end = strstr(p->buf, "\n")) != NULL) { -+ *end = 0; -+ } -+ -+ // TODO: Provide whitespace trimming helper function -+ if (strncmp(p->buf, "=>", 2) == 0) { -+ tok->token = GEMINI_LINK; -+ int i = 2; -+ while (p->buf[i] && isspace(p->buf[i])) ++i; -+ tok->link.url = &p->buf[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]); -+ } -+ break; -+ } -+ } -+ -+ tok->link.url = strdup(tok->link.url); -+ } else if (strncmp(p->buf, "```", 3) == 0) { -+ tok->token = GEMINI_PREFORMATTED; // TODO -+ tok->preformatted.text = strdup("<text>"); -+ tok->preformatted.alt_text = strdup("<alt-text>"); -+ } else if (p->buf[0] == '#') { -+ tok->token = GEMINI_HEADING; -+ int level = 1; -+ while (p->buf[level] == '#' && level < 3) { -+ ++level; -+ } -+ tok->heading.level = level; -+ tok->heading.title = strdup(&p->buf[level]); -+ } else if (p->buf[0] == '*') { -+ tok->token = GEMINI_LIST_ITEM; -+ tok->list_item = strdup(&p->buf[1]); -+ } else if (p->buf[0] == '>') { -+ tok->token = GEMINI_QUOTE; -+ tok->quote_text = strdup(&p->buf[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; -+ } else { -+ p->buf[0] = 0; -+ p->bufln = 0; -+ } -+ -+ return eof; -+} -+ -+void -+gemini_token_finish(struct gemini_token *tok) -+{ -+ if (!tok) { -+ return; -+ } -+ -+ switch (tok->token) { -+ case GEMINI_TEXT: -+ free(tok->text); -+ break; -+ case GEMINI_LINK: -+ free(tok->link.text); -+ free(tok->link.url); -+ break; -+ case GEMINI_PREFORMATTED: -+ free(tok->preformatted.text); -+ free(tok->preformatted.alt_text); -+ break; -+ case GEMINI_HEADING: -+ free(tok->heading.title); -+ break; -+ case GEMINI_LIST_ITEM: -+ free(tok->list_item); -+ break; -+ case GEMINI_QUOTE: -+ free(tok->quote_text); -+ break; -+ } -+} diff --git a/sources/cgmnlm.git/commits/49c0c523c69842f8ebc33135947591cf6f7a7cab.patch b/sources/cgmnlm.git/commits/49c0c523c69842f8ebc33135947591cf6f7a7cab.patch @@ -1,13 +0,0 @@ -diff --git a/src/gmni.c b/src/gmni.c -index 000a56550a686928f84e2c301c9017a8c48b3d68..61f41e29c324d980bd3a1f270e78649cdcec88e0 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -315,7 +315,7 @@ ret = download_resp(stderr, resp, output_file, url); - break; - } - -- char last; -+ char last = 0; - char buf[BUFSIZ]; - for (int n = 1; n > 0;) { - n = BIO_read(resp.bio, buf, BUFSIZ); diff --git a/sources/cgmnlm.git/commits/49eea555e605e6e0155756ad9739a5729340db81.patch b/sources/cgmnlm.git/commits/49eea555e605e6e0155756ad9739a5729340db81.patch @@ -1,12 +0,0 @@ -diff --git a/src/tofu.c b/src/tofu.c -index 50a295870876265849eae7fdf0de926a3ad309fd..5b34850de9e17dcd59fc64a3227cd92bfb0a6cef 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -88,7 +88,6 @@ cc->hash[i], i + 1 == sizeof(cc->hash) ? "" : ":"); - } - - enum tofu_error error = TOFU_UNTRUSTED_CERT; -- (void)error; - struct known_host *host = cc->store->known_hosts; - while (host) { - if (strcmp(host->host, cc->server_name) != 0) { diff --git a/sources/cgmnlm.git/commits/4a6172f1bf9cb41eb1ce3a5f720f9ebe4febc62b.patch b/sources/cgmnlm.git/commits/4a6172f1bf9cb41eb1ce3a5f720f9ebe4febc62b.patch @@ -1,43 +0,0 @@ -diff --git a/Makefile b/Makefile -index 9d17d35eb67ee4b86a2d9219c3f1fce98695a521..5e468e26f12fef2c066826e4e5662e51ca4dde42 100644 ---- a/Makefile -+++ b/Makefile -@@ -56,13 +56,16 @@ @rm -rf "$(OUTDIR)" - - install: all install_docs - mkdir -p $(BINDIR) -- install -Dm755 gmni $(BINDIR)/gmni -- install -Dm755 gmnlm $(BINDIR)/gmnlm -- install -Dm755 libgmni.a $(LIBDIR)/libgmni.a -- install -Dm644 include/gmni/gmni.h $(INCLUDEDIR)/gmni/gmni.h -- install -Dm644 include/gmni/tofu.h $(INCLUDEDIR)/gmni/tofu.h -- install -Dm644 include/gmni/url.h $(INCLUDEDIR)/gmni/url.h -- install -Dm644 libgmni.pc $(LIBDIR)/pkgconfig/libgmni.pc -+ mkdir -p $(LIBDIR) -+ mkdir -p $(INCLUDEDIR)/gmni -+ mkdir -p $(LIBDIR)/pkgconfig -+ install -m755 gmni $(BINDIR)/gmni -+ install -m755 gmnlm $(BINDIR)/gmnlm -+ install -m755 libgmni.a $(LIBDIR)/libgmni.a -+ install -m644 include/gmni/gmni.h $(INCLUDEDIR)/gmni/gmni.h -+ install -m644 include/gmni/tofu.h $(INCLUDEDIR)/gmni/tofu.h -+ install -m644 include/gmni/url.h $(INCLUDEDIR)/gmni/url.h -+ install -m644 libgmni.pc $(LIBDIR)/pkgconfig/libgmni.pc - - uninstall: - rm -f $(BINDIR)/gmni -diff --git a/config.sh b/config.sh -index ca5444c73b47daa204d9dd17a3949147385dedec..fd6a325bb21807ed6b6dc871b64479ebfe89a68d 100644 ---- a/config.sh -+++ b/config.sh -@@ -127,8 +127,8 @@ echo yes - all="$all docs" - install_docs=" - mkdir -p \$(MANDIR)/man1 -- install -Dm644 doc/gmni.1 \$(MANDIR)/man1/gmni.1 -- install -Dm644 doc/gmnlm.1 \$(MANDIR)/man1/gmnlm.1" -+ install -m644 doc/gmni.1 \$(MANDIR)/man1/gmni.1 -+ install -m644 doc/gmnlm.1 \$(MANDIR)/man1/gmnlm.1" - else - echo no - fi diff --git a/sources/cgmnlm.git/commits/4b2437b17e00d61da9356ac96bb38a8043c66fca.patch b/sources/cgmnlm.git/commits/4b2437b17e00d61da9356ac96bb38a8043c66fca.patch @@ -1,12 +0,0 @@ -diff --git a/.gitignore b/.gitignore -index 7b35cd8ef5366f89f8db24b1f62f0a1f2c3e336d..6ec8d5188d32b997f3a91f8a11f64f4e79b43770 100644 ---- a/.gitignore -+++ b/.gitignore -@@ -1,5 +1,6 @@ - .build - build --gmnic -+gmni -+gmnlm - *.1 - *.o diff --git a/sources/cgmnlm.git/commits/4b7fba261a70bd37e160a7304d454c72c1f75b69.patch b/sources/cgmnlm.git/commits/4b7fba261a70bd37e160a7304d454c72c1f75b69.patch @@ -1,13 +0,0 @@ -diff --git a/src/gmni.c b/src/gmni.c -index a61ee7bcb3ce77da24b1db97d379c2003c61d055..171e1407ba2dfa3341f0693ae8d60bb9cedb6564 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -85,7 +85,7 @@ fprintf(stderr, - "The certificate offered by this server is of unknown trust. " - "Its fingerprint is: \n" - "%s\n\n" -- "Use -j trust to trust temporarily, or -j always to add to the trust store.\n", fingerprint); -+ "Use -j once to trust temporarily, or -j always to add to the trust store.\n", fingerprint); - break; - case TOFU_FINGERPRINT_MISMATCH: - fprintf(stderr, diff --git a/sources/cgmnlm.git/commits/4bc55aaa138171db365377db0624c3ce0d878257.patch b/sources/cgmnlm.git/commits/4bc55aaa138171db365377db0624c3ce0d878257.patch @@ -1,59 +0,0 @@ -diff --git a/src/gmnic.c b/src/gmnic.c -index 794b94ba2235757864acb81194b8de88220cc6da..60d5c68530fffacbeea62c1a8b78497857e02a02 100644 ---- a/src/gmnic.c -+++ b/src/gmnic.c -@@ -34,9 +34,10 @@ INPUT_SUPPRESS, - }; - enum input_mode input_mode = INPUT_READ; - FILE *input_source = stdin; -+ bool linefeed = true; - - int c; -- while ((c = getopt(argc, argv, "46C:d:D:hLiIN")) != -1) { -+ while ((c = getopt(argc, argv, "46C:d:D:hlLiIN")) != -1) { - switch (c) { - case '4': - assert(0); // TODO -@@ -67,6 +68,9 @@ break; - case 'h': - usage(argv[0]); - return 0; -+ case 'l': -+ linefeed = false; -+ break; - case 'L': - assert(0); // TODO: Follow redirects - break; -@@ -81,7 +85,7 @@ case 'N': - input_mode = INPUT_SUPPRESS; - break; - default: -- fprintf(stderr, "fatal: unknown flag %c", c); -+ fprintf(stderr, "fatal: unknown flag %c\n", c); - return 1; - } - } -@@ -167,8 +171,9 @@ case OMIT_HEADERS: - if (resp.status / 10 != 2) { - break; - } -- for (int n = 1; n > 0;) { -- char buf[BUFSIZ]; -+ char buf[BUFSIZ]; -+ int n; -+ for (n = 1; n > 0;) { - n = BIO_read(resp.bio, buf, BUFSIZ); - if (n == -1) { - fprintf(stderr, "Error: read\n"); -@@ -184,6 +189,11 @@ return 1; - } - w += x; - } -+ } -+ if (strncmp(resp.meta, "text/", 5) == 0 -+ && linefeed -+ && buf[n - 1] != '\n') { -+ printf("\n"); - } - break; - } diff --git a/sources/cgmnlm.git/commits/4c0f931d6688d06df2e22d001182f6fa1b776fab.patch b/sources/cgmnlm.git/commits/4c0f931d6688d06df2e22d001182f6fa1b776fab.patch @@ -1,77 +0,0 @@ -diff --git a/README.md b/README.md -index ea1f81f04aac1b93f73fca7d984b6e3f02415a2b..e1b424367e49daad233793f663f6340849eb4286 100644 ---- a/README.md -+++ b/README.md -@@ -17,6 +17,7 @@ This project is of fork of https://git.sr.ht/~sircmpwn/gmni - - It includes the following modifications: - - default 4 char indenting -+- e[N] command to open a link in default external program (requires `xdg-open`) - - colored headings & links - - The actual colors used depend on your terminal palette: -diff --git a/src/cgmnlm.c b/src/cgmnlm.c -index 6f0b8773c16001ab95edaec345879874483114a9..5bf7dfdbea9a60cd610d76b82bb8e9bc3d5ffa1d 100644 ---- a/src/cgmnlm.c -+++ b/src/cgmnlm.c -@@ -82,6 +82,7 @@ "The following commands are available:\n\n" - "q\tQuit\n" - "[N]\tFollow Nth link (where N is a number)\n" - "u[N]\tShow URL of Nth link (where N is a number)\n" -+ "e[N]\tSend URL of Nth link in external default program\n" - "b\tBack (in the page history)\n" - "f\tForward (in the page history)\n" - "H\tView all page history\n" -@@ -502,8 +503,11 @@ { - enum prompt_result result; - fprintf(browser->tty, "%s", prompt); - -- size_t l = 0; -+ struct link *link = browser->links; -+ char *endptr = NULL; -+ int linksel = 0; - char *in = NULL; -+ size_t l = 0; - ssize_t n = getline(&in, &l, browser->tty); - if (n == -1 && feof(browser->tty)) { - result = PROMPT_QUIT; -@@ -592,11 +596,10 @@ fprintf(stderr, "Cannot move to next result; we are not searching for anything\n"); - result = PROMPT_AGAIN; - goto exit; - } -+ case 'e': - case 'u': - if (!in[1]) break; -- struct link *link = browser->links; -- char *endptr; -- int linksel = (int)strtol(in+1, &endptr, 10); -+ linksel = (int)strtol(in+1, &endptr, 10); - if (!endptr[0] && linksel >= 0) { - while (linksel > 0 && link) { - link = link->next; -@@ -607,8 +610,12 @@ if (!link) { - fprintf(stderr, "Error: no such link.\n"); - } else { - fprintf(browser->tty, "=> %s\n", link->url); -- result = PROMPT_AGAIN; -- goto exit; -+ if (in[0] == 'e') { -+ char xdgopen[4096]; -+ snprintf(xdgopen, sizeof(xdgopen), "xdg-open %s", link->url); -+ if ( !system(xdgopen) ) fprintf(browser->tty, "Link send to xdg-open\n"); -+ } -+ fprintf(browser->tty, "\n"); - } - } else { - fprintf(stderr, "Error: invalid argument.\n"); -@@ -664,9 +671,7 @@ result = PROMPT_AGAIN; - goto exit; - } - -- struct link *link = browser->links; -- char *endptr; -- int linksel = (int)strtol(in, &endptr, 10); -+ linksel = (int)strtol(in, &endptr, 10); - if ((endptr[0] == '\0' || endptr[0] == '|') && linksel >= 0) { - while (linksel > 0 && link) { - link = link->next; diff --git a/sources/cgmnlm.git/commits/4c12342bcad95cdc44f9107161429524226a2d37.patch b/sources/cgmnlm.git/commits/4c12342bcad95cdc44f9107161429524226a2d37.patch @@ -1,13 +0,0 @@ -diff --git a/README.md b/README.md -index dfd2e1744003996f1e629c78e459dac70985a430..ce71012d402a0c975c73f6d4acd2922f32c16cbc 100644 ---- a/README.md -+++ b/README.md -@@ -5,7 +5,7 @@ - - A CLI utility (like curl): gmni - - A [line-mode browser](https://en.wikipedia.org/wiki/Line_Mode_Browser): gmnlm - --[](https://asciinema.org/a/ldo2gV7qiDoBXvGwuD6x1jbn3) -+[](https://asciinema.org/a/Y7viodM01e0AXYyf40CwSLAVA) - - Dependencies: - diff --git a/sources/cgmnlm.git/commits/4dd50ac07e82dfc1785f98a3535109e2d738029d.patch b/sources/cgmnlm.git/commits/4dd50ac07e82dfc1785f98a3535109e2d738029d.patch @@ -1,31 +0,0 @@ -diff --git a/src/gmni.c b/src/gmni.c -index f3015ac679eba77397fb4aac5068f3a63e1cdbab..ff6c619f7c6cde57faa7398d4201d2cf867bf05a 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -290,6 +290,10 @@ exit = true; - goto next; - } - -+ if (header_mode != OMIT_HEADERS) { -+ printf("%d %s\n", resp.status, resp.meta); -+ } -+ - switch (gemini_response_class(resp.status)) { - case GEMINI_STATUS_CLASS_INPUT: - if (input_mode == INPUT_SUPPRESS) { -@@ -351,14 +355,7 @@ exit = true; - break; - } - -- switch (header_mode) { -- case ONLY_HEADERS: -- printf("%d %s\n", resp.status, resp.meta); -- break; -- case SHOW_HEADERS: -- printf("%d %s\n", resp.status, resp.meta); -- /* fallthrough */ -- case OMIT_HEADERS: -+ if (header_mode != ONLY_HEADERS) { - if (gemini_response_class(resp.status) != - GEMINI_STATUS_CLASS_SUCCESS) { - break; diff --git a/sources/cgmnlm.git/commits/4e61e266076fbda20cbf268300e7f645669c7062.patch b/sources/cgmnlm.git/commits/4e61e266076fbda20cbf268300e7f645669c7062.patch @@ -1,14 +0,0 @@ -diff --git a/src/gmni.c b/src/gmni.c -index 245ba76363b58e9c9ddbf87464d48cfb38463892..a61ee7bcb3ce77da24b1db97d379c2003c61d055 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -84,7 +84,8 @@ case TOFU_UNTRUSTED_CERT: - fprintf(stderr, - "The certificate offered by this server is of unknown trust. " - "Its fingerprint is: \n" -- "%s\n\n", fingerprint); -+ "%s\n\n" -+ "Use -j trust to trust temporarily, or -j always to add to the trust store.\n", fingerprint); - break; - case TOFU_FINGERPRINT_MISMATCH: - fprintf(stderr, diff --git a/sources/cgmnlm.git/commits/514cb373019e94f743424b2813602722ca09b917.patch b/sources/cgmnlm.git/commits/514cb373019e94f743424b2813602722ca09b917.patch @@ -1,13 +0,0 @@ -diff --git a/src/client.c b/src/client.c -index 8b6b9e7a3c09656a5d59ed86977cb8b5c39541d4..a8a12f3cfbe1e98fd2045eec257cfaf95c319910 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -212,7 +212,7 @@ } - - char *endptr; - resp->status = (enum gemini_status)strtol(buf, &endptr, 10); -- if (*endptr != ' ' || resp->status < 10 || resp->status >= 70) { -+ if (*endptr != ' ' || resp->status < 10 || (int)resp->status >= 70) { - fprintf(stderr, "invalid status\n"); - res = GEMINI_ERR_PROTOCOL; - goto cleanup; diff --git a/sources/cgmnlm.git/commits/563922a7e2da77b3973dcf707854121932ca244e.patch b/sources/cgmnlm.git/commits/563922a7e2da77b3973dcf707854121932ca244e.patch @@ -1,331 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 2c2b67e0e56d2e6be04bba6cee96cb4a24ff3b51..3598e3fb1770aca2bd646d7a3f2bf642b54c2ab9 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -27,9 +27,18 @@ bool pagination; - struct gemini_options opts; - - FILE *tty; -+ char *plain_url; - struct Curl_URL *url; - struct link *links; - struct history *history; -+ bool running; -+}; -+ -+enum prompt_result { -+ PROMPT_AGAIN, -+ PROMPT_MORE, -+ PROMPT_QUIT, -+ PROMPT_ANSWERED, - }; - - static void -@@ -70,6 +79,63 @@ } - return true; - } - -+static enum prompt_result -+do_prompts(const char *prompt, struct browser *browser) -+{ -+ fprintf(browser->tty, "%s", prompt); -+ -+ size_t l = 0; -+ char *in = NULL; -+ ssize_t n = getline(&in, &l, browser->tty); -+ if (n == -1 && feof(browser->tty)) { -+ return PROMPT_QUIT; -+ } -+ if (strcmp(in, "\n") == 0) { -+ return PROMPT_MORE; -+ } -+ if (strcmp(in, "q\n") == 0) { -+ return PROMPT_QUIT; -+ } -+ if (strcmp(in, "b\n") == 0) { -+ if (!browser->history->prev) { -+ fprintf(stderr, "At beginning of history\n"); -+ return PROMPT_AGAIN; -+ } -+ browser->history = browser->history->prev; -+ set_url(browser, browser->history->url, NULL); -+ return PROMPT_ANSWERED; -+ } -+ if (strcmp(in, "f\n") == 0) { -+ if (!browser->history->next) { -+ fprintf(stderr, "At end of history\n"); -+ return PROMPT_AGAIN; -+ } -+ browser->history = browser->history->next; -+ set_url(browser, browser->history->url, NULL); -+ return PROMPT_ANSWERED; -+ } -+ -+ struct link *link = browser->links; -+ char *endptr; -+ int linksel = (int)strtol(in, &endptr, 10); -+ if (endptr[0] == '\n' && linksel >= 0) { -+ while (linksel > 0 && link) { -+ link = link->next; -+ --linksel; -+ } -+ -+ if (!link) { -+ fprintf(stderr, "Error: no such link.\n"); -+ } else { -+ set_url(browser, link->url, &browser->history); -+ return PROMPT_ANSWERED; -+ } -+ } -+ free(in); -+ -+ return PROMPT_AGAIN; -+} -+ - static char * - trim_ws(char *in) - { -@@ -80,7 +146,7 @@ for (; *in && isspace(*in); ++in); - return in; - } - --static void -+static bool - display_gemini(struct browser *browser, struct gemini_response *resp) - { - // TODO: Strip ANSI escape sequences -@@ -136,29 +202,36 @@ } - ++row; - col = 0; - -- // TODO: It would be nice if we could follow links from this -- // prompt -- if (browser->pagination && row >= ws.ws_row - 1) { -- fprintf(browser->tty, "[Enter for more, or q to stop] "); -+ if (browser->pagination && row >= ws.ws_row - 4) { -+ char prompt[4096]; -+ snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -+ "[Enter]: read more; [n]: follow Nth link; [b]ack; [f]orward; [q]uit\n" -+ "(more) => ", resp->meta, browser->plain_url); -+ enum prompt_result result = PROMPT_AGAIN; -+ while (result == PROMPT_AGAIN) { -+ result = do_prompts(prompt, browser); -+ } - -- size_t n = 0; -- char *l = NULL; -- if (getline(&l, &n, browser->tty) == -1) { -- return; -- } -- if (strcmp(l, "q\n") == 0) { -- return; -+ switch (result) { -+ case PROMPT_AGAIN: -+ case PROMPT_MORE: -+ break; -+ case PROMPT_QUIT: -+ browser->running = false; -+ return true; -+ case PROMPT_ANSWERED: -+ return true; - } - -- free(l); - row = col = 0; - } - } - - gemini_parser_finish(&p); -+ return false; - } - --static void -+static bool - display_plaintext(struct browser *browser, struct gemini_response *resp) - { - // TODO: Strip ANSI escape sequences -@@ -175,20 +248,20 @@ } - } - - (void)row; (void)col; // TODO: generalize pagination -+ return false; - } - --static void -+static bool - display_response(struct browser *browser, struct gemini_response *resp) - { - if (strcmp(resp->meta, "text/gemini") == 0 - || strncmp(resp->meta, "text/gemini;", 12) == 0) { -- display_gemini(browser, resp); -- return; -+ return display_gemini(browser, resp); - } - if (strncmp(resp->meta, "text/", 5) == 0) { -- display_plaintext(browser, resp); -- return; -+ return display_plaintext(browser, resp); - } -+ assert(0); // TODO: Deal with other mimetypes - } - - static char * -@@ -225,19 +298,19 @@ } - return input; - } - --static char * -+// Returns true to skip prompting -+static bool - do_requests(struct browser *browser, struct gemini_response *resp) - { -- char *plain_url; - int nredir = 0; - bool requesting = true; - while (requesting) { - CURLUcode uc = curl_url_get(browser->url, -- CURLUPART_URL, &plain_url, 0); -+ CURLUPART_URL, &browser->plain_url, 0); - assert(uc == CURLUE_OK); // Invariant - -- enum gemini_result res = gemini_request( -- plain_url, &browser->opts, resp); -+ enum gemini_result res = gemini_request(browser->plain_url, -+ &browser->opts, resp); - if (res != GEMINI_OK) { - fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); - requesting = false; -@@ -253,7 +326,8 @@ requesting = false; - break; - } - -- char *new_url = gemini_input_url(plain_url, input); -+ char *new_url = gemini_input_url( -+ browser->plain_url, input); - assert(new_url); - set_url(browser, new_url, NULL); - break; -@@ -278,8 +352,7 @@ resp->status, resp->meta); - break; - case GEMINI_STATUS_CLASS_SUCCESS: - requesting = false; -- display_response(browser, resp); -- break; -+ return display_response(browser, resp); - } - - if (requesting) { -@@ -287,63 +360,7 @@ gemini_response_finish(resp); - } - } - -- return plain_url; --} -- --static bool --do_prompts(const char *prompt, struct browser *browser) --{ -- bool prompting = true; -- while (prompting) { -- fprintf(browser->tty, "%s", prompt); -- -- size_t l = 0; -- char *in = NULL; -- ssize_t n = getline(&in, &l, browser->tty); -- if (n == -1 && feof(browser->tty)) { -- return false; -- } -- if (strcmp(in, "q\n") == 0) { -- return false; -- } -- if (strcmp(in, "b\n") == 0) { -- if (!browser->history->prev) { -- fprintf(stderr, "At beginning of history\n"); -- continue; -- } -- browser->history = browser->history->prev; -- set_url(browser, browser->history->url, NULL); -- break; -- } -- if (strcmp(in, "f\n") == 0) { -- if (!browser->history->next) { -- fprintf(stderr, "At end of history\n"); -- continue; -- } -- browser->history = browser->history->next; -- set_url(browser, browser->history->url, NULL); -- break; -- } -- -- struct link *link = browser->links; -- char *endptr; -- int linksel = (int)strtol(in, &endptr, 10); -- if (endptr[0] == '\n' && linksel >= 0) { -- while (linksel > 0 && link) { -- link = link->next; -- --linksel; -- } -- -- if (!link) { -- fprintf(stderr, "Error: no such link.\n"); -- } else { -- set_url(browser, link->url, &browser->history); -- break; -- } -- } -- free(in); -- } -- return true; -+ return false; - } - - int -@@ -381,24 +398,38 @@ SSL_load_error_strings(); - ERR_load_crypto_strings(); - browser.opts.ssl_ctx = SSL_CTX_new(TLS_method()); - -- bool run = true; - struct gemini_response resp; -- while (run) { -+ browser.running = true; -+ while (browser.running) { - static char prompt[4096]; -- char *plain_url = do_requests(&browser, &resp); -+ if (do_requests(&browser, &resp)) { -+ // Skip prompts -+ goto next; -+ } - -- snprintf(prompt, sizeof(prompt), "\n%s%s at %s\n" -- "[n]: follow Nth link; [o <url>]: open URL; " -- "[b]ack; [f]orward; " -- "[q]uit\n" -+ snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -+ "[n]: follow Nth link; [b]ack; [f]orward; [q]uit\n" - "=> ", -- resp.status == GEMINI_STATUS_SUCCESS ? " " : "", - resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", -- plain_url); -+ browser.plain_url); - gemini_response_finish(&resp); - -- run = do_prompts(prompt, &browser); -+ enum prompt_result result = PROMPT_AGAIN; -+ while (result == PROMPT_AGAIN || result == PROMPT_MORE) { -+ result = do_prompts(prompt, &browser); -+ } -+ switch (result) { -+ case PROMPT_AGAIN: -+ case PROMPT_MORE: -+ assert(0); -+ case PROMPT_QUIT: -+ browser.running = false; -+ break; -+ case PROMPT_ANSWERED: -+ break; -+ } - -+next:; - struct link *link = browser.links; - while (link) { - struct link *next = link->next; diff --git a/sources/cgmnlm.git/commits/5799323f4c92181a3446a729366b230456e93c81.patch b/sources/cgmnlm.git/commits/5799323f4c92181a3446a729366b230456e93c81.patch @@ -1,135 +0,0 @@ -diff --git a/include/gmni.h b/include/gmni.h -index d78d1c8eb4e94507c363219573691e60d40e8ea8..4240c6231010ebb86aead2d49700fe2e3d00b65c 100644 ---- a/include/gmni.h -+++ b/include/gmni.h -@@ -2,6 +2,7 @@ #ifndef GEMINI_CLIENT_H - #define GEMINI_CLIENT_H - #include <netdb.h> - #include <openssl/ssl.h> -+#include <stdbool.h> - #include <sys/socket.h> - - enum gemini_result { -@@ -106,7 +107,9 @@ - enum gemini_tok { - GEMINI_TEXT, - GEMINI_LINK, -- GEMINI_PREFORMATTED, -+ GEMINI_PREFORMATTED_BEGIN, -+ GEMINI_PREFORMATTED_END, -+ GEMINI_PREFORMATTED_TEXT, - GEMINI_HEADING, - GEMINI_LIST_ITEM, - GEMINI_QUOTE, -@@ -124,10 +127,7 @@ char *text; - char *url; // May be NULL - } link; - -- struct { -- char *text; -- char *alt_text; // May be NULL -- } preformatted; -+ char *preformatted; - - struct { - char *title; -@@ -144,6 +144,7 @@ BIO *f; - char *buf; - size_t bufsz; - size_t bufln; -+ bool preformatted; - }; - - // Initializes a text/gemini parser which reads from the specified BIO. -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 5e5d828f6f246e0baa3917177acd8cb6482387f8..8841c4613e208e379c6cd768a65a9a3b631d3146 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -278,8 +278,15 @@ } else { - col += fprintf(out, " "); - } - break; -- case GEMINI_PREFORMATTED: -- continue; // TODO -+ case GEMINI_PREFORMATTED_BEGIN: -+ case GEMINI_PREFORMATTED_END: -+ continue; // Not used -+ case GEMINI_PREFORMATTED_TEXT: -+ col += fprintf(out, "` "); -+ if (text == NULL) { -+ text = tok.text; -+ } -+ break; - case GEMINI_HEADING: - if (text == NULL) { - for (int n = tok.heading.level; n; --n) { -diff --git a/src/parser.c b/src/parser.c -index b9db3d2000f1176df4a21300f7b806ed6a5ded75..5b0f01399d934f593cdf987e096dd2cfb134d114 100644 ---- a/src/parser.c -+++ b/src/parser.c -@@ -1,6 +1,7 @@ - #include <assert.h> - #include <ctype.h> - #include <openssl/bio.h> -+#include <stdbool.h> - #include <stddef.h> - #include <stdlib.h> - #include <string.h> -@@ -15,6 +16,7 @@ p->bufsz = BUFSIZ; - p->buf = malloc(p->bufsz + 1); - p->buf[0] = 0; - BIO_up_ref(p->f); -+ p->preformatted = false; - } - - void -@@ -57,7 +59,15 @@ if ((end = strstr(p->buf, "\n")) != NULL) { - *end = 0; - } - -- if (strncmp(p->buf, "=>", 2) == 0) { -+ if (p->preformatted) { -+ if (strncmp(p->buf, "```", 3) == 0) { -+ tok->token = GEMINI_PREFORMATTED_END; -+ p->preformatted = false; -+ } else { -+ tok->token = GEMINI_PREFORMATTED_TEXT; -+ tok->preformatted = strdup(p->buf); -+ } -+ } else if (strncmp(p->buf, "=>", 2) == 0) { - tok->token = GEMINI_LINK; - int i = 2; - while (p->buf[i] && isspace(p->buf[i])) ++i; -@@ -76,9 +86,11 @@ } - - tok->link.url = strdup(tok->link.url); - } else if (strncmp(p->buf, "```", 3) == 0) { -- tok->token = GEMINI_PREFORMATTED; // TODO -- tok->preformatted.text = strdup("<text>"); -- tok->preformatted.alt_text = strdup("<alt-text>"); -+ tok->token = GEMINI_PREFORMATTED_BEGIN; -+ if (p->buf[3]) { -+ tok->preformatted = strdup(&p->buf[3]); -+ } -+ p->preformatted = true; - } else if (p->buf[0] == '#') { - tok->token = GEMINI_HEADING; - int level = 1; -@@ -125,9 +137,14 @@ case GEMINI_LINK: - free(tok->link.text); - free(tok->link.url); - break; -- case GEMINI_PREFORMATTED: -- free(tok->preformatted.text); -- free(tok->preformatted.alt_text); -+ case GEMINI_PREFORMATTED_BEGIN: -+ free(tok->preformatted); -+ break; -+ case GEMINI_PREFORMATTED_TEXT: -+ free(tok->preformatted); -+ break; -+ case GEMINI_PREFORMATTED_END: -+ // Nothing to free - break; - case GEMINI_HEADING: - free(tok->heading.title); diff --git a/sources/cgmnlm.git/commits/59d19b9894083cecafc4439f7df1031bd6cefb01.patch b/sources/cgmnlm.git/commits/59d19b9894083cecafc4439f7df1031bd6cefb01.patch @@ -1,33 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index f218f0a43c1d32223e6bb73ecc5069c5cd0cf21a..92e866a2a58a222f56993e57849c348ead658ab4 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -157,7 +157,7 @@ } - - char *title = browser->page_title; - if (title) { -- title = trim_ws(strdup(browser->page_title)); -+ title = trim_ws(browser->page_title); - } - - fprintf(f, "=> %s%s%s\n", browser->plain_url, -@@ -166,17 +166,15 @@ fclose(f); - - fprintf(browser->tty, "Bookmark saved: %s\n", - title ? title : browser->plain_url); -- if (title != NULL) { -- free(title); -- } - } - - static void - open_bookmarks(struct browser *browser) - { -- const char *path_fmt = get_data_pathfmt(); -+ char *path_fmt = get_data_pathfmt(); - static char path[PATH_MAX+1]; - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ free(path_fmt); - static char url[PATH_MAX+1+7]; - snprintf(url, sizeof(url), "file://%s", path); - set_url(browser, url, &browser->history); diff --git a/sources/cgmnlm.git/commits/59d43726bb18a1e240a7188b3dd33af5876a126e.patch b/sources/cgmnlm.git/commits/59d43726bb18a1e240a7188b3dd33af5876a126e.patch @@ -1,21 +0,0 @@ -diff --git a/src/client.c b/src/client.c -index 34d25f3794f8069b15a688ebd89b7bf82fdd01e2..d8b67d7b9f47b4473ba6eb278853ea0565739f09 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -59,7 +59,7 @@ { - struct addrinfo *addr; - enum gemini_result res = gemini_get_addrinfo(uri, options, resp, &addr); - if (res != GEMINI_OK) { -- goto cleanup; -+ return res; - } - - struct addrinfo *rp; -@@ -79,7 +79,6 @@ res = GEMINI_ERR_CONNECT; - return res; - } - --cleanup: - if (!options || !options->addr) { - freeaddrinfo(addr); - } diff --git a/sources/cgmnlm.git/commits/5a955c5f241b87018dfb0cda6872dc7ae2784222.patch b/sources/cgmnlm.git/commits/5a955c5f241b87018dfb0cda6872dc7ae2784222.patch @@ -1,456 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 273423b8261a9608c89505b4e154899806f994f4..15256bbd0dcd1c6899e8e91ba5228baf93ce289f 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -22,6 +22,16 @@ char *url; - struct history *prev, *next; - }; - -+struct browser { -+ bool pagination; -+ struct gemini_options opts; -+ -+ FILE *tty; -+ struct Curl_URL *url; -+ struct link *links; -+ struct history *history; -+}; -+ - static void - usage(const char *argv_0) - { -@@ -39,7 +49,7 @@ free(history); - } - - static bool --set_url(struct Curl_URL *url, char *new_url, struct history **history) -+set_url(struct browser *browser, char *new_url, struct history **history) - { - if (history) { - struct history *next = calloc(1, sizeof(struct history)); -@@ -53,7 +63,7 @@ (*history)->next = next; - } - *history = next; - } -- if (curl_url_set(url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { -+ if (curl_url_set(browser->url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { - fprintf(stderr, "Error: invalid URL\n"); - return false; - } -@@ -71,27 +81,29 @@ return in; - } - - static void --display_gemini(FILE *tty, struct gemini_response *resp, -- struct link **next, bool pagination) -+display_gemini(struct browser *browser, struct gemini_response *resp) - { -+ // TODO: Strip ANSI escape sequences - int nlinks = 0; - struct gemini_parser p; - gemini_parser_init(&p, resp->bio); - - struct winsize ws; -- ioctl(fileno(tty), TIOCGWINSZ, &ws); -+ ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); - - int row = 0, col = 0; - struct gemini_token tok; -+ struct link **next = &browser->links; - while (gemini_parser_next(&p, &tok) == 0) { - switch (tok.token) { - case GEMINI_TEXT: - // TODO: word wrap -- col += fprintf(tty, " %s\n", trim_ws(tok.text)); -+ col += fprintf(browser->tty, " %s\n", -+ trim_ws(tok.text)); - break; - case GEMINI_LINK: -- col += fprintf(tty, "[%d] %s\n", nlinks++, trim_ws( -- tok.link.text ? tok.link.text : tok.link.url)); -+ col += fprintf(browser->tty, "[%d] %s\n", nlinks++, -+ trim_ws(tok.link.text ? tok.link.text : tok.link.url)); - *next = calloc(1, sizeof(struct link)); - (*next)->url = strdup(trim_ws(tok.link.url)); - next = &(*next)->next; -@@ -100,17 +112,20 @@ case GEMINI_PREFORMATTED: - continue; // TODO - case GEMINI_HEADING: - for (int n = tok.heading.level; n; --n) { -- col += fprintf(tty, "#"); -+ col += fprintf(browser->tty, "#"); - } -- col += fprintf(tty, " %s\n", trim_ws(tok.heading.title)); -+ col += fprintf(browser->tty, " %s\n", -+ trim_ws(tok.heading.title)); - break; - case GEMINI_LIST_ITEM: - // TODO: Option to disable Unicode -- col += fprintf(tty, " • %s\n", trim_ws(tok.list_item)); -+ col += fprintf(browser->tty, " • %s\n", -+ trim_ws(tok.list_item)); - break; - case GEMINI_QUOTE: - // TODO: Option to disable Unicode -- col += fprintf(tty, " | %s\n", trim_ws(tok.quote_text)); -+ col += fprintf(browser->tty, " | %s\n", -+ trim_ws(tok.quote_text)); - break; - } - -@@ -121,12 +136,14 @@ } - ++row; - col = 0; - -- if (pagination && row >= ws.ws_row - 1) { -- fprintf(tty, "[Enter for more, or q to stop] "); -+ // TODO: It would be nice if we could follow links from this -+ // prompt -+ if (browser->pagination && row >= ws.ws_row - 1) { -+ fprintf(browser->tty, "[Enter for more, or q to stop] "); - - size_t n = 0; - char *l = NULL; -- if (getline(&l, &n, tty) == -1) { -+ if (getline(&l, &n, browser->tty) == -1) { - return; - } - if (strcmp(l, "q\n") == 0) { -@@ -142,52 +159,163 @@ gemini_parser_finish(&p); - } - - static void --display_plaintext(FILE *tty, struct gemini_response *resp, bool pagination) -+display_plaintext(struct browser *browser, struct gemini_response *resp) - { -+ // TODO: Strip ANSI escape sequences - struct winsize ws; - int row = 0, col = 0; -- ioctl(fileno(tty), TIOCGWINSZ, &ws); -+ ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); - - char buf[BUFSIZ]; - int n; - while ((n = BIO_read(resp->bio, buf, sizeof(buf)) != 0)) { - while (n) { -- n -= fwrite(buf, 1, n, tty); -+ n -= fwrite(buf, 1, n, browser->tty); - } - } - -- (void)pagination; (void)row; (void)col; // TODO: generalize pagination -+ (void)row; (void)col; // TODO: generalize pagination - } - - static void --display_response(FILE *tty, struct gemini_response *resp, -- struct link **next, bool pagination) -+display_response(struct browser *browser, struct gemini_response *resp) - { - if (strcmp(resp->meta, "text/gemini") == 0 - || strncmp(resp->meta, "text/gemini;", 12) == 0) { -- display_gemini(tty, resp, next, pagination); -+ display_gemini(browser, resp); - return; - } - if (strncmp(resp->meta, "text/", 5) == 0) { -- display_plaintext(tty, resp, pagination); -+ display_plaintext(browser, resp); - return; - } - } - -+static char * -+do_requests(struct browser *browser, struct gemini_response *resp) -+{ -+ char *plain_url; -+ int nredir = 0; -+ bool requesting = true; -+ while (requesting) { -+ CURLUcode uc = curl_url_get(browser->url, -+ CURLUPART_URL, &plain_url, 0); -+ assert(uc == CURLUE_OK); // Invariant -+ -+ enum gemini_result res = gemini_request( -+ plain_url, &browser->opts, resp); -+ if (res != GEMINI_OK) { -+ fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); -+ requesting = false; -+ break; -+ } -+ -+ switch (gemini_response_class(resp->status)) { -+ case GEMINI_STATUS_CLASS_INPUT: -+ assert(0); // TODO -+ case GEMINI_STATUS_CLASS_REDIRECT: -+ if (++nredir >= 5) { -+ requesting = false; -+ fprintf(stderr, "Error: maximum redirects (5) exceeded"); -+ break; -+ } -+ fprintf(stderr, "Following redirect to %s\n", resp->meta); -+ set_url(browser, resp->meta, NULL); -+ break; -+ case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: -+ assert(0); // TODO -+ case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: -+ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: -+ requesting = false; -+ fprintf(stderr, "Server returned %s %d %s\n", -+ resp->status / 10 == 4 ? -+ "TEMPORARY FAILURE" : "PERMANENT FALIURE", -+ resp->status, resp->meta); -+ break; -+ case GEMINI_STATUS_CLASS_SUCCESS: -+ requesting = false; -+ display_response(browser, resp); -+ break; -+ } -+ -+ if (requesting) { -+ gemini_response_finish(resp); -+ } -+ } -+ -+ return plain_url; -+} -+ -+static bool -+do_prompts(const char *prompt, struct browser *browser) -+{ -+ bool prompting = true; -+ while (prompting) { -+ fprintf(browser->tty, "%s", prompt); -+ -+ size_t l = 0; -+ char *in = NULL; -+ ssize_t n = getline(&in, &l, browser->tty); -+ if (n == -1 && feof(browser->tty)) { -+ return false; -+ } -+ if (strcmp(in, "q\n") == 0) { -+ return false; -+ } -+ if (strcmp(in, "b\n") == 0) { -+ if (!browser->history->prev) { -+ fprintf(stderr, "At beginning of history\n"); -+ continue; -+ } -+ browser->history = browser->history->prev; -+ set_url(browser, browser->history->url, NULL); -+ break; -+ } -+ if (strcmp(in, "f\n") == 0) { -+ if (!browser->history->next) { -+ fprintf(stderr, "At end of history\n"); -+ continue; -+ } -+ browser->history = browser->history->next; -+ set_url(browser, browser->history->url, NULL); -+ break; -+ } -+ -+ struct link *link = browser->links; -+ char *endptr; -+ int linksel = (int)strtol(in, &endptr, 10); -+ if (endptr[0] == '\n' && linksel >= 0) { -+ while (linksel > 0 && link) { -+ link = link->next; -+ --linksel; -+ } -+ -+ if (!link) { -+ fprintf(stderr, "Error: no such link.\n"); -+ } else { -+ set_url(browser, link->url, &browser->history); -+ break; -+ } -+ } -+ free(in); -+ } -+ return true; -+} -+ - int - main(int argc, char *argv[]) - { -- bool pagination = true; -- -- struct Curl_URL *url = curl_url(); -- -- FILE *tty = fopen("/dev/tty", "w+"); -+ struct browser browser = { -+ .pagination = true, -+ .url = curl_url(), -+ .tty = fopen("/dev/tty", "w+"), -+ }; - - int c; - while ((c = getopt(argc, argv, "hP")) != -1) { - switch (c) { - case 'P': -- pagination = false; -+ browser.pagination = false; - break; - case 'h': - usage(argv[0]); -@@ -198,9 +326,8 @@ return 1; - } - } - -- struct history *history; - if (optind == argc - 1) { -- set_url(url, argv[optind], &history); -+ set_url(&browser, argv[optind], &browser.history); - } else { - usage(argv[0]); - return 1; -@@ -208,65 +335,13 @@ } - - SSL_load_error_strings(); - ERR_load_crypto_strings(); -- struct gemini_options opts = { -- .ssl_ctx = SSL_CTX_new(TLS_method()), -- }; -+ browser.opts.ssl_ctx = SSL_CTX_new(TLS_method()); - - bool run = true; - struct gemini_response resp; - while (run) { -- struct link *links; - static char prompt[4096]; -- char *plain_url; -- -- int nredir = 0; -- bool requesting = true; -- while (requesting) { -- CURLUcode uc = curl_url_get(url, CURLUPART_URL, &plain_url, 0); -- assert(uc == CURLUE_OK); // Invariant -- -- enum gemini_result res = gemini_request( -- plain_url, &opts, &resp); -- if (res != GEMINI_OK) { -- fprintf(stderr, "Error: %s\n", -- gemini_strerr(res, &resp)); -- requesting = false; -- break; -- } -- -- switch (gemini_response_class(resp.status)) { -- case GEMINI_STATUS_CLASS_INPUT: -- assert(0); // TODO -- case GEMINI_STATUS_CLASS_REDIRECT: -- if (++nredir >= 5) { -- requesting = false; -- fprintf(stderr, "Error: maximum redirects (5) exceeded"); -- break; -- } -- fprintf(stderr, -- "Following redirect to %s\n", resp.meta); -- set_url(url, resp.meta, NULL); -- break; -- case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: -- assert(0); // TODO -- case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: -- case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: -- requesting = false; -- fprintf(stderr, "Server returned %s %d %s\n", -- resp.status / 10 == 4 ? -- "TEMPORARY FAILURE" : "PERMANENT FALIURE", -- resp.status, resp.meta); -- break; -- case GEMINI_STATUS_CLASS_SUCCESS: -- requesting = false; -- display_response(tty, &resp, &links, pagination); -- break; -- } -- -- if (requesting) { -- gemini_response_finish(&resp); -- } -- } -+ char *plain_url = do_requests(&browser, &resp); - - snprintf(prompt, sizeof(prompt), "\n%s%s at %s\n" - "[n]: follow Nth link; [o <url>]: open URL; " -@@ -276,74 +351,22 @@ "=> ", - resp.status == GEMINI_STATUS_SUCCESS ? " " : "", - resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", - plain_url); -- - gemini_response_finish(&resp); - -- bool prompting = true; -- while (prompting) { -- fprintf(tty, "%s", prompt); -+ run = do_prompts(prompt, &browser); - -- size_t l = 0; -- char *in = NULL; -- ssize_t n = getline(&in, &l, tty); -- if (n == -1 && feof(tty)) { -- run = false; -- break; -- } -- if (strcmp(in, "q\n") == 0) { -- run = false; -- break; -- } -- if (strcmp(in, "b\n") == 0) { -- if (!history->prev) { -- fprintf(stderr, "At beginning of history\n"); -- continue; -- } -- history = history->prev; -- set_url(url, history->url, NULL); -- break; -- } -- if (strcmp(in, "f\n") == 0) { -- if (!history->next) { -- fprintf(stderr, "At end of history\n"); -- continue; -- } -- history = history->next; -- set_url(url, history->url, NULL); -- break; -- } -- -- struct link *link = links; -- char *endptr; -- int linksel = (int)strtol(in, &endptr, 10); -- if (endptr[0] == '\n' && linksel >= 0) { -- while (linksel > 0 && link) { -- link = link->next; -- --linksel; -- } -- -- if (!link) { -- fprintf(stderr, "Error: no such link.\n"); -- } else { -- set_url(url, link->url, &history); -- break; -- } -- } -- free(in); -- } -- -- struct link *link = links; -+ struct link *link = browser.links; - while (link) { - struct link *next = link->next; - free(link->url); - free(link); - link = next; - } -- links = NULL; -+ browser.links = NULL; - } - -- history_free(history); -- SSL_CTX_free(opts.ssl_ctx); -- curl_url_cleanup(url); -+ history_free(browser.history); -+ SSL_CTX_free(browser.opts.ssl_ctx); -+ curl_url_cleanup(browser.url); - return 0; - } diff --git a/sources/cgmnlm.git/commits/5ad3f0aaccbcf328756d0eaad0e98068587395d1.patch b/sources/cgmnlm.git/commits/5ad3f0aaccbcf328756d0eaad0e98068587395d1.patch @@ -1,61 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 25afbaf0ce2621c4559fd34a035a9f944485aa3d..7fba942066c9d9870f78311f050ef19dc42f7ce1 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -10,6 +10,7 @@ #include <stdbool.h> - #include <stdio.h> - #include <string.h> - #include <sys/ioctl.h> -+#include <sys/stat.h> - #include <termios.h> - #include <unistd.h> - #include "gmni.h" -@@ -52,6 +53,17 @@ PROMPT_QUIT, - PROMPT_ANSWERED, - PROMPT_NEXT, - }; -+ -+const char *default_bookmarks = -+ "# Welcome to gmni\n\n" -+ "Links:\n\n" -+ // TODO: sub out the URL for the appropriate geminispace version once -+ // sr.ht supports gemini -+ "=> https://sr.ht/~sircmpwn/gmni The gmni browser\n" -+ "=> gemini://gemini.circumlunar.space The gemini protocol\n\n" -+ "This file can be found at %s and may be edited at your pleasure.\n\n" -+ "Bookmarks:\n" -+ ; - - const char *help_msg = - "The following commands are available:\n\n" -@@ -175,6 +187,20 @@ char *path_fmt = get_data_pathfmt(); - static char path[PATH_MAX+1]; - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - free(path_fmt); -+ -+ struct stat buf; -+ if (stat(path, &buf) == -1 && errno == ENOENT) { -+ // TOCTOU, but we almost certainly don't care -+ FILE *f = fopen(path, "a"); -+ if (f == NULL) { -+ fprintf(stderr, "Error opening %s for writing: %s\n", -+ path, strerror(errno)); -+ return; -+ } -+ fprintf(f, default_bookmarks, path); -+ fclose(f); -+ } -+ - static char url[PATH_MAX+1+7]; - snprintf(url, sizeof(url), "file://%s", path); - set_url(browser, url, &browser->history); -@@ -867,8 +893,7 @@ if (!set_url(&browser, argv[optind], &browser.history)) { - return 1; - } - } else { -- usage(argv[0]); -- return 1; -+ open_bookmarks(&browser); - } - - SSL_load_error_strings(); diff --git a/sources/cgmnlm.git/commits/5d3ae7b7f52ba83428ba8d728712e8c1710b2ea0.patch b/sources/cgmnlm.git/commits/5d3ae7b7f52ba83428ba8d728712e8c1710b2ea0.patch @@ -1,20 +0,0 @@ -diff --git a/src/tofu.c b/src/tofu.c -index de3746563ed3e0c515a5c52a053f3a535fda5e09..af5d9f20d78558b3ff7dc9253bef53f2deef1d25 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -175,13 +175,14 @@ tofu->callback = cb; - tofu->cb_data = cb_data; - SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_callback, tofu); - -+ tofu->known_hosts = NULL; -+ - FILE *f = fopen(tofu->known_hosts_path, "r"); - if (!f) { - return; - } - size_t n = 0; - char *line = NULL; -- tofu->known_hosts = NULL; - while (getline(&line, &n, f) != -1) { - struct known_host *host = calloc(1, sizeof(struct known_host)); - char *tok = strtok(line, " "); diff --git a/sources/cgmnlm.git/commits/5fd43e8d02ffea38b5e4a3531e366f2b9b510201.patch b/sources/cgmnlm.git/commits/5fd43e8d02ffea38b5e4a3531e366f2b9b510201.patch @@ -1,15 +0,0 @@ -diff --git a/src/gmni.c b/src/gmni.c -index 4af98fbbe70b09ce5a7fce46863438b0487d8738..245ba76363b58e9c9ddbf87464d48cfb38463892 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -315,8 +315,8 @@ last = buf[n - 1]; - } - ssize_t w = 0; - while (w < (ssize_t)n) { -- ssize_t x = write(STDOUT_FILENO, &buf[w], n - w); -- if (x == -1) { -+ ssize_t x = fwrite(&buf[w], 1, n - w, stdout); -+ if (ferror(stdout)) { - fprintf(stderr, "Error: write: %s\n", - strerror(errno)); - return 1; diff --git a/sources/cgmnlm.git/commits/601f9008863af571980c1cd39920483d59cfbfb4.patch b/sources/cgmnlm.git/commits/601f9008863af571980c1cd39920483d59cfbfb4.patch @@ -1,243 +0,0 @@ -diff --git a/configure b/configure -index 151bdae8c12d9ad07d3a5240d7c554f4c62664c8..7412cf545fa96e6be169c0055778b83a06b557f5 100755 ---- a/configure -+++ b/configure -@@ -16,7 +16,8 @@ src/client.c \ - src/escape.c \ - src/gmnlm.c \ - src/parser.c \ -- src/url.c -+ src/url.c \ -+ src/util.c - } - - all="gmni gmnlm" -diff --git a/include/util.h b/include/util.h -new file mode 100644 -index 0000000000000000000000000000000000000000..cf731bfdc75a750df6f18873f48dd859786587c7 ---- /dev/null -+++ b/include/util.h -@@ -0,0 +1,12 @@ -+#ifndef GEMINI_UTIL_H -+#define GEMINI_UTIL_H -+ -+struct pathspec { -+ const char *var; -+ const char *path; -+}; -+ -+char *getpath(const struct pathspec *paths, size_t npaths); -+int mkdirs(char *path, mode_t mode); -+ -+#endif -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 4b844200eeb4318d694566d7a84eb3e64c7c7668..c5d5da36ffad8772315928e8a69b785d994511e4 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -12,6 +12,7 @@ #include <termios.h> - #include <unistd.h> - #include "gmni.h" - #include "url.h" -+#include "util.h" - - struct link { - char *url; -@@ -29,6 +30,7 @@ struct gemini_options opts; - - FILE *tty; - char *plain_url; -+ char *page_title; - struct Curl_URL *url; - struct link *links; - struct history *history; -@@ -52,6 +54,8 @@ "q\tQuit\n" - "N\tFollow Nth link (where N is a number)\n" - "b\tBack (in the page history)\n" - "f\tForward (in the page history)\n" -+ "m\tSave bookmark\n" -+ "M\tBrowse bookmarks\n" - "\n" - "Other commands include:\n\n" - "<Enter>\tread more lines\n" -@@ -98,6 +102,63 @@ } - return true; - } - -+static char * -+get_data_pathfmt() -+{ -+ const struct pathspec paths[] = { -+ {.var = "GMNIDATA", .path = "/%s"}, -+ {.var = "XDG_DATA_HOME", .path = "/gmni/%s"}, -+ {.var = "HOME", .path = "/.local/share/gmni/%s"} -+ }; -+ return getpath(paths, sizeof(paths) / sizeof(paths[0])); -+} -+ -+static char * -+trim_ws(char *in) -+{ -+ while (*in && isspace(*in)) ++in; -+ return in; -+} -+ -+static void -+save_bookmark(struct browser *browser) -+{ -+ const char *path_fmt = get_data_pathfmt(); -+ static char path[PATH_MAX+1]; -+ snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ mkdirs(path, 0755); -+ -+ FILE *f = fopen(path, "a"); -+ if (!f) { -+ fprintf(stderr, "Error opening %s for writing: %s\n", -+ path, strerror(errno)); -+ return; -+ } -+ -+ char *title = browser->page_title; -+ if (title) { -+ title = trim_ws(browser->page_title); -+ } -+ -+ fprintf(f, "=> %s%s%s\n", browser->plain_url, -+ title ? " " : "", title ? title : ""); -+ fclose(f); -+ -+ fprintf(browser->tty, "Bookmark saved: %s\n", -+ title ? title : browser->plain_url); -+} -+ -+static void -+open_bookmarks(struct browser *browser) -+{ -+ const char *path_fmt = get_data_pathfmt(); -+ static char path[PATH_MAX+1]; -+ snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ static char url[PATH_MAX+1+7]; -+ snprintf(url, sizeof(url), "file://%s", path); -+ set_url(browser, url, &browser->history); -+} -+ - static enum prompt_result - do_prompts(const char *prompt, struct browser *browser) - { -@@ -134,6 +195,16 @@ browser->history = browser->history->prev; - set_url(browser, browser->history->url, NULL); - result = PROMPT_ANSWERED; - goto exit; -+ case 'm': -+ if (in[1]) break; -+ save_bookmark(browser); -+ result = PROMPT_AGAIN; -+ goto exit; -+ case 'M': -+ if (in[1]) break; -+ open_bookmarks(browser); -+ result = PROMPT_ANSWERED; -+ goto exit; - case 'f': - if (in[1]) break; - if (!browser->history->next) { -@@ -205,13 +276,6 @@ free(in); - return result; - } - --static char * --trim_ws(char *in) --{ -- while (*in && isspace(*in)) ++in; -- return in; --} -- - static int - wrap(FILE *f, char *s, struct winsize *ws, int *row, int *col) - { -@@ -258,6 +322,8 @@ { - int nlinks = 0; - struct gemini_parser p; - gemini_parser_init(&p, resp->bio); -+ free(browser->page_title); -+ browser->page_title = NULL; - - struct winsize ws; - ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); -@@ -302,6 +368,9 @@ text = tok.text; - } - break; - case GEMINI_HEADING: -+ if (!browser->page_title) { -+ browser->page_title = strdup(tok.heading.title); -+ } - if (text == NULL) { - for (int n = tok.heading.level; n; --n) { - col += fprintf(out, "#"); -diff --git a/src/util.c b/src/util.c -new file mode 100644 -index 0000000000000000000000000000000000000000..26e7283a26cc8e593bb3e3fd53d4b7a9bc646e3f ---- /dev/null -+++ b/src/util.c -@@ -0,0 +1,62 @@ -+#include <assert.h> -+#include <errno.h> -+#include <libgen.h> -+#include <limits.h> -+#include <stdlib.h> -+#include <string.h> -+#include <sys/stat.h> -+#include "util.h" -+ -+static void -+posix_dirname(char *path, char *dname) -+{ -+ char p[PATH_MAX+1]; -+ char *t; -+ -+ assert(strlen(path) <= PATH_MAX); -+ -+ strcpy(p, path); -+ t = dirname(path); -+ memmove(dname, t, strlen(t) + 1); -+ -+ /* restore the path if dirname worked in-place */ -+ if (t == path && path != dname) { -+ strcpy(path, p); -+ } -+} -+ -+/** Make directory and all of its parents */ -+int -+mkdirs(char *path, mode_t mode) -+{ -+ char dname[PATH_MAX + 1]; -+ posix_dirname(path, dname); -+ if (strcmp(dname, "/") == 0) { -+ return 0; -+ } -+ if (mkdirs(dname, mode) != 0) { -+ return -1; -+ } -+ if (mkdir(path, mode) != 0 && errno != EEXIST) { -+ return -1; -+ } -+ errno = 0; -+ return 0; -+} -+ -+char * -+getpath(const struct pathspec *paths, size_t npaths) { -+ for (size_t i = 0; i < npaths; i++) { -+ const char *var = ""; -+ if (paths[i].var) { -+ var = getenv(paths[i].var); -+ } -+ if (var) { -+ char *out = calloc(1, -+ strlen(var) + strlen(paths[i].path) + 1); -+ strcat(strcat(out, var), paths[i].path); -+ return out; -+ } -+ } -+ return NULL; -+} diff --git a/sources/cgmnlm.git/commits/60496bae0cbda1162ae00bc6f6f4047ba9c7d86f.patch b/sources/cgmnlm.git/commits/60496bae0cbda1162ae00bc6f6f4047ba9c7d86f.patch @@ -1,22 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 07b699134c0c71a20d4780148ad74cb246634549..da99a84580894312ba86b92e6abd02cfdfc80fd4 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -457,7 +457,6 @@ /* fallthrough */ - case GEMINI_PREFORMATTED_END: - continue; // Not used - case GEMINI_PREFORMATTED_TEXT: -- col += fprintf(out, "` "); - if (text == NULL) { - text = tok.preformatted; - } -@@ -494,7 +493,8 @@ col += fprintf(out, " "); - } - break; - case GEMINI_QUOTE: -- col += fprintf(out, "> "); -+ col += fprintf(out, "%s ", -+ browser->unicode ? "┃" : ">"); - if (text == NULL) { - text = trim_ws(tok.quote_text); - } diff --git a/sources/cgmnlm.git/commits/60cf41e7dd66897e6987704921b6cd57da1f084f.patch b/sources/cgmnlm.git/commits/60cf41e7dd66897e6987704921b6cd57da1f084f.patch @@ -1,62 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 41284df2235e869f49e542f5e1f2dcc240deb684..756520c728baad52206866a3af64b0ffebbac3cd 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -59,6 +59,7 @@ "q\tQuit\n" - "N\tFollow Nth link (where N is a number)\n" - "b\tBack (in the page history)\n" - "f\tForward (in the page history)\n" -+ "H\tView all page history\n" - "m\tSave bookmark\n" - "M\tBrowse bookmarks\n" - "\n" -@@ -206,16 +207,6 @@ browser->history = browser->history->prev; - set_url(browser, browser->history->url, NULL); - result = PROMPT_ANSWERED; - goto exit; -- case 'm': -- if (in[1]) break; -- save_bookmark(browser); -- result = PROMPT_AGAIN; -- goto exit; -- case 'M': -- if (in[1]) break; -- open_bookmarks(browser); -- result = PROMPT_ANSWERED; -- goto exit; - case 'f': - if (in[1]) break; - if (!browser->history->next) { -@@ -225,6 +216,32 @@ goto exit; - } - browser->history = browser->history->next; - set_url(browser, browser->history->url, NULL); -+ result = PROMPT_ANSWERED; -+ goto exit; -+ case 'H': -+ if (in[1]) break; -+ struct history *cur = browser->history; -+ while (cur->prev) cur = cur->prev; -+ while (cur != browser->history) { -+ fprintf(browser->tty, " %s\n", cur->url); -+ cur = cur->next; -+ } -+ fprintf(browser->tty, "* %s\n", cur->url); -+ cur = cur->next; -+ while (cur) { -+ fprintf(browser->tty, " %s\n", cur->url); -+ cur = cur->next; -+ } -+ result = PROMPT_AGAIN; -+ goto exit; -+ case 'm': -+ if (in[1]) break; -+ save_bookmark(browser); -+ result = PROMPT_AGAIN; -+ goto exit; -+ case 'M': -+ if (in[1]) break; -+ open_bookmarks(browser); - result = PROMPT_ANSWERED; - goto exit; - case '/': diff --git a/sources/cgmnlm.git/commits/61af57e302efd90458e17fa9f0bfaf5b3828954f.patch b/sources/cgmnlm.git/commits/61af57e302efd90458e17fa9f0bfaf5b3828954f.patch @@ -1,13 +0,0 @@ -diff --git a/src/gmni.c b/src/gmni.c -index 4f563ed19e0edc52086f55e4057a91f0802a1f93..1c3e5f2bebddf5b606d11acd8924562cabde9a6f 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -287,7 +287,7 @@ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: - if (header_mode == OMIT_HEADERS) { - fprintf(stderr, "%s: %d %s\n", - resp.status / 10 == 4 ? -- "TEMPORARY FAILURE" : "PERMANENT FALIURE", -+ "TEMPORARY FAILURE" : "PERMANENT FAILURE", - resp.status, resp.meta); - } - exit = true; diff --git a/sources/cgmnlm.git/commits/678bff58ed32e77c9af90a5d8fc7b1f3c38af86c.patch b/sources/cgmnlm.git/commits/678bff58ed32e77c9af90a5d8fc7b1f3c38af86c.patch @@ -1,31 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 81923101e320b8517405ef2ed7efdbc626241201..6d77264cabb0db317ddf0807953d327f2379d9be 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -168,7 +168,7 @@ col += fprintf(browser->tty, " %s\n", - trim_ws(tok.text)); - break; - case GEMINI_LINK: -- col += fprintf(browser->tty, "[%d] %s\n", nlinks++, -+ col += fprintf(browser->tty, "%d) %s\n", nlinks++, - trim_ws(tok.link.text ? tok.link.text : tok.link.url)); - *next = calloc(1, sizeof(struct link)); - (*next)->url = strdup(trim_ws(tok.link.url)); -@@ -205,7 +205,7 @@ - if (browser->pagination && row >= ws.ws_row - 4) { - char prompt[4096]; - snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -- "[Enter]: read more; [n]: follow Nth link; [b]ack; [f]orward; [q]uit\n" -+ "[Enter]: read more; [N]: follow Nth link; [b]ack; [f]orward; [q]uit\n" - "(more) => ", resp->meta, browser->plain_url); - enum prompt_result result = PROMPT_AGAIN; - while (result == PROMPT_AGAIN) { -@@ -412,7 +412,7 @@ goto next; - } - - snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -- "[n]: follow Nth link; [b]ack; [f]orward; [q]uit\n" -+ "[N]: follow Nth link; [b]ack; [f]orward; [q]uit\n" - "=> ", - resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", - browser.plain_url); diff --git a/sources/cgmnlm.git/commits/689fb8b470f19fb83ee1e32efe64b42d6961630c.patch b/sources/cgmnlm.git/commits/689fb8b470f19fb83ee1e32efe64b42d6961630c.patch @@ -1,28 +0,0 @@ -diff --git a/src/client.c b/src/client.c -index bb508e04c8d41fe12aa99ae42b8fa30d4f3a9a2e..18b5115371bc6b13b9b796a85f7ad4f826eef973 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -236,11 +236,6 @@ if (!resp) { - return; - } - -- if (resp->fd != -1) { -- close(resp->fd); -- resp->fd = -1; -- } -- - if (resp->bio) { - BIO_free_all(resp->bio); - resp->bio = NULL; -@@ -253,6 +248,11 @@ if (resp->ssl_ctx) { - SSL_CTX_free(resp->ssl_ctx); - } - free(resp->meta); -+ -+ if (resp->fd != -1) { -+ close(resp->fd); -+ resp->fd = -1; -+ } - - resp->ssl = NULL; - resp->ssl_ctx = NULL; diff --git a/sources/cgmnlm.git/commits/6d2f78eeded101ccd755b1b2be16105fe5af881d.patch b/sources/cgmnlm.git/commits/6d2f78eeded101ccd755b1b2be16105fe5af881d.patch @@ -1,16 +0,0 @@ -diff --git a/configure b/configure -index aa761971fe95d70ed242977bed8dfd6d55a9f794..70a19b193d6e33b41b5cc7ef13511bb0f5b4e076 100755 ---- a/configure -+++ b/configure -@@ -16,7 +16,7 @@ cgmnlm() { - genrules cgmnlm \ - src/client.c \ - src/escape.c \ -- src/cgmnlm.c \ -+ src/gmnlm.c \ - src/parser.c \ - src/tofu.c \ - src/url.c \ -diff --git a/src/cgmnlm.c b/src/gmnlm.c -rename from src/cgmnlm.c -rename to src/gmnlm.c diff --git a/sources/cgmnlm.git/commits/6f36d2a0fc5de0a9d25229c47c75481f47f32c87.patch b/sources/cgmnlm.git/commits/6f36d2a0fc5de0a9d25229c47c75481f47f32c87.patch @@ -1,62 +0,0 @@ -diff --git a/Makefile b/Makefile -index 3d4df602cd7f7ab4f5a45b47dee0d47729f0739c..12ff4b45e4b043ae5c93629b502e42e8396b2b24 100644 ---- a/Makefile -+++ b/Makefile -@@ -13,6 +13,7 @@ @printf 'CCLD\t$@\n' - @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmnlm_objects) - - doc/gmni.1: doc/gmni.scd -+doc/gmnlm.1: doc/gmnlm.scd - - .SUFFIXES: .c .o .scd .1 - -@@ -27,10 +28,10 @@ .scd.1: - @printf 'SCDOC\t$@\n' - @$(SCDOC) < $< > $@ - --docs: doc/gmni.1 -+docs: doc/gmni.1 doc/gmnlm.1 - - clean: -- @rm -f gmni doc/gmni.1 -+ @rm -f gmni doc/gmni.1 doc/gmnlm.1 - - distclean: clean - @rm -rf "$(OUTDIR)" -@@ -39,6 +40,8 @@ install: all - mkdir -p $(BINDIR) - mkdir -p $(MANDIR)/man1 - install -Dm755 gmni $(BINDIR)/gmni -+ install -Dm755 gmnlm $(BINDIR)/gmnlm - install -Dm644 doc/gmni.1 $(MANDIR)/man1/gmni.1 -+ install -Dm644 doc/gmnlm.1 $(MANDIR)/man1/gmnlm.1 - - .PHONY: clean distclean docs install -diff --git a/doc/gmnlm.scd b/doc/gmnlm.scd -new file mode 100644 -index 0000000000000000000000000000000000000000..c5e7bf7f6b189f984e01ea2a942f47acb993f7e7 ---- /dev/null -+++ b/doc/gmnlm.scd -@@ -0,0 +1,22 @@ -+gmnlm(1) -+ -+# NAME -+ -+gmnlm - Gemini line-mode browser -+ -+# SYNPOSIS -+ -+*gmnlm* [-PU] _gemini://..._ -+ -+# DESCRIPTION -+ -+*gmnlm* is an interactive line-mode Gemini browser. -+ -+# OPTIONS -+ -+*-P* -+ Disable pagination. -+ -+*-U* -+ Disable conservative use of Unicode symbols to render Gemini layout -+ features. diff --git a/sources/cgmnlm.git/commits/71ececc4f264eed36f022b4b52c9100b9e7b1b12.patch b/sources/cgmnlm.git/commits/71ececc4f264eed36f022b4b52c9100b9e7b1b12.patch @@ -1,21 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index cb7e2843798f66f596e071823d9e41153a555fcc..c70a4126acc5337a768c3a70513aecc1b0e16668 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -89,6 +89,7 @@ "B\tBrowse bookmarks\n" - "r\tReload the page\n" - "d <path>\tDownload page to <path>\n" - "|<prog>\tPipe page into program\n" -+ "[N]|<prog>\tPipe content of Nth link into program\n" - "\n" - "Other commands include:\n\n" - "<Enter>\tread more lines\n" -@@ -791,7 +792,7 @@ } - break; - case GEMINI_LINK: - if (text == NULL) { -- col += fprintf(out, "%2d> %s", nlinks++, ANSI_COLOR_CYAN); -+ col += fprintf(out, "%2d) %s", nlinks++, ANSI_COLOR_CYAN); - text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); - *next = calloc(1, sizeof(struct link)); - (*next)->url = strdup(trim_ws(tok.link.url)); diff --git a/sources/cgmnlm.git/commits/73f5a5bc2b97fb36ded8feb36828d598d6e9fed3.patch b/sources/cgmnlm.git/commits/73f5a5bc2b97fb36ded8feb36828d598d6e9fed3.patch @@ -1,39 +0,0 @@ -diff --git a/include/client.h b/include/client.h -index 40729073bcaa2c0a172d3375662ac91cef0ea550..671de12e7ee53fbbfc03407760723d2a66458c2d 100644 ---- a/include/client.h -+++ b/include/client.h -@@ -39,11 +39,8 @@ enum gemini_result { - GEMINI_OK, - GEMINI_ERR_OOM, - GEMINI_ERR_INVALID_URL, -- // status is set to the return value from getaddrinfo - GEMINI_ERR_RESOLVE, -- // status is set to errno - GEMINI_ERR_CONNECT, -- // use SSL_get_error(resp->ssl, resp->status) to get details - GEMINI_ERR_SSL, - GEMINI_ERR_IO, - GEMINI_ERR_PROTOCOL, -@@ -52,17 +49,16 @@ - // Requests the specified URL via the gemini protocol. If options is non-NULL, - // it may specify some additional configuration to adjust client behavior. - // --// Returns a value indicating the success of the request. If GEMINI_OK is --// returned, the response details shall be written to the gemini_response --// argument. -+// Returns a value indicating the success of the request. -+// -+// Caller must call gemini_response_finish afterwards to clean up resources -+// before exiting or re-using it for another request. - enum gemini_result gemini_request(const char *url, - struct gemini_options *options, - struct gemini_response *resp); - - // Must be called after gemini_request in order to free up the resources --// allocated during the request. If you intend to re-use the SSL_CTX provided by --// gemini_options, set the ctx pointer to NULL before calling --// gemini_response_finish. -+// allocated during the request. - void gemini_response_finish(struct gemini_response *resp); - - // Returns a user-friendly string describing an error. diff --git a/sources/cgmnlm.git/commits/75087ce65f54c86d44ce13bb63e1041226a53f7b.patch b/sources/cgmnlm.git/commits/75087ce65f54c86d44ce13bb63e1041226a53f7b.patch @@ -1,20 +0,0 @@ -diff --git a/config.sh b/config.sh -index a6963622d4b41d6e25bb4fe230c2513849c077d3..55bc9a2ab6b15f91c655a4ab033fcbd6107b83fe 100644 ---- a/config.sh -+++ b/config.sh -@@ -144,11 +144,11 @@ CFLAGS+=-DLIBDIR='"\$(LIBDIR)"' - - all: ${all} - EOF -- -- for target in $all -+ -+ for target in $(printf '%s\n' $all | tr '.' '_') - do -- ${target//./_} >>"$outdir"/config.mk -- done -+ $target -+ done >>"$outdir"/config.mk - echo done - - touch $outdir/cppcache diff --git a/sources/cgmnlm.git/commits/7619edcd116385414b55764a3401a0c66c04da79.patch b/sources/cgmnlm.git/commits/7619edcd116385414b55764a3401a0c66c04da79.patch @@ -1,13 +0,0 @@ -diff --git a/src/parser.c b/src/parser.c -index 04501b6b644af28abe7843076ae593b7165912b2..579415150f842557b6faf4097a63aac9324928fb 100644 ---- a/src/parser.c -+++ b/src/parser.c -@@ -46,7 +46,7 @@ ssize_t n = BIO_read(p->f, &p->buf[p->bufln], p->bufsz - p->bufln - 1); - if (n == -1) { - return -1; - } else if (n == 0) { -- eof = 1; -+ eof = p->bufln == 0; - break; - } - p->bufln += n; diff --git a/sources/cgmnlm.git/commits/77de1bb2a84e0980d23b7fc2dda1480a1093ca21.patch b/sources/cgmnlm.git/commits/77de1bb2a84e0980d23b7fc2dda1480a1093ca21.patch @@ -1,22 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 07d0000babe7cd51876ead407e83c3062adfda56..07b699134c0c71a20d4780148ad74cb246634549 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -69,7 +69,7 @@ const char *help_msg = - "The following commands are available:\n\n" - "q\tQuit\n" - "N\tFollow Nth link (where N is a number)\n" -- "p N\tShow URL of Nth link (where N is a number)\n" -+ "p[N]\tShow URL of Nth link (where N is a number)\n" - "b\tBack (in the page history)\n" - "f\tForward (in the page history)\n" - "H\tView all page history\n" -@@ -303,7 +303,7 @@ result = PROMPT_AGAIN; - goto exit; - } - case 'p': -- if (!isspace(in[1])) break; -+ if (!in[1]) break; - struct link *link = browser->links; - char *endptr; - int linksel = (int)strtol(in+1, &endptr, 10); diff --git a/sources/cgmnlm.git/commits/78cfe1b669fad6b7a3638d371ed9825e2ee53243.patch b/sources/cgmnlm.git/commits/78cfe1b669fad6b7a3638d371ed9825e2ee53243.patch @@ -1,53 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index ef4c97950495e369e4fd3b1256c185fdb01f8cf1..8cafaea17de972c240031b0fe6ab88e3621bd871 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -179,8 +179,7 @@ - n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - free(path_fmt); - assert(n < sizeof(path)); -- strncpy(dname, path, sizeof(dname)-1); -- dirname(dname); -+ posix_dirname(path, dname); - if (mkdirs(dname, 0755) != 0) { - fprintf(stderr, "Error creating directory %s: %s\n", - dname, strerror(errno)); -@@ -262,8 +261,7 @@ n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - free(path_fmt); - - assert(n < sizeof(path)); -- strncpy(dname, path, sizeof(dname)-1); -- dirname(dname); -+ posix_dirname(path, dname); - if (mkdirs(dname, 0755) != 0) { - fprintf(stderr, "Error creating directory %s: %s\n", - dname, strerror(errno)); -diff --git a/src/tofu.c b/src/tofu.c -index 54183a79278de45bfbe5a1f8ddbcbf10be1c01c4..0923aed3f3d9ac02a7bc6b81db965d6924e4af74 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -177,20 +177,15 @@ - n = snprintf(tofu->known_hosts_path, - sizeof(tofu->known_hosts_path), - path_fmt, "known_hosts"); -+ free(path_fmt); - assert(n < sizeof(tofu->known_hosts_path)); - -- strncpy(dname, dirname(tofu->known_hosts_path), sizeof(dname)-1); -+ posix_dirname(tofu->known_hosts_path, dname); - if (mkdirs(dname, 0755) != 0) { -- snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), -- path_fmt, "known_hosts"); -- fprintf(stderr, "Error creating directory %s: %s\n", -- dirname(tofu->known_hosts_path), strerror(errno)); -+ fprintf(stderr, "Error creating directory %s: %s\n", dname, -+ strerror(errno)); - return; - } -- -- snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), -- path_fmt, "known_hosts"); -- free(path_fmt); - - tofu->callback = cb; - tofu->cb_data = cb_data; diff --git a/sources/cgmnlm.git/commits/78eb57cad45aa27e83d3a5e78be0bb5ce4d631f7.patch b/sources/cgmnlm.git/commits/78eb57cad45aa27e83d3a5e78be0bb5ce4d631f7.patch @@ -1,281 +0,0 @@ -diff --git a/include/client.h b/include/client.h -index f711eea91c3842ac29a884b38e0d7a0ab99cb7fb..40729073bcaa2c0a172d3375662ac91cef0ea550 100644 ---- a/include/client.h -+++ b/include/client.h -@@ -68,4 +68,8 @@ - // Returns a user-friendly string describing an error. - const char *gemini_strerr(enum gemini_result r, struct gemini_response *resp); - -+// Returns the given URL with the input response set to the specified value. -+// The caller must free the string. -+char *gemini_input_url(const char *url, const char *input); -+ - #endif -diff --git a/src/client.c b/src/client.c -index 67671ccca2a25a460681bdffacb981c7f26c1407..c252c9d9ab1783dd462343ce346cc74611e9ae85 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -176,7 +176,7 @@ } - - char *endptr; - resp->status = (int)strtol(buf, &endptr, 10); -- if (*endptr != ' ' || resp->status <= 10 || resp->status >= 70) { -+ if (*endptr != ' ' || resp->status < 10 || resp->status >= 70) { - res = GEMINI_ERR_PROTOCOL; - goto cleanup; - } -@@ -195,14 +195,25 @@ { - if (!resp) { - return; - } -+ - if (resp->fd != -1) { - close(resp->fd); -+ resp->fd = -1; - } -- BIO_free(BIO_pop(resp->bio)); // ssl bio -- BIO_free(resp->bio); // buffered bio -+ -+ if (resp->bio) { -+ BIO_free(BIO_pop(resp->bio)); // ssl bio -+ BIO_free(resp->bio); // buffered bio -+ resp->bio = NULL; -+ } -+ - SSL_free(resp->ssl); - SSL_CTX_free(resp->ssl_ctx); - free(resp->meta); -+ -+ resp->ssl = NULL; -+ resp->ssl_ctx = NULL; -+ resp->meta = NULL; - } - - const char * -@@ -230,3 +241,26 @@ return "Protocol error"; - } - assert(0); - } -+ -+char * -+gemini_input_url(const char *url, const char *input) -+{ -+ char *new_url = NULL; -+ struct Curl_URL *uri = curl_url(); -+ if (!uri) { -+ return NULL; -+ } -+ if (curl_url_set(uri, CURLUPART_URL, url, 0) != CURLUE_OK) { -+ goto cleanup; -+ } -+ if (curl_url_set(uri, CURLUPART_QUERY, input, CURLU_URLENCODE) != CURLUE_OK) { -+ goto cleanup; -+ } -+ if (curl_url_get(uri, CURLUPART_URL, &new_url, 0) != CURLUE_OK) { -+ new_url = NULL; -+ goto cleanup; -+ } -+cleanup: -+ curl_url_cleanup(uri); -+ return new_url; -+} -diff --git a/src/gmnic.c b/src/gmnic.c -index 014211dc3784ba3eadcf1885ef7bcf6fe84d8462..794b94ba2235757864acb81194b8de88220cc6da 100644 ---- a/src/gmnic.c -+++ b/src/gmnic.c -@@ -26,10 +26,17 @@ OMIT_HEADERS, - SHOW_HEADERS, - ONLY_HEADERS, - }; -- enum header_mode headers = OMIT_HEADERS; -+ enum header_mode header_mode = OMIT_HEADERS; -+ -+ enum input_mode { -+ INPUT_READ, -+ INPUT_SUPPRESS, -+ }; -+ enum input_mode input_mode = INPUT_READ; -+ FILE *input_source = stdin; - - int c; -- while ((c = getopt(argc, argv, "46C:d:hLiI")) != -1) { -+ while ((c = getopt(argc, argv, "46C:d:D:hLiIN")) != -1) { - switch (c) { - case '4': - assert(0); // TODO -@@ -41,7 +48,21 @@ case 'C': - assert(0); // TODO: Client certificates - break; - case 'd': -- assert(0); // TODO: Input -+ input_mode = INPUT_READ; -+ input_source = fmemopen(optarg, strlen(optarg), "r"); -+ break; -+ case 'D': -+ input_mode = INPUT_READ; -+ if (strcmp(optarg, "-") == 0) { -+ input_source = stdin; -+ } else { -+ input_source = fopen(optarg, "r"); -+ if (!input_source) { -+ fprintf(stderr, "Error: open %s: %s", -+ optarg, strerror(errno)); -+ return 1; -+ } -+ } - break; - case 'h': - usage(argv[0]); -@@ -50,10 +71,14 @@ case 'L': - assert(0); // TODO: Follow redirects - break; - case 'i': -- headers = SHOW_HEADERS; -+ header_mode = SHOW_HEADERS; - break; - case 'I': -- headers = ONLY_HEADERS; -+ header_mode = ONLY_HEADERS; -+ input_mode = INPUT_SUPPRESS; -+ break; -+ case 'N': -+ input_mode = INPUT_SUPPRESS; - break; - default: - fprintf(stderr, "fatal: unknown flag %c", c); -@@ -69,43 +94,105 @@ - SSL_load_error_strings(); - ERR_load_crypto_strings(); - -- struct gemini_response resp; -- enum gemini_result r = gemini_request(argv[optind], NULL, &resp); -- if (r != GEMINI_OK) { -- fprintf(stderr, "Error: %s\n", gemini_strerr(r, &resp)); -- gemini_response_finish(&resp); -- return (int)r; -- } -+ bool exit = false; -+ char *url = strdup(argv[optind]); -+ -+ int ret = 0; -+ while (!exit) { -+ struct gemini_response resp; -+ enum gemini_result r = gemini_request(url, NULL, &resp); -+ if (r != GEMINI_OK) { -+ fprintf(stderr, "Error: %s\n", gemini_strerr(r, &resp)); -+ ret = (int)r; -+ exit = true; -+ goto next; -+ } -+ -+ char *new_url, *input = NULL; -+ switch (resp.status / 10) { -+ case 1: // INPUT -+ if (input_mode == INPUT_SUPPRESS) { -+ exit = true; -+ break; -+ } -+ -+ if (fileno(input_source) != -1 && -+ isatty(fileno(input_source))) { -+ fprintf(stderr, "%s: ", resp.meta); -+ } - -- switch (headers) { -- case ONLY_HEADERS: -- printf("%d %s\n", resp.status, resp.meta); -- break; -- case SHOW_HEADERS: -- printf("%d %s\n", resp.status, resp.meta); -- /* fallthrough */ -- case OMIT_HEADERS: -- for (int n = 1; n > 0;) { -- char buf[BUFSIZ]; -- n = BIO_read(resp.bio, buf, BUFSIZ); -+ size_t s = 0; -+ ssize_t n = getline(&input, &s, input_source); - if (n == -1) { -- fprintf(stderr, "Error: read\n"); -- return 1; -+ fprintf(stderr, "Error reading input: %s\n", -+ feof(input_source) ? "EOF" : -+ strerror(ferror(input_source))); -+ r = 1; -+ exit = true; -+ break; -+ } -+ input[n - 1] = '\0'; // Drop LF -+ -+ new_url = gemini_input_url(url, input); -+ free(url); -+ url = new_url; -+ goto next; -+ case 3: // REDIRECT -+ assert(0); // TODO -+ case 6: // CLIENT CERTIFICATE REQUIRED -+ assert(0); // TODO -+ case 4: // TEMPORARY FAILURE -+ case 5: // PERMANENT FAILURE -+ if (header_mode == OMIT_HEADERS) { -+ fprintf(stderr, "%s: %d %s\n", -+ resp.status / 10 == 4 ? -+ "TEMPORARY FAILURE" : "PERMANENT FALIURE", -+ resp.status, resp.meta); - } -- ssize_t w = 0; -- while (w < (ssize_t)n) { -- ssize_t x = write(STDOUT_FILENO, &buf[w], n - w); -- if (x == -1) { -- fprintf(stderr, "Error: write: %s\n", -- strerror(errno)); -+ exit = true; -+ break; -+ case 2: // SUCCESS -+ exit = true; -+ break; -+ } -+ -+ switch (header_mode) { -+ case ONLY_HEADERS: -+ printf("%d %s\n", resp.status, resp.meta); -+ break; -+ case SHOW_HEADERS: -+ printf("%d %s\n", resp.status, resp.meta); -+ /* fallthrough */ -+ case OMIT_HEADERS: -+ if (resp.status / 10 != 2) { -+ break; -+ } -+ for (int n = 1; n > 0;) { -+ char buf[BUFSIZ]; -+ n = BIO_read(resp.bio, buf, BUFSIZ); -+ if (n == -1) { -+ fprintf(stderr, "Error: read\n"); - return 1; - } -- w += x; -+ ssize_t w = 0; -+ while (w < (ssize_t)n) { -+ ssize_t x = write(STDOUT_FILENO, &buf[w], n - w); -+ if (x == -1) { -+ fprintf(stderr, "Error: write: %s\n", -+ strerror(errno)); -+ return 1; -+ } -+ w += x; -+ } - } -+ break; - } -- break; -+ -+next: -+ gemini_response_finish(&resp); - } - -- gemini_response_finish(&resp); -- return 0; -+ (void)input_mode; -+ free(url); -+ return ret; - } diff --git a/sources/cgmnlm.git/commits/7a099135cd9dae483679cf51a4b630a5dd64c74e.patch b/sources/cgmnlm.git/commits/7a099135cd9dae483679cf51a4b630a5dd64c74e.patch @@ -1,115 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index bc3f647d42372678b4180f539df8637d0ba69a12..dd8c8d200a949a24d030ce3429a4d20f1430fe97 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -63,9 +63,11 @@ // TODO: word wrap - col += fprintf(tty, " %s\n", trim_ws(tok.text)); - break; - case GEMINI_LINK: -- (void)next; // TODO: Record links - col += fprintf(tty, "[%d] %s\n", nlinks++, trim_ws( - tok.link.text ? tok.link.text : tok.link.url)); -+ *next = calloc(1, sizeof(struct link)); -+ (*next)->url = strdup(trim_ws(tok.link.url)); -+ next = &(*next)->next; - break; - case GEMINI_PREFORMATTED: - continue; // TODO -@@ -117,7 +119,6 @@ main(int argc, char *argv[]) - { - bool pagination = true; - -- bool have_url = false; - struct Curl_URL *url = curl_url(); - - FILE *tty = fopen("/dev/tty", "w+"); -@@ -139,8 +140,7 @@ } - - if (optind == argc - 1) { - set_url(url, argv[optind]); -- have_url = true; -- } else if (optind < argc - 1) { -+ } else { - usage(argv[0]); - return 1; - } -@@ -154,8 +154,6 @@ - bool run = true; - struct gemini_response resp; - while (run) { -- assert(have_url); // TODO -- - struct link *links; - static char prompt[4096]; - -@@ -164,7 +162,8 @@ CURLUcode uc = curl_url_get(url, CURLUPART_URL, &plain_url, 0); - assert(uc == CURLUE_OK); // Invariant - - snprintf(prompt, sizeof(prompt), "\n\t%s\n" -- "\tWhere to? [n]: follow Nth link; [o <url>]: open URL; [q]: quit\n" -+ "\tWhere to? [n]: follow Nth link; [o <url>]: open URL; [q]: quit " -+ "[b]ack; [f]orward\n" - "=> ", plain_url); - - enum gemini_result res = gemini_request(plain_url, &opts, &resp); -@@ -194,19 +193,49 @@ } - - gemini_response_finish(&resp); - -- fprintf(tty, "%s", prompt); -- size_t l = 0; -- char *in = NULL; -- ssize_t n = getline(&in, &l, tty); -- if (n == -1 && feof(tty)) { -- break; -- } -+ bool prompting = true; -+ while (prompting) { -+ fprintf(tty, "%s", prompt); -+ -+ size_t l = 0; -+ char *in = NULL; -+ ssize_t n = getline(&in, &l, tty); -+ if (n == -1 && feof(tty)) { -+ prompting = run = false; -+ break; -+ } -+ if (strcmp(in, "q\n") == 0) { -+ prompting = run = false; -+ break; -+ } -+ -+ struct link *link = links; -+ char *endptr; -+ int linksel = (int)strtol(in, &endptr, 10); -+ if (endptr[0] == '\n' && linksel >= 0) { -+ while (linksel > 0 && link) { -+ link = link->next; -+ --linksel; -+ } - -- if (strcmp(in, "q\n") == 0) { -- run = false; -- } -+ if (!link) { -+ fprintf(stderr, "Error: no such link.\n"); -+ } else { -+ prompting = false; -+ set_url(url, link->url); -+ } -+ } - -- free(in); -+ link = links; -+ while (link) { -+ struct link *next = link->next; -+ free(link->url); -+ free(link); -+ link = next; -+ } -+ -+ free(in); -+ } - } - - SSL_CTX_free(opts.ssl_ctx); diff --git a/sources/cgmnlm.git/commits/7c453fb45f831ce9178799af9855ecb0bda518ea.patch b/sources/cgmnlm.git/commits/7c453fb45f831ce9178799af9855ecb0bda518ea.patch @@ -1,116 +0,0 @@ -diff --git a/src/client.c b/src/client.c -index 86152d6183462636cca4eecbd47824569a688cb1..f1674d534a9a5f2edcb6c7be678e17ad5724a9a7 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -175,7 +175,7 @@ goto cleanup; - } - - char *endptr; -- resp->status = (int)strtol(buf, &endptr, 10); -+ resp->status = (enum gemini_status)strtol(buf, &endptr, 10); - if (*endptr != ' ' || resp->status < 10 || resp->status >= 70) { - res = GEMINI_ERR_PROTOCOL; - goto cleanup; -@@ -268,5 +268,5 @@ - enum gemini_status_class - gemini_response_class(enum gemini_status status) - { -- return status / 10; -+ return status / 10 * 10; - } -diff --git a/src/gmni.c b/src/gmni.c -index b4efdc0235dac0279ce284f2c6b19f680a191647..5b1f37522e93da360e74ba92ccb00c530bd41463 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -10,6 +10,7 @@ #include <stdlib.h> - #include <string.h> - #include <sys/socket.h> - #include <sys/types.h> -+#include <termios.h> - #include <unistd.h> - #include "gmni.h" - -@@ -19,6 +20,41 @@ { - fprintf(stderr, - "usage: %s [-46lLiIN] [-E cert] [-d input] [-D path] gemini://...\n", - argv_0); -+} -+ -+static char * -+get_input(const struct gemini_response *resp, FILE *source) -+{ -+ int r = 0; -+ struct termios attrs; -+ bool tty = fileno(source) != -1 && isatty(fileno(source)); -+ char *input = NULL; -+ if (tty) { -+ fprintf(stderr, "%s: ", resp->meta); -+ if (resp->status == GEMINI_STATUS_SENSITIVE_INPUT) { -+ r = tcgetattr(fileno(source), &attrs); -+ struct termios new_attrs; -+ r = tcgetattr(fileno(source), &new_attrs); -+ if (r != -1) { -+ new_attrs.c_lflag &= ~ECHO; -+ tcsetattr(fileno(source), TCSANOW, &new_attrs); -+ } -+ } -+ } -+ size_t s = 0; -+ ssize_t n = getline(&input, &s, source); -+ if (n == -1) { -+ fprintf(stderr, "Error reading input: %s\n", -+ feof(source) ? "EOF" : -+ strerror(ferror(source))); -+ return NULL; -+ } -+ input[n - 1] = '\0'; // Drop LF -+ if (tty && resp->status == GEMINI_STATUS_SENSITIVE_INPUT && r != -1) { -+ attrs.c_lflag &= ~ECHO; -+ tcsetattr(fileno(source), TCSANOW, &attrs); -+ } -+ return input; - } - - int -@@ -119,7 +155,6 @@ exit = true; - goto next; - } - -- char *new_url, *input = NULL; - switch (gemini_response_class(resp.status)) { - case GEMINI_STATUS_CLASS_INPUT: - if (input_mode == INPUT_SUPPRESS) { -@@ -127,27 +162,19 @@ exit = true; - break; - } - -- if (fileno(input_source) != -1 && -- isatty(fileno(input_source))) { -- fprintf(stderr, "%s: ", resp.meta); -- } -- -- size_t s = 0; -- ssize_t n = getline(&input, &s, input_source); -- if (n == -1) { -- fprintf(stderr, "Error reading input: %s\n", -- feof(input_source) ? "EOF" : -- strerror(ferror(input_source))); -+ char *input = get_input(&resp, input_source); -+ if (!input) { - r = 1; - exit = true; - break; - } -- input[n - 1] = '\0'; // Drop LF - -- new_url = gemini_input_url(url, input); -+ char *new_url = gemini_input_url(url, input); -+ assert(new_url); -+ -+ free(input); - free(url); - url = new_url; -- assert(url); - goto next; - case GEMINI_STATUS_CLASS_REDIRECT: - free(url); diff --git a/sources/cgmnlm.git/commits/7e4e43b05c298aa812027bf1921ce3f224e86bda.patch b/sources/cgmnlm.git/commits/7e4e43b05c298aa812027bf1921ce3f224e86bda.patch @@ -1,20 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index cec330b44ae6e9252e92fbd1a5d74e7592b84e2a..9efe3e231d445d55ea43d444a72eaa79f595ac76 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -475,7 +475,6 @@ } - } else { - browser->opts.client_cert = NULL; - } -- free(host); - } - - while (requesting) { -@@ -600,6 +599,7 @@ if (client_cert.key) { - free(client_cert.key); - } - free(scheme); -+ free(host); - return res; - } - diff --git a/sources/cgmnlm.git/commits/801d9b8f13f6adef25fb14ec2e9acbc6dd4e92a9.patch b/sources/cgmnlm.git/commits/801d9b8f13f6adef25fb14ec2e9acbc6dd4e92a9.patch @@ -1,87 +0,0 @@ -diff --git a/README.md b/README.md -index fa803f99158582d982ef3a01d6b87471fb27f0cb..9aa989fe358ecccd9b984b1aff0df97c6c2ceec0 100644 ---- a/README.md -+++ b/README.md -@@ -29,7 +29,7 @@ - s command to directly search in geminispace (via geminispace.info) - - k command to remove the bookmark for the current page - - e[N] command to open a link in default external program (requires `xdg-open`) - - t[N] command to download the content behind a link to a temporary file --- b & f commands to navigate history can jump multiple entries at once -+- a command to switch between display of preformatted blocks and alt text (if available) - - The actual colors used depend on your terminal palette: - - heading 1: light red -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 8cafaea17de972c240031b0fe6ab88e3621bd871..77d03cb16e1e27c440ecb1c5b7d154db50659dc6 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -44,7 +44,7 @@ struct history *prev, *next; - }; - - struct browser { -- bool pagination, unicode; -+ bool pagination, unicode, alttext; - int max_width; - struct gemini_options opts; - struct gemini_tofu tofu; -@@ -103,6 +103,7 @@ "n\t\tjump to next search match\n" - "d <path>\tDownload page to <path>\n" - "|<prog>\t\tPipe page into program\n" - "[N]|<prog>\tPipe content of Nth link into program\n" -+ "a\t\ttoggle usage of alt text instead of preformatted text\n" - "q\t\tQuit\n" - "\n" - ; -@@ -636,6 +637,11 @@ if (in[1]) break; - set_url(browser, "gemini://geminispace.info/search", &browser->history); - result = PROMPT_ANSWERED; - goto exit; -+ case 'a': -+ browser->alttext = !browser->alttext; -+ fprintf(browser->tty, "Alttext instead of preformatted block is now %s\n", browser->alttext ? "ENABLED" : "DISABLED"); -+ result = PROMPT_AGAIN; -+ goto exit; - case 'b': - if (in[1]) historyhops =(int)strtol(in+1, &endptr, 10); - while (historyhops > 0) { -@@ -939,6 +945,7 @@ - fprintf(out, "\n"); - char *text = NULL; - int row = 0, col = 0; -+ bool no_alttext; - struct gemini_token tok; - struct link **next = &browser->links; - while (text != NULL || gemini_parser_next(&p, &tok) == 0) { -@@ -962,13 +969,21 @@ col += fprintf(out, " "); - } - break; - case GEMINI_PREFORMATTED_BEGIN: -- gemini_token_finish(&tok); -+ if (text == NULL && browser->alttext) { -+ if (tok.preformatted == NULL) { -+ no_alttext = true; -+ } else { -+ fprintf(out, " A %s", ANSI_COLOR_GRAY); -+ text = tok.preformatted; -+ } -+ } -+ break; - /* fallthrough */ - case GEMINI_PREFORMATTED_END: - continue; // Not used - case GEMINI_PREFORMATTED_TEXT: -- if (text == NULL) { -- fprintf(out, " %s", ANSI_COLOR_GRAY); -+ if (text == NULL && (!browser->alttext || no_alttext)) { -+ fprintf(out, " P %s", ANSI_COLOR_GRAY); - text = tok.preformatted; - } - break; -@@ -1241,6 +1256,7 @@ main(int argc, char *argv[]) - { - struct browser browser = { - .pagination = true, -+ .alttext = false, - .tofu_mode = TOFU_ASK, - .unicode = true, - .url = curl_url(), diff --git a/sources/cgmnlm.git/commits/84da4b3f2b95bead2c1609eb572a2369576eae77.patch b/sources/cgmnlm.git/commits/84da4b3f2b95bead2c1609eb572a2369576eae77.patch @@ -1,46 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index d22f64ce27dfcf61eed80ed09b2d072efb8d71e0..6ad61939f8dcd6f6cf530f5615e442fcfb92ffde 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -71,13 +71,13 @@ - const char *help_msg = - "The following commands are available:\n\n" - "q\tQuit\n" -- "N\tFollow Nth link (where N is a number)\n" -- "p[N]\tShow URL of Nth link (where N is a number)\n" -+ "[N]\tFollow Nth link (where N is a number)\n" -+ "u[N]\tShow URL of Nth link (where N is a number)\n" - "b\tBack (in the page history)\n" - "f\tForward (in the page history)\n" - "H\tView all page history\n" -- "m\tSave bookmark\n" -- "M\tBrowse bookmarks\n" -+ "a\tSave bookmark\n" -+ "B\tBrowse bookmarks\n" - "r\tReload the page\n" - "d <path>\tDownload page to <path>\n" - "|<prog>\tPipe page into program\n" -@@ -549,12 +549,12 @@ cur = cur->next; - } - result = PROMPT_AGAIN; - goto exit; -- case 'm': -+ case 'a': - if (in[1]) break; - save_bookmark(browser); - result = PROMPT_AGAIN; - goto exit; -- case 'M': -+ case 'B': - if (in[1]) break; - open_bookmarks(browser); - result = PROMPT_ANSWERED; -@@ -582,7 +582,7 @@ fprintf(stderr, "Cannot move to next result; we are not searching for anything\n"); - result = PROMPT_AGAIN; - goto exit; - } -- case 'p': -+ case 'u': - if (!in[1]) break; - struct link *link = browser->links; - char *endptr; diff --git a/sources/cgmnlm.git/commits/84df94447cdb081ec305a5ba9d2b0ef89dd34fc3.patch b/sources/cgmnlm.git/commits/84df94447cdb081ec305a5ba9d2b0ef89dd34fc3.patch @@ -1,13 +0,0 @@ -diff --git a/README.md b/README.md -index 842e238162c33eec93dad8b10abb7bfe21af447b..dfd2e1744003996f1e629c78e459dac70985a430 100644 ---- a/README.md -+++ b/README.md -@@ -5,7 +5,7 @@ - - A CLI utility (like curl): gmni - - A [line-mode browser](https://en.wikipedia.org/wiki/Line_Mode_Browser): gmnlm - --[](https://asciinema.org/a/ldo2gV7qiDoBXvGwuD6x1jbn3) -+[](https://asciinema.org/a/ldo2gV7qiDoBXvGwuD6x1jbn3) - - Dependencies: - diff --git a/sources/cgmnlm.git/commits/852bc7198f9d1d838d76d74a006cc2a2e63e4f1c.patch b/sources/cgmnlm.git/commits/852bc7198f9d1d838d76d74a006cc2a2e63e4f1c.patch @@ -1,120 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 2375151c7f5fced09a80e41d26809730827f8925..4b844200eeb4318d694566d7a84eb3e64c7c7668 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -82,6 +82,7 @@ if (curl_url_set(browser->url, CURLUPART_URL, new_url, 0) != CURLUE_OK) { - fprintf(stderr, "Error: invalid URL\n"); - return false; - } -+ curl_url_get(browser->url, CURLUPART_URL, &browser->plain_url, 0); - if (history) { - struct history *next = calloc(1, sizeof(struct history)); - curl_url_get(browser->url, CURLUPART_URL, &next->url, 0); -@@ -118,19 +119,23 @@ case '\0': - result = PROMPT_MORE; - goto exit; - case 'q': -+ if (in[1]) break; - result = PROMPT_QUIT; - goto exit; - case 'b': -+ if (in[1]) break; - if (!browser->history->prev) { - fprintf(stderr, "At beginning of history\n"); - result = PROMPT_AGAIN; - goto exit; - } -+ if (in[1]) break; - browser->history = browser->history->prev; - set_url(browser, browser->history->url, NULL); - result = PROMPT_ANSWERED; - goto exit; - case 'f': -+ if (in[1]) break; - if (!browser->history->next) { - fprintf(stderr, "At end of history\n"); - result = PROMPT_AGAIN; -@@ -141,6 +146,7 @@ set_url(browser, browser->history->url, NULL); - result = PROMPT_ANSWERED; - goto exit; - case '/': -+ if (in[1]) break; - if ((r = regcomp(&browser->regex, &in[1], REG_EXTENDED)) != 0) { - static char buf[1024]; - r = regerror(r, &browser->regex, buf, sizeof(buf)); -@@ -153,6 +159,7 @@ result = PROMPT_ANSWERED; - } - goto exit_re; - case 'n': -+ if (in[1]) break; - if (browser->searching) { - result = PROMPT_NEXT; - goto exit_re; -@@ -162,6 +169,7 @@ result = PROMPT_AGAIN; - goto exit; - } - case '?': -+ if (in[1]) break; - fprintf(browser->tty, "%s", help_msg); - result = PROMPT_AGAIN; - goto exit; -@@ -465,6 +473,17 @@ } - 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; -+} -+ - // Returns true to skip prompting - static bool - do_requests(struct browser *browser, struct gemini_response *resp) -@@ -472,9 +491,40 @@ { - int nredir = 0; - bool requesting = true; - while (requesting) { -+ char *scheme; - CURLUcode uc = curl_url_get(browser->url, -- CURLUPART_URL, &browser->plain_url, 0); -+ CURLUPART_SCHEME, &scheme, 0); - assert(uc == CURLUE_OK); // Invariant -+ 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; -+ } -+ -+ FILE *fp = fopen(path, "r"); -+ if (!fp) { -+ resp->status = GEMINI_STATUS_NOT_FOUND; -+ break; -+ } -+ -+ BIO *file = BIO_new_fp(fp, BIO_CLOSE); -+ resp->bio = BIO_new(BIO_f_buffer()); -+ BIO_push(resp->bio, file); -+ 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"); -+ } -+ resp->status = GEMINI_STATUS_SUCCESS; -+ return display_response(browser, resp); -+ } - - enum gemini_result res = gemini_request(browser->plain_url, - &browser->opts, resp); diff --git a/sources/cgmnlm.git/commits/86b299819c86758f2b537c1de0475a2906f0a4d2.patch b/sources/cgmnlm.git/commits/86b299819c86758f2b537c1de0475a2906f0a4d2.patch @@ -1,28 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 37b4db277d8fcd961eb1746294724783e638109c..3bd2ea9a9b70cc33f6fc5b330234c17a974f80f5 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -684,15 +684,19 @@ goto exit; - case 'H': - if (in[1]) break; - struct history *cur = browser->history; -- while (cur->prev) cur = cur->prev; -+ int hist_count = 0; -+ while (cur->prev) { -+ cur = cur->prev; -+ hist_count++; -+ } - while (cur != browser->history) { -- fprintf(browser->tty, " %s\n", cur->url); -+ fprintf(browser->tty, "b%-3i %s\n", hist_count--, cur->url); - cur = cur->next; - } -- fprintf(browser->tty, "* %s\n", cur->url); -+ fprintf(browser->tty, "* %s\n", cur->url); - cur = cur->next; - while (cur) { -- fprintf(browser->tty, " %s\n", cur->url); -+ fprintf(browser->tty, "f%-3i %s\n", ++hist_count, cur->url); - cur = cur->next; - } - result = PROMPT_AGAIN; diff --git a/sources/cgmnlm.git/commits/8970adc23e0a1bcf29d211f353dbd5ebd68cfe66.patch b/sources/cgmnlm.git/commits/8970adc23e0a1bcf29d211f353dbd5ebd68cfe66.patch @@ -1,13 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 9f76188252a30e95868b99d64d57369ac03a7e1c..23b8e6054c6824ebfe4aba19404d7eeb2c3dab35 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -471,7 +471,7 @@ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: - requesting = false; - fprintf(stderr, "Server returned %s %d %s\n", - resp->status / 10 == 4 ? -- "TEMPORARY FAILURE" : "PERMANENT FALIURE", -+ "TEMPORARY FAILURE" : "PERMANENT FAILURE", - resp->status, resp->meta); - break; - case GEMINI_STATUS_CLASS_SUCCESS: diff --git a/sources/cgmnlm.git/commits/8a83030e5a390c2151c485b3c091ba28ddebcab7.patch b/sources/cgmnlm.git/commits/8a83030e5a390c2151c485b3c091ba28ddebcab7.patch @@ -1,35 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 23b8e6054c6824ebfe4aba19404d7eeb2c3dab35..c5419243f983c878abff1cfb36633d648f6449f2 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -159,7 +159,7 @@ size_t n; - - n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - assert(n < sizeof(path)); -- strncpy(dname, dirname(path), sizeof(dname)); -+ strncpy(dname, dirname(path), sizeof(dname)-1); - if (mkdirs(dname, 0755) != 0) { - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - free(path_fmt); -@@ -200,7 +200,7 @@ size_t n; - - n = snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - assert(n < sizeof(path)); -- strncpy(dname, dirname(path), sizeof(dname)); -+ strncpy(dname, dirname(path), sizeof(dname)-1); - if (mkdirs(dname, 0755) != 0) { - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - free(path_fmt); -diff --git a/src/tofu.c b/src/tofu.c -index 16548a1cbcebc8c4cdda4a57a4655a04e7f390a3..b9100c77fd61c71ce561f2194b991eee7130e689 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -164,7 +164,7 @@ n = snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), - path_fmt, "known_hosts"); - assert(n < sizeof(tofu->known_hosts_path)); - -- strncpy(dname, dirname(tofu->known_hosts_path), sizeof(dname)); -+ strncpy(dname, dirname(tofu->known_hosts_path), sizeof(dname)-1); - if (mkdirs(dname, 0755) != 0) { - snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path), - path_fmt, "known_hosts"); diff --git a/sources/cgmnlm.git/commits/8bb1d81f539f1e223e8fcd79e2d18f58e3c9d28f.patch b/sources/cgmnlm.git/commits/8bb1d81f539f1e223e8fcd79e2d18f58e3c9d28f.patch @@ -1,14 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index def42ac8b1af405da29681d29cd7b9d07b727f11..07d0000babe7cd51876ead407e83c3062adfda56 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -380,6 +380,9 @@ assert(0); // Not supposed to happen - case '\t': - *col = *col + (8 - *col % 8); - break; -+ case '\r': -+ if (!s[i+1]) break; -+ /* fallthrough */ - default: - if (iscntrl(s[i])) { - s[i] = '.'; diff --git a/sources/cgmnlm.git/commits/8c473eda5e4c6537058d0ff1815f2943e7b41498.patch b/sources/cgmnlm.git/commits/8c473eda5e4c6537058d0ff1815f2943e7b41498.patch @@ -1,197 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 26654324b4fb845633f7db2b2ac01e5e3b4635a1..99ecb7d1d76f8b5f0c856ca9dcd2d6abf5dd6de6 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -35,6 +35,7 @@ struct gemini_tofu tofu; - enum tofu_action tofu_mode; - - FILE *tty; -+ char *meta; - char *plain_url; - char *page_title; - struct Curl_URL *url; -@@ -206,6 +207,52 @@ snprintf(url, sizeof(url), "file://%s", path); - set_url(browser, url, &browser->history); - } - -+static void -+print_media_parameters(FILE *out, char *params) -+{ -+ if (params == NULL) { -+ fprintf(out, "No media parameters\n"); -+ return; -+ } -+ for (char *param = strtok(params, ";"); param; -+ param = strtok(NULL, ";")) { -+ char *value = strchr(param, '='); -+ if (value == NULL) { -+ fprintf(out, "Invalid media type parameter '%s'\n", -+ trim_ws(param)); -+ continue; -+ } -+ *value = 0; -+ fprintf(out, "%s: ", trim_ws(param)); -+ *value++ = '='; -+ if (*value != '"') { -+ fprintf(out, "%s\n", value); -+ continue; -+ } -+ while (value++) { -+ switch (*value) { -+ case '\0': -+ if ((value = strtok(NULL, ";")) != NULL) { -+ fprintf(out, ";%c", *value); -+ } -+ break; -+ case '"': -+ value = NULL; -+ break; -+ case '\\': -+ if (value[1] == '\0') { -+ break; -+ } -+ value++; -+ /* fallthrough */ -+ default: -+ putc(*value, out); -+ } -+ } -+ putc('\n', out); -+ } -+} -+ - static enum prompt_result - do_prompts(const char *prompt, struct browser *browser) - { -@@ -329,6 +376,12 @@ case 'r': - if (in[1]) break; - result = PROMPT_ANSWERED; - goto exit; -+ case 'i': -+ if (in[1]) break; -+ print_media_parameters(browser->tty, browser->meta -+ ? strchr(browser->meta, ';') : NULL); -+ result = PROMPT_AGAIN; -+ goto exit; - case '?': - if (in[1]) break; - fprintf(browser->tty, "%s", help_msg); -@@ -539,12 +592,19 @@ ++row; col = 0; - - if (browser->pagination && row >= ws.ws_row - 4) { - char prompt[4096]; -+ char *end = NULL; -+ if (browser->meta && (end = strchr(resp->meta, ';')) != NULL) { -+ *end = 0; -+ } - snprintf(prompt, sizeof(prompt), "\n%s at %s\n" - "[Enter]: read more; %s[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" - "(more) => ", resp->meta, browser->plain_url, - browser->searching ? "[n]ext result; " : "", - browser->history->prev ? "[b]ack; " : "", - browser->history->next ? "[f]orward; " : ""); -+ if (end != NULL) { -+ *end = ';'; -+ } - enum prompt_result result = PROMPT_AGAIN; - while (result == PROMPT_AGAIN) { - result = do_prompts(prompt, browser); -@@ -714,6 +774,7 @@ &browser->opts, resp); - if (res != GEMINI_OK) { - fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); - requesting = false; -+ resp->status = 70 + res; - break; - } - -@@ -858,6 +919,7 @@ .tofu_mode = TOFU_ASK, - .unicode = true, - .url = curl_url(), - .tty = fopen("/dev/tty", "w+"), -+ .meta = NULL, - }; - - int c; -@@ -909,38 +971,45 @@ struct gemini_response resp; - browser.running = true; - while (browser.running) { - static char prompt[4096]; -- if (do_requests(&browser, &resp)) { -- // Skip prompts -- gemini_response_finish(&resp); -- goto next; -+ bool skip_prompt = do_requests(&browser, &resp); -+ if (browser.meta) { -+ free(browser.meta); - } -- -- snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -- "[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" -- "=> ", -- resp.status == GEMINI_STATUS_SUCCESS ? resp.meta : "", -- browser.plain_url, -- browser.history->prev ? "[b]ack; " : "", -- browser.history->next ? "[f]orward; " : ""); -+ browser.meta = resp.status == GEMINI_STATUS_SUCCESS -+ ? strdup(resp.meta) : NULL; - gemini_response_finish(&resp); -+ if (!skip_prompt) { -+ char *end = NULL; -+ if (browser.meta && (end = strchr(browser.meta, ';')) != NULL) { -+ *end = 0; -+ } -+ snprintf(prompt, sizeof(prompt), "\n%s at %s\n" -+ "[N]: follow Nth link; %s%s[q]uit; [?]; or type a URL\n" -+ "=> ", browser.meta ? browser.meta -+ : "[request failed]", browser.plain_url, -+ browser.history->prev ? "[b]ack; " : "", -+ browser.history->next ? "[f]orward; " : ""); -+ if (end != NULL) { -+ *end = ';'; -+ } - -- enum prompt_result result = PROMPT_AGAIN; -- while (result == PROMPT_AGAIN || result == PROMPT_MORE) { -- result = do_prompts(prompt, &browser); -- } -- switch (result) { -- case PROMPT_AGAIN: -- case PROMPT_MORE: -- assert(0); -- case PROMPT_QUIT: -- browser.running = false; -- break; -- case PROMPT_ANSWERED: -- case PROMPT_NEXT: -- break; -+ enum prompt_result result = PROMPT_AGAIN; -+ while (result == PROMPT_AGAIN || result == PROMPT_MORE) { -+ result = do_prompts(prompt, &browser); -+ } -+ switch (result) { -+ case PROMPT_AGAIN: -+ case PROMPT_MORE: -+ assert(0); -+ case PROMPT_QUIT: -+ browser.running = false; -+ break; -+ case PROMPT_ANSWERED: -+ case PROMPT_NEXT: -+ break; -+ } - } - --next:; - struct link *link = browser.links; - while (link) { - struct link *next = link->next; -@@ -961,6 +1030,9 @@ SSL_CTX_free(browser.opts.ssl_ctx); - curl_url_cleanup(browser.url); - free(browser.page_title); - free(browser.plain_url); -+ if (browser.meta != NULL) { -+ free(browser.meta); -+ } - fclose(browser.tty); - return 0; - } diff --git a/sources/cgmnlm.git/commits/8cac260a4b7c0b4df4d1229a5e41e64c3a687173.patch b/sources/cgmnlm.git/commits/8cac260a4b7c0b4df4d1229a5e41e64c3a687173.patch @@ -1,30 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 489a6833f75c9e6a13562c84dc8e3ded24be23c4..930ab19abe717b186070151a184b2a8e98542f77 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -82,6 +82,7 @@ "<Enter>\t\tread more lines (if available)\n" - "<url>\t\tgo to url\n" - "[N]\t\tFollow Nth link (where N is a number)\n" - "p[N]\t\tPrint URL of Nth link (where N is a number)\n" -+ "e\t\tSend current URL of browser to external default program\n" - "e[N]\t\tSend URL of Nth link in external default program\n" - "t[N]\t\tDownload content of Nth link to a temporary file\n" - "b[N]\t\tJump back N entries in history, N is optional, default 1\n" -@@ -645,7 +646,16 @@ } - case 'e': - case 'p': - case 't': -- if (!in[1]) break; -+ if (!in[1]) { -+ if (in[0] == 'e') { -+ char xdgopen[4096]; -+ snprintf(xdgopen, sizeof(xdgopen), "xdg-open %s", browser->plain_url); -+ if ( !system(xdgopen) ) fprintf(browser->tty, "Link send to xdg-open\n"); -+ goto exit; -+ } else { -+ break; -+ } -+ } - linksel = (int)strtol(in+1, &endptr, 10); - if (!endptr[0] && linksel >= 0) { - while (linksel > 0 && link) { diff --git a/sources/cgmnlm.git/commits/8d897e4a00be9986209f1ca394ed46befadf6088.patch b/sources/cgmnlm.git/commits/8d897e4a00be9986209f1ca394ed46befadf6088.patch @@ -1,13 +0,0 @@ -diff --git a/Makefile b/Makefile -index 22fed5d19ec4f989ead26d22d3d1887614e4927c..c686cc60d215bc9e6032b429a1bc8babc6509a2f 100644 ---- a/Makefile -+++ b/Makefile -@@ -40,7 +40,7 @@ @printf 'CC\t$@\n' - @touch $(OUTDIR)/cppcache - @grep $< $(OUTDIR)/cppcache >/dev/null || \ - $(CPP) $(CFLAGS) -MM -MT $@ $< >> $(OUTDIR)/cppcache -- @$(CC) -c -fPIC $(CFLAGS) -o $@ $< -+ @$(CC) -c $(CFLAGS) -o $@ $< - - .scd.1: - @printf 'SCDOC\t$@\n' diff --git a/sources/cgmnlm.git/commits/8ddc99fdc336957d7565cd50e329da9cbe9e4de8.patch b/sources/cgmnlm.git/commits/8ddc99fdc336957d7565cd50e329da9cbe9e4de8.patch @@ -1,12 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 77d03cb16e1e27c440ecb1c5b7d154db50659dc6..041096cc5f0117632f80b0427b9a916aaa6401c2 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -1085,6 +1085,7 @@ - switch (result) { - case PROMPT_AGAIN: - case PROMPT_MORE: -+ printf("\n"); - break; - case PROMPT_QUIT: - browser->running = false; diff --git a/sources/cgmnlm.git/commits/90995e834f2e87427f2f4bddf26a93258b45aa31.patch b/sources/cgmnlm.git/commits/90995e834f2e87427f2f4bddf26a93258b45aa31.patch @@ -1,50 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 99ecb7d1d76f8b5f0c856ca9dcd2d6abf5dd6de6..85917dfa97c034af41b9ab15e45139d6b95709de 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -834,7 +834,7 @@ } - - static enum tofu_action - tofu_callback(enum tofu_error error, const char *fingerprint, -- struct known_host *host, void *data) -+ struct known_host *khost, void *data) - { - struct browser *browser = data; - if (browser->tofu_mode != TOFU_ASK) { -@@ -852,15 +852,22 @@ "you should not disclose personal information or trust the contents of the page.\n" - "trust [o]nce; [a]bort\n" - "=> "); - break; -- case TOFU_UNTRUSTED_CERT: -+ case TOFU_UNTRUSTED_CERT:; -+ char *host; -+ if (curl_url_get(browser->url, CURLUPART_HOST, &host, 0) != CURLUE_OK) { -+ fprintf(stderr, "Error: invalid URL %s\n", -+ browser->plain_url); -+ return TOFU_FAIL; -+ } - snprintf(prompt, sizeof(prompt), -- "The certificate offered by this server is of unknown trust. " -+ "The certificate offered by %s is of unknown trust. " - "Its fingerprint is: \n" - "%s\n\n" - "If you knew the fingerprint to expect in advance, verify that this matches.\n" - "Otherwise, it should be safe to trust this certificate.\n\n" - "[t]rust always; trust [o]nce; [a]bort\n" -- "=> ", fingerprint); -+ "=> ", host, fingerprint); -+ free(host); - break; - case TOFU_FINGERPRINT_MISMATCH: - snprintf(prompt, sizeof(prompt), -@@ -871,8 +878,8 @@ "%s\n\n" - "The expected fingerprint is:\n" - "%s\n\n" - "If you're certain that this is correct, edit %s:%d\n", -- fingerprint, host->fingerprint, -- browser->tofu.known_hosts_path, host->lineno); -+ fingerprint, khost->fingerprint, -+ browser->tofu.known_hosts_path, khost->lineno); - return TOFU_FAIL; - } - diff --git a/sources/cgmnlm.git/commits/95518992983e6531106b48c82edeb0ce825bf351.patch b/sources/cgmnlm.git/commits/95518992983e6531106b48c82edeb0ce825bf351.patch @@ -1,164 +0,0 @@ -diff --git a/include/client.h b/include/gmni.h -rename from include/client.h -rename to include/gmni.h -index 671de12e7ee53fbbfc03407760723d2a66458c2d..42cfdac95530c9d6a5f4e6e6c3b85635908cc1e6 100644 ---- a/include/client.h -+++ b/include/gmni.h -@@ -4,8 +4,49 @@ #include <netdb.h> - #include <openssl/ssl.h> - #include <sys/socket.h> - -+enum gemini_result { -+ GEMINI_OK, -+ GEMINI_ERR_OOM, -+ GEMINI_ERR_INVALID_URL, -+ GEMINI_ERR_RESOLVE, -+ GEMINI_ERR_CONNECT, -+ GEMINI_ERR_SSL, -+ GEMINI_ERR_IO, -+ GEMINI_ERR_PROTOCOL, -+}; -+ -+enum gemini_status { -+ GEMINI_STATUS_INPUT = 10, -+ GEMINI_STATUS_SENSITIVE_INPUT = 11, -+ GEMINI_STATUS_SUCCESS = 20, -+ GEMINI_STATUS_REDIRECT_TEMPORARY = 30, -+ GEMINI_STATUS_REDIRECT_PERMANENT = 31, -+ GEMINI_STATUS_TEMPORARY_FAILURE = 40, -+ GEMINI_STATUS_SERVER_UNAVAILABLE = 41, -+ GEMINI_STATUS_CGI_ERROR = 42, -+ GEMINI_STATUS_PROXY_ERROR = 43, -+ GEMINI_STATUS_SLOW_DOWN = 44, -+ GEMINI_STATUS_PERMANENT_FAILURE = 50, -+ GEMINI_STATUS_NOT_FOUND = 51, -+ GEMINI_STATUS_GONE = 52, -+ GEMINI_STATUS_PROXY_REQUEST_REFUSED = 53, -+ GEMINI_STATUS_BAD_REQUEST = 59, -+ GEMINI_STATUS_CLIENT_CERTIFICATE_REQUIRED = 60, -+ GEMINI_STATUS_CERTIFICATE_NOT_AUTHORIZED = 61, -+ GEMINI_STATUS_CERTIFICATE_NOT_VALID = 62, -+}; -+ -+enum gemini_status_class { -+ GEMINI_STATUS_CLASS_INPUT = 10, -+ GEMINI_STATUS_CLASS_SUCCESS = 20, -+ GEMINI_STATUS_CLASS_REDIRECT = 30, -+ GEMINI_STATUS_CLASS_TEMPORARY_FAILURE = 40, -+ GEMINI_STATUS_CLASS_PERMANENT_FAILURE = 50, -+ GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED = 60, -+}; -+ - struct gemini_response { -- int status; -+ enum gemini_status status; - char *meta; - - // Response body may be read from here if appropriate: -@@ -35,17 +76,6 @@ // example, to force IPv4/IPv6. - struct addrinfo *hints; - }; - --enum gemini_result { -- GEMINI_OK, -- GEMINI_ERR_OOM, -- GEMINI_ERR_INVALID_URL, -- GEMINI_ERR_RESOLVE, -- GEMINI_ERR_CONNECT, -- GEMINI_ERR_SSL, -- GEMINI_ERR_IO, -- GEMINI_ERR_PROTOCOL, --}; -- - // Requests the specified URL via the gemini protocol. If options is non-NULL, - // it may specify some additional configuration to adjust client behavior. - // -@@ -67,5 +97,9 @@ - // Returns the given URL with the input response set to the specified value. - // The caller must free the string. - char *gemini_input_url(const char *url, const char *input); -+ -+// Returns the general response class (i.e. with the second digit set to zero) -+// of the given Gemini status code. -+enum gemini_status_class gemini_response_class(enum gemini_status status); - - #endif -diff --git a/src/client.c b/src/client.c -index 59fa1381ab66496900acb4321f22c6ec3675db5e..86152d6183462636cca4eecbd47824569a688cb1 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -9,7 +9,7 @@ #include <string.h> - #include <sys/socket.h> - #include <sys/types.h> - #include <unistd.h> --#include "client.h" -+#include "gmni.h" - #include "url.h" - - static enum gemini_result -@@ -264,3 +264,9 @@ cleanup: - curl_url_cleanup(uri); - return new_url; - } -+ -+enum gemini_status_class -+gemini_response_class(enum gemini_status status) -+{ -+ return status / 10; -+} -diff --git a/src/gmni.c b/src/gmni.c -index bdaa9baaca75e5f167add9a0ebed73cc60eaa647..b4efdc0235dac0279ce284f2c6b19f680a191647 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -11,7 +11,7 @@ #include <string.h> - #include <sys/socket.h> - #include <sys/types.h> - #include <unistd.h> --#include "client.h" -+#include "gmni.h" - - static void - usage(char *argv_0) -@@ -120,8 +120,8 @@ goto next; - } - - char *new_url, *input = NULL; -- switch (resp.status / 10) { -- case 1: // INPUT -+ switch (gemini_response_class(resp.status)) { -+ case GEMINI_STATUS_CLASS_INPUT: - if (input_mode == INPUT_SUPPRESS) { - exit = true; - break; -@@ -149,7 +149,7 @@ free(url); - url = new_url; - assert(url); - goto next; -- case 3: // REDIRECT -+ case GEMINI_STATUS_CLASS_REDIRECT: - free(url); - url = strdup(resp.meta); - if (!follow_redirects) { -@@ -160,10 +160,10 @@ } - exit = true; - } - goto next; -- case 6: // CLIENT CERTIFICATE REQUIRED -+ case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: - assert(0); // TODO -- case 4: // TEMPORARY FAILURE -- case 5: // PERMANENT FAILURE -+ case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: -+ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: - if (header_mode == OMIT_HEADERS) { - fprintf(stderr, "%s: %d %s\n", - resp.status / 10 == 4 ? -@@ -172,7 +172,7 @@ resp.status, resp.meta); - } - exit = true; - break; -- case 2: // SUCCESS -+ case GEMINI_STATUS_CLASS_SUCCESS: - exit = true; - break; - } diff --git a/sources/cgmnlm.git/commits/9551d0a3822312a0a4917ccbe80fdaeb49954d70.patch b/sources/cgmnlm.git/commits/9551d0a3822312a0a4917ccbe80fdaeb49954d70.patch @@ -1,42 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 02d8829598902d93f1dea3d68073f609573370c1..199572c363f9e50acdd052111350f3926d6a350d 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -57,6 +57,7 @@ const char *help_msg = - "The following commands are available:\n\n" - "q\tQuit\n" - "N\tFollow Nth link (where N is a number)\n" -+ "p[N]\tShow URL of Nth link (where N is a number)\n" - "b\tBack (in the page history)\n" - "f\tForward (in the page history)\n" - "H\tView all page history\n" -@@ -267,6 +268,29 @@ fprintf(stderr, "Cannot move to next result; we are not searching for anything\n"); - result = PROMPT_AGAIN; - goto exit; - } -+ case 'p': -+ if (!in[1]) break; -+ struct link *link = browser->links; -+ char *endptr; -+ int linksel = (int)strtol(in+1, &endptr, 10); -+ if (!endptr[0] && linksel >= 0) { -+ while (linksel > 0 && link) { -+ link = link->next; -+ --linksel; -+ } -+ -+ if (!link) { -+ fprintf(stderr, "Error: no such link.\n"); -+ } else { -+ fprintf(browser->tty, "=> %s\n", link->url); -+ result = PROMPT_AGAIN; -+ goto exit; -+ } -+ } else { -+ fprintf(stderr, "Error: invalid argument.\n"); -+ } -+ result = PROMPT_AGAIN; -+ goto exit; - case '?': - if (in[1]) break; - fprintf(browser->tty, "%s", help_msg); diff --git a/sources/cgmnlm.git/commits/955f7524b955e19bc89c6e9f76f3f3ecfb7bfb58.patch b/sources/cgmnlm.git/commits/955f7524b955e19bc89c6e9f76f3f3ecfb7bfb58.patch @@ -1,445 +0,0 @@ -diff --git a/configure b/configure -index 70a19b193d6e33b41b5cc7ef13511bb0f5b4e076..7b55a2580018cc69b2f52c9bc53832cc9faadc4f 100755 ---- a/configure -+++ b/configure -@@ -4,6 +4,7 @@ eval ". $srcdir/config.sh" - - gmni() { - genrules gmni \ -+ src/certs.c \ - src/client.c \ - src/escape.c \ - src/gmni.c \ -diff --git a/doc/gmni.scd b/doc/gmni.scd -index 1f20672f1fb0c8ff6a4f3a97a3324a25d2a0a01d..866dd418a510ccd141d9c23afb4b321b84a5f9ec 100644 ---- a/doc/gmni.scd -+++ b/doc/gmni.scd -@@ -38,11 +38,9 @@ If the server requests user input, _path_ is opened and read, and a - second request is performed with the contents of _path_ as the user - input. - --*-E* _path_[:_password_] -- Sets the path to the client certificate to use (and optionally a -- password). If the filename contains ":" but the certificate does not -- accept a password, append ":" to the path and it will be intepreted as -- an empty password. -+*-E* _path_:_key_ -+ Sets the path to the client certificate and private key file to use, -+ both PEM encoded. - - *-l* - For *text/\** responses, *gmni* normally adds a line feed if stdout is a -diff --git a/include/gmni/certs.h b/include/gmni/certs.h -new file mode 100644 -index 0000000000000000000000000000000000000000..22e226d6b4252dddd8526970cec0a947f12242d1 ---- /dev/null -+++ b/include/gmni/certs.h -@@ -0,0 +1,27 @@ -+#ifndef GEMINI_CERTS_H -+#define GEMINI_CERTS_H -+#include <bearssl.h> -+#include <stdio.h> -+ -+struct gmni_options; -+ -+struct gmni_client_certificate { -+ br_x509_certificate *chain; -+ size_t nchain; -+ struct gmni_private_key *key; -+}; -+ -+struct gmni_private_key { -+ int type; -+ union { -+ br_rsa_private_key rsa; -+ br_ec_private_key ec; -+ }; -+ unsigned char data[]; -+}; -+ -+// Returns nonzero on failure and sets errno -+int gmni_ccert_load(struct gmni_client_certificate *cert, -+ FILE *certin, FILE *skin); -+ -+#endif -diff --git a/include/gmni/gmni.h b/include/gmni/gmni.h -index 16bef51024275bbb6ade9c90a11ae68876df387a..22295e20fb36d492a979c060de8dcdca14df5a34 100644 ---- a/include/gmni/gmni.h -+++ b/include/gmni/gmni.h -@@ -1,6 +1,6 @@ - #ifndef GEMINI_CLIENT_H - #define GEMINI_CLIENT_H --#include <bearssl_ssl.h> -+#include <bearssl.h> - #include <netdb.h> - #include <stdbool.h> - #include <sys/socket.h> -@@ -61,6 +61,8 @@ br_ssl_client_context *sc; - int fd; - }; - -+struct gmni_client_certificate; -+ - struct gemini_options { - // If ai_family != AF_UNSPEC (the default value on most systems), the - // client will connect to this address and skip name resolution. -@@ -69,6 +71,10 @@ - // If non-NULL, these hints are provided to getaddrinfo. Useful, for - // example, to force IPv4/IPv6. - struct addrinfo *hints; -+ -+ // If non-NULL, this will be used as the client certificate for the -+ // request. The other fields must be set as well. -+ struct gmni_client_certificate *client_cert; - }; - - struct gemini_tofu; -diff --git a/include/gmni/tofu.h b/include/gmni/tofu.h -index a0981a5296421541a766846bc36c733dab1dfd1a..51d1d60a3719469a1f8cd295b9d85e21d7affb43 100644 ---- a/include/gmni/tofu.h -+++ b/include/gmni/tofu.h -@@ -1,6 +1,6 @@ - #ifndef GEMINI_TOFU_H - #define GEMINI_TOFU_H --#include <bearssl_x509.h> -+#include <bearssl.h> - #include <limits.h> - - enum tofu_error { -diff --git a/src/certs.c b/src/certs.c -new file mode 100644 -index 0000000000000000000000000000000000000000..f40bfa7d27925baaa2fe6dbdf0d6c18ce4e10014 ---- /dev/null -+++ b/src/certs.c -@@ -0,0 +1,156 @@ -+#include <assert.h> -+#include <bearssl.h> -+#include <errno.h> -+#include <gmni/certs.h> -+#include <gmni/gmni.h> -+#include <stdio.h> -+#include <stdlib.h> -+ -+static void -+crt_append(void *ctx, const void *src, size_t len) -+{ -+ br_x509_certificate *crt = (br_x509_certificate *)ctx; -+ crt->data = realloc(crt->data, crt->data_len + len); -+ assert(crt->data); -+ memcpy(&crt->data[crt->data_len], src, len); -+ crt->data_len += len; -+} -+ -+static void -+key_append(void *ctx, const void *src, size_t len) -+{ -+ br_skey_decoder_context *skctx = (br_skey_decoder_context *)ctx; -+ br_skey_decoder_push(skctx, src, len); -+} -+ -+int -+gmni_ccert_load(struct gmni_client_certificate *cert, FILE *certin, FILE *skin) -+{ -+ // TODO: Better error propagation to caller -+ static unsigned char buf[BUFSIZ]; -+ -+ br_pem_decoder_context pemdec; -+ br_pem_decoder_init(&pemdec); -+ -+ cert->chain = NULL; -+ cert->nchain = 0; -+ -+ static const char *certname = "CERTIFICATE"; -+ while (!feof(certin)) { -+ size_t n = fread(&buf, 1, sizeof(buf), certin); -+ if (ferror(certin)) { -+ goto error; -+ } -+ size_t q = 0; -+ while (q < n) { -+ q += br_pem_decoder_push(&pemdec, &buf[q], n - q); -+ switch (br_pem_decoder_event(&pemdec)) { -+ case BR_PEM_BEGIN_OBJ: -+ if (strcmp(br_pem_decoder_name(&pemdec), certname) != 0) { -+ break; -+ } -+ cert->chain = realloc(cert->chain, -+ sizeof(br_x509_certificate) * (cert->nchain + 1)); -+ memset(&cert->chain[cert->nchain], 0, sizeof(*cert->chain)); -+ br_pem_decoder_setdest(&pemdec, &crt_append, -+ &cert->chain[cert->nchain]); -+ ++cert->nchain; -+ break; -+ case BR_PEM_END_OBJ: -+ break; -+ case BR_PEM_ERROR: -+ fprintf(stderr, "Error decoding PEM certificate\n"); -+ errno = EINVAL; -+ goto error; -+ } -+ } -+ } -+ -+ if (cert->nchain == 0) { -+ fprintf(stderr, "No certificates found in provided client certificate file\n"); -+ errno = EINVAL; -+ goto error; -+ } -+ -+ br_skey_decoder_context skdec = {0}; -+ br_skey_decoder_init(&skdec); -+ br_pem_decoder_init(&pemdec); -+ -+ // TODO: Better validation of PEM file -+ while (!feof(skin)) { -+ size_t n = fread(&buf, 1, sizeof(buf), skin); -+ if (ferror(skin)) { -+ goto error; -+ } -+ size_t q = 0; -+ while (q < n) { -+ q += br_pem_decoder_push(&pemdec, &buf[q], n - q); -+ switch (br_pem_decoder_event(&pemdec)) { -+ case BR_PEM_BEGIN_OBJ: -+ br_pem_decoder_setdest(&pemdec, &key_append, &skdec); -+ break; -+ case BR_PEM_END_OBJ: -+ // no-op -+ break; -+ case BR_PEM_ERROR: -+ fprintf(stderr, "Error decoding PEM private key\n"); -+ errno = EINVAL; -+ goto error; -+ } -+ } -+ } -+ -+ int err = br_skey_decoder_last_error(&skdec); -+ if (err != 0) { -+ fprintf(stderr, "Error loading private key: %d\n", err); -+ errno = EINVAL; -+ goto error; -+ } -+ switch (br_skey_decoder_key_type(&skdec)) { -+ struct gmni_private_key *k; -+ const br_ec_private_key *ec; -+ const br_rsa_private_key *rsa; -+ case BR_KEYTYPE_RSA: -+ rsa = br_skey_decoder_get_rsa(&skdec); -+ cert->key = k = malloc(sizeof(*k) -+ + rsa->plen + rsa->qlen -+ + rsa->dplen + rsa->dqlen -+ + rsa->iqlen); -+ assert(k); -+ k->type = BR_KEYTYPE_RSA; -+ k->rsa = *rsa; -+ k->rsa.p = k->data; -+ k->rsa.q = k->rsa.p + k->rsa.plen; -+ k->rsa.dp = k->rsa.q + k->rsa.qlen; -+ k->rsa.dq = k->rsa.dp + k->rsa.dplen; -+ k->rsa.iq = k->rsa.dq + k->rsa.dqlen; -+ memcpy(k->rsa.p, rsa->p, rsa->plen); -+ memcpy(k->rsa.q, rsa->q, rsa->qlen); -+ memcpy(k->rsa.dp, rsa->dp, rsa->dplen); -+ memcpy(k->rsa.dq, rsa->dq, rsa->dqlen); -+ memcpy(k->rsa.iq, rsa->iq, rsa->iqlen); -+ break; -+ case BR_KEYTYPE_EC: -+ ec = br_skey_decoder_get_ec(&skdec); -+ cert->key = k = malloc(sizeof(*k) + ec->xlen); -+ assert(k); -+ k->type = BR_KEYTYPE_EC; -+ k->ec.curve = ec->curve; -+ k->ec.x = k->data; -+ k->ec.xlen = ec->xlen; -+ memcpy(k->ec.x, ec->x, ec->xlen); -+ break; -+ default: -+ assert(0); -+ } -+ -+ fclose(certin); -+ fclose(skin); -+ return 0; -+ -+error: -+ fclose(certin); -+ fclose(skin); -+ free(cert->chain); -+ return 1; -+} -diff --git a/src/client.c b/src/client.c -index e402cc97d96a904a2a8e9e2db40345b222cb85ae..127a56ca59859e645fea6f1e4ac62431d734e4fd 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -1,13 +1,14 @@ - #include <assert.h> - #include <errno.h> - #include <netdb.h> --#include <bearssl_ssl.h> -+#include <bearssl.h> - #include <stdlib.h> - #include <stdio.h> - #include <string.h> - #include <sys/socket.h> - #include <sys/types.h> - #include <unistd.h> -+#include <gmni/certs.h> - #include <gmni/gmni.h> - #include <gmni/tofu.h> - #include <gmni/url.h> -@@ -169,7 +170,26 @@ } - - // TODO: session reuse - resp->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, -+ 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, -+ cert->chain, cert->nchain, &key->ec, -+ BR_KEYTYPE_SIGN, 0, -+ br_ec_get_default(), -+ br_ecdsa_sign_asn1_get_default()); -+ break; -+ } -+ } - br_ssl_client_reset(resp->sc, host, 0); -+ - br_sslio_init(&resp->body, &resp->sc->eng, - sock_read, &resp->fd, sock_write, &resp->fd); - -diff --git a/src/gmni.c b/src/gmni.c -index a8321d06c367128706d19ac283a53e55d95d0a92..f3015ac679eba77397fb4aac5068f3a63e1cdbab 100644 ---- a/src/gmni.c -+++ b/src/gmni.c -@@ -1,5 +1,5 @@ - #include <assert.h> --#include <bearssl_ssl.h> -+#include <bearssl.h> - #include <errno.h> - #include <getopt.h> - #include <netdb.h> -@@ -11,6 +11,7 @@ #include <sys/socket.h> - #include <sys/types.h> - #include <termios.h> - #include <unistd.h> -+#include <gmni/certs.h> - #include <gmni/gmni.h> - #include <gmni/tofu.h> - #include <gmni/url.h> -@@ -109,6 +110,45 @@ - return action; - } - -+static struct gmni_client_certificate * -+load_client_cert(char *argv_0, char *path) -+{ -+ char *certpath = strtok(path, ":"); -+ if (!certpath) { -+ usage(argv_0); -+ exit(1); -+ } -+ -+ FILE *certf = fopen(certpath, "r"); -+ if (!certf) { -+ fprintf(stderr, "Failed to open certificate: %s\n", -+ strerror(errno)); -+ exit(1); -+ } -+ -+ char *keypath = strtok(NULL, ":"); -+ if (!keypath) { -+ usage(argv_0); -+ exit(1); -+ } -+ -+ FILE *keyf = fopen(keypath, "r"); -+ if (!keyf) { -+ fprintf(stderr, "Failed to open certificate: %s\n", -+ strerror(errno)); -+ exit(1); -+ } -+ -+ struct gmni_client_certificate *cert = -+ calloc(1, sizeof(struct gmni_client_certificate)); -+ if (gmni_ccert_load(cert, certf, keyf) != 0) { -+ fprintf(stderr, "Failed to load client certificate: %s\n", -+ strerror(errno)); -+ exit(1); -+ } -+ return cert; -+} -+ - int - main(int argc, char *argv[]) - { -@@ -165,7 +205,7 @@ } - } - break; - case 'E': -- assert(0); // TODO: Client certificates -+ opts.client_cert = load_client_cert(argv[0], optarg); - break; - case 'h': - usage(argv[0]); -@@ -226,7 +266,7 @@ - bool exit = false; - struct Curl_URL *url = curl_url(); - -- if(curl_url_set(url, CURLUPART_URL, argv[optind], 0) != CURLUE_OK) { -+ if (curl_url_set(url, CURLUPART_URL, argv[optind], 0) != CURLUE_OK) { - // TODO: Better error - fprintf(stderr, "Error: invalid URL\n"); - return 1; -@@ -238,8 +278,8 @@ char *buf; - curl_url_get(url, CURLUPART_URL, &buf, 0); - - struct gemini_response resp; -- enum gemini_result r = gemini_request( -- buf, &opts, &cfg.tofu, &resp); -+ enum gemini_result r = gemini_request(buf, -+ &opts, &cfg.tofu, &resp); - - free(buf); - -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 0ea492bb85fe024bd250c304808dcb8f0a915f90..aeb0c834d49c799fcdae54acfcbd3925dcedd392 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -1,5 +1,5 @@ - #include <assert.h> --#include <bearssl_ssl.h> -+#include <bearssl.h> - #include <ctype.h> - #include <errno.h> - #include <fcntl.h> -diff --git a/src/tofu.c b/src/tofu.c -index 570bd41c885cd9bb007b58bffca957ace6916bea..0acdf33a50bb4957741f4789472ffe56035a7159 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -1,6 +1,5 @@ - #include <assert.h> --#include <bearssl_hash.h> --#include <bearssl_x509.h> -+#include <bearssl.h> - #include <errno.h> - #include <gmni/gmni.h> - #include <gmni/tofu.h> -diff --git a/src/util.c b/src/util.c -index 780d0e8803ab9179683bd9a92cbead05624f2681..1cb0bf42b6319e6bd1b0cf0cc94e28627e4f9c68 100644 ---- a/src/util.c -+++ b/src/util.c -@@ -1,5 +1,5 @@ - #include <assert.h> --#include <bearssl_ssl.h> -+#include <bearssl.h> - #include <errno.h> - #include <gmni/gmni.h> - #include <libgen.h> diff --git a/sources/cgmnlm.git/commits/963700d8d6e31aecfc14e12184637f4c3360f6ed.patch b/sources/cgmnlm.git/commits/963700d8d6e31aecfc14e12184637f4c3360f6ed.patch @@ -1,22 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 7fba942066c9d9870f78311f050ef19dc42f7ce1..def42ac8b1af405da29681d29cd7b9d07b727f11 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -69,7 +69,7 @@ const char *help_msg = - "The following commands are available:\n\n" - "q\tQuit\n" - "N\tFollow Nth link (where N is a number)\n" -- "p[N]\tShow URL of Nth link (where N is a number)\n" -+ "p N\tShow URL of Nth link (where N is a number)\n" - "b\tBack (in the page history)\n" - "f\tForward (in the page history)\n" - "H\tView all page history\n" -@@ -303,7 +303,7 @@ result = PROMPT_AGAIN; - goto exit; - } - case 'p': -- if (!in[1]) break; -+ if (!isspace(in[1])) break; - struct link *link = browser->links; - char *endptr; - int linksel = (int)strtol(in+1, &endptr, 10); diff --git a/sources/cgmnlm.git/commits/996bd24225e7a63fd160d1feb9af193225a065b3.patch b/sources/cgmnlm.git/commits/996bd24225e7a63fd160d1feb9af193225a065b3.patch @@ -1,14 +0,0 @@ -diff --git a/src/tofu.c b/src/tofu.c -index aefac17cf61674d5c40d9554d0cb9089b1519ba4..50a295870876265849eae7fdf0de926a3ad309fd 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -63,6 +63,9 @@ if (err != 0 && err != BR_ERR_X509_TRUNCATED) { - cc->err = err; - return; - } -+ if (br_x509_decoder_isCA(&cc->decoder) && cc->pkey) { -+ return; -+ } - cc->pkey = br_x509_decoder_get_pkey(&cc->decoder); - br_sha512_out(&cc->sha512, &cc->hash); - } diff --git a/sources/cgmnlm.git/commits/9a195d92566b5790b2b7d3ca848987a095bf3d9c.patch b/sources/cgmnlm.git/commits/9a195d92566b5790b2b7d3ca848987a095bf3d9c.patch @@ -1,13 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index a0bc81f851a69ec34c7e9baf643c10728632c7e2..4cd601913caa3fb307de28cfbe861689f933eaf5 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -88,7 +88,7 @@ "b\t\tBack (in the page history)\n" - "f\t\tForward (in the page history)\n" - "H\t\tView all page history\n" - "a\t\tSave bookmark\n" -- "s\tRemove bookmark for current URL\n" -+ "s\t\tRemove bookmark for current URL\n" - "B\t\tBrowse bookmarks\n" - "r\t\tReload the page\n" - "/<text>\t\tsearch for text (POSIX regular expression)\n" diff --git a/sources/cgmnlm.git/commits/9b0006509931c9a3defb64c64f4b0071657f8e61.patch b/sources/cgmnlm.git/commits/9b0006509931c9a3defb64c64f4b0071657f8e61.patch @@ -1,31 +0,0 @@ -diff --git a/src/tofu.c b/src/tofu.c -index 5b34850de9e17dcd59fc64a3227cd92bfb0a6cef..570bd41c885cd9bb007b58bffca957ace6916bea 100644 ---- a/src/tofu.c -+++ b/src/tofu.c -@@ -25,7 +25,7 @@ static void - xt_start_cert(const br_x509_class **ctx, uint32_t length) - { - struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; -- if (cc->err != 0) { -+ if (cc->err != 0 || cc->pkey) { - return; - } - if (length == 0) { -@@ -40,7 +40,7 @@ static void - xt_append(const br_x509_class **ctx, const unsigned char *buf, size_t len) - { - struct x509_tofu_context *cc = (struct x509_tofu_context *)(void *)ctx; -- if (cc->err != 0) { -+ if (cc->err != 0 || cc->pkey) { - return; - } - br_x509_decoder_push(&cc->decoder, buf, len); -@@ -63,7 +63,7 @@ if (err != 0 && err != BR_ERR_X509_TRUNCATED) { - cc->err = err; - return; - } -- if (br_x509_decoder_isCA(&cc->decoder) && cc->pkey) { -+ if (br_x509_decoder_isCA(&cc->decoder)) { - return; - } - cc->pkey = br_x509_decoder_get_pkey(&cc->decoder); diff --git a/sources/cgmnlm.git/commits/9b1a618b4211a029c352c72f6d273e3085c8457d.patch b/sources/cgmnlm.git/commits/9b1a618b4211a029c352c72f6d273e3085c8457d.patch @@ -1,221 +0,0 @@ -diff --git a/include/client.h b/include/client.h -index dbd73234311331ea52650d59dbd9cf535b73298b..f711eea91c3842ac29a884b38e0d7a0ab99cb7fb 100644 ---- a/include/client.h -+++ b/include/client.h -@@ -46,6 +46,7 @@ GEMINI_ERR_CONNECT, - // use SSL_get_error(resp->ssl, resp->status) to get details - GEMINI_ERR_SSL, - GEMINI_ERR_IO, -+ GEMINI_ERR_PROTOCOL, - }; - - // Requests the specified URL via the gemini protocol. If options is non-NULL, -@@ -63,5 +64,8 @@ // allocated during the request. If you intend to re-use the SSL_CTX provided by - // gemini_options, set the ctx pointer to NULL before calling - // gemini_response_finish. - void gemini_response_finish(struct gemini_response *resp); -+ -+// Returns a user-friendly string describing an error. -+const char *gemini_strerr(enum gemini_result r, struct gemini_response *resp); - - #endif -diff --git a/src/client.c b/src/client.c -index 5f2debb52c310f88e82ad83ca2251ba233d6a704..67671ccca2a25a460681bdffacb981c7f26c1407 100644 ---- a/src/client.c -+++ b/src/client.c -@@ -2,6 +2,7 @@ #include <assert.h> - #include <errno.h> - #include <netdb.h> - #include <openssl/bio.h> -+#include <openssl/err.h> - #include <openssl/ssl.h> - #include <stdlib.h> - #include <string.h> -@@ -168,6 +169,21 @@ res = GEMINI_ERR_IO; - goto cleanup; - } - -+ if (r < 3 || strcmp(&buf[r - 2], "\r\n") != 0) { -+ res = GEMINI_ERR_PROTOCOL; -+ goto cleanup; -+ } -+ -+ char *endptr; -+ resp->status = (int)strtol(buf, &endptr, 10); -+ if (*endptr != ' ' || resp->status <= 10 || resp->status >= 70) { -+ res = GEMINI_ERR_PROTOCOL; -+ goto cleanup; -+ } -+ resp->meta = calloc(r - 5 /* 2 digits, space, and CRLF */ + 1 /* NUL */, 1); -+ strncpy(resp->meta, &endptr[1], r - 5); -+ resp->meta[r - 5] = '\0'; -+ - cleanup: - curl_url_cleanup(uri); - return res; -@@ -188,3 +204,29 @@ SSL_free(resp->ssl); - SSL_CTX_free(resp->ssl_ctx); - free(resp->meta); - } -+ -+const char * -+gemini_strerr(enum gemini_result r, struct gemini_response *resp) -+{ -+ switch (r) { -+ case GEMINI_OK: -+ return "OK"; -+ case GEMINI_ERR_OOM: -+ return "Out of memory"; -+ case GEMINI_ERR_INVALID_URL: -+ return "Invalid URL"; -+ case GEMINI_ERR_RESOLVE: -+ return gai_strerror(resp->status); -+ case GEMINI_ERR_CONNECT: -+ return strerror(errno); -+ case GEMINI_ERR_SSL: -+ return ERR_error_string( -+ SSL_get_error(resp->ssl, resp->status), -+ NULL); -+ case GEMINI_ERR_IO: -+ return "I/O error"; -+ case GEMINI_ERR_PROTOCOL: -+ return "Protocol error"; -+ } -+ assert(0); -+} -diff --git a/src/gmnic.c b/src/gmnic.c -index 7b2eb183ebfe67dce0aaf8c6010bf6785b503df6..014211dc3784ba3eadcf1885ef7bcf6fe84d8462 100644 ---- a/src/gmnic.c -+++ b/src/gmnic.c -@@ -1,9 +1,13 @@ - #include <assert.h> -+#include <errno.h> - #include <getopt.h> -+#include <openssl/bio.h> - #include <openssl/err.h> - #include <stdbool.h> - #include <stdio.h> - #include <stdlib.h> -+#include <string.h> -+#include <unistd.h> - #include "client.h" - - static void -@@ -17,11 +21,15 @@ - int - main(int argc, char *argv[]) - { -- bool headers = false, follow_redirect = false; -- char *certificate = NULL, *input = NULL; -+ enum header_mode { -+ OMIT_HEADERS, -+ SHOW_HEADERS, -+ ONLY_HEADERS, -+ }; -+ enum header_mode headers = OMIT_HEADERS; - - int c; -- while ((c = getopt(argc, argv, "46C:d:hLI")) != -1) { -+ while ((c = getopt(argc, argv, "46C:d:hLiI")) != -1) { - switch (c) { - case '4': - assert(0); // TODO -@@ -30,25 +38,29 @@ case '6': - assert(0); // TODO - break; - case 'C': -- certificate = optarg; -+ assert(0); // TODO: Client certificates - break; - case 'd': -- input = optarg; -+ assert(0); // TODO: Input - break; - case 'h': - usage(argv[0]); - return 0; - case 'L': -- follow_redirect = true; -+ assert(0); // TODO: Follow redirects -+ break; -+ case 'i': -+ headers = SHOW_HEADERS; - break; - case 'I': -- headers = true; -+ headers = ONLY_HEADERS; - break; - default: - fprintf(stderr, "fatal: unknown flag %c", c); - return 1; - } - } -+ - if (optind != argc - 1) { - usage(argv[0]); - return 1; -@@ -59,33 +71,41 @@ ERR_load_crypto_strings(); - - struct gemini_response resp; - enum gemini_result r = gemini_request(argv[optind], NULL, &resp); -- switch (r) { -- case GEMINI_OK: -- printf("OK\n"); -- break; -- case GEMINI_ERR_OOM: -- printf("OOM\n"); -- break; -- case GEMINI_ERR_INVALID_URL: -- printf("INVALID_URL\n"); -- break; -- case GEMINI_ERR_RESOLVE: -- printf("RESOLVE\n"); -- break; -- case GEMINI_ERR_CONNECT: -- printf("CONNECT\n"); -+ if (r != GEMINI_OK) { -+ fprintf(stderr, "Error: %s\n", gemini_strerr(r, &resp)); -+ gemini_response_finish(&resp); -+ return (int)r; -+ } -+ -+ switch (headers) { -+ case ONLY_HEADERS: -+ printf("%d %s\n", resp.status, resp.meta); - break; -- case GEMINI_ERR_SSL: -- fprintf(stderr, "SSL error: %s\n", ERR_error_string( -- SSL_get_error(resp.ssl, resp.status), NULL)); -+ case SHOW_HEADERS: -+ printf("%d %s\n", resp.status, resp.meta); -+ /* fallthrough */ -+ case OMIT_HEADERS: -+ for (int n = 1; n > 0;) { -+ char buf[BUFSIZ]; -+ n = BIO_read(resp.bio, buf, BUFSIZ); -+ if (n == -1) { -+ fprintf(stderr, "Error: read\n"); -+ return 1; -+ } -+ ssize_t w = 0; -+ while (w < (ssize_t)n) { -+ ssize_t x = write(STDOUT_FILENO, &buf[w], n - w); -+ if (x == -1) { -+ fprintf(stderr, "Error: write: %s\n", -+ strerror(errno)); -+ return 1; -+ } -+ w += x; -+ } -+ } - break; - } - - gemini_response_finish(&resp); -- -- (void)headers; -- (void)follow_redirect; -- (void)certificate; -- (void)input; - return 0; - } diff --git a/sources/cgmnlm.git/commits/9bd1a7457ea58ddd568fdbe46a1155c28424e8be.patch b/sources/cgmnlm.git/commits/9bd1a7457ea58ddd568fdbe46a1155c28424e8be.patch @@ -1,12 +0,0 @@ -diff --git a/src/parser.c b/src/parser.c -index ffcc28767be7d638d18609764999d004790aa9a2..eb9aa5ec3d1da4a222e9ec0c76ad19ba004b9a2c 100644 ---- a/src/parser.c -+++ b/src/parser.c -@@ -58,7 +58,6 @@ if ((end = strstr(p->buf, "\n")) != NULL) { - *end = 0; - } - -- // TODO: Provide whitespace trimming helper function - if (strncmp(p->buf, "=>", 2) == 0) { - tok->token = GEMINI_LINK; - int i = 2; diff --git a/sources/cgmnlm.git/commits/9ddd5c16dae4b556c7aeac88c219568c479d87f2.patch b/sources/cgmnlm.git/commits/9ddd5c16dae4b556c7aeac88c219568c479d87f2.patch @@ -1,363 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index d05180c7a74c88bd7ed8ba551a4a522bb8b74009..9bc95fad89c0f92bc5a68fdd40d272b021c3cc25 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -263,6 +263,167 @@ putc('\n', out); - } - } - -+static char * -+get_input(const struct gemini_response *resp, FILE *source) -+{ -+ int r = 0; -+ struct termios attrs; -+ bool tty = fileno(source) != -1 && isatty(fileno(source)); -+ char *input = NULL; -+ if (tty) { -+ fprintf(stderr, "%s: ", resp->meta); -+ if (resp->status == GEMINI_STATUS_SENSITIVE_INPUT) { -+ r = tcgetattr(fileno(source), &attrs); -+ struct termios new_attrs; -+ r = tcgetattr(fileno(source), &new_attrs); -+ if (r != -1) { -+ new_attrs.c_lflag &= ~ECHO; -+ tcsetattr(fileno(source), TCSANOW, &new_attrs); -+ } -+ } -+ } -+ size_t s = 0; -+ ssize_t n = getline(&input, &s, source); -+ if (n == -1) { -+ fprintf(stderr, "Error reading input: %s\n", -+ feof(source) ? "EOF" : strerror(ferror(source))); -+ return NULL; -+ } -+ input[n - 1] = '\0'; // Drop LF -+ if (tty && resp->status == GEMINI_STATUS_SENSITIVE_INPUT && r != -1) { -+ attrs.c_lflag &= ~ECHO; -+ tcsetattr(fileno(source), TCSANOW, &attrs); -+ } -+ 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 enum gemini_result -+do_requests(struct browser *browser, struct gemini_response *resp) -+{ -+ int nredir = 0; -+ bool requesting = true; -+ enum gemini_result res; -+ while (requesting) { -+ char *scheme; -+ CURLUcode uc = curl_url_get(browser->url, -+ CURLUPART_SCHEME, &scheme, 0); -+ assert(uc == CURLUE_OK); // Invariant -+ if (strcmp(scheme, "file") == 0) { -+ free(scheme); -+ 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; -+ } -+ -+ FILE *fp = fopen(path, "r"); -+ if (!fp) { -+ 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->bio = NULL; -+ resp->ssl = NULL; -+ resp->ssl_ctx = NULL; -+ resp->meta = NULL; -+ resp->fd = -1; -+ free(path); -+ break; -+ } -+ -+ BIO *file = BIO_new_fp(fp, BIO_CLOSE); -+ resp->bio = BIO_new(BIO_f_buffer()); -+ BIO_push(resp->bio, file); -+ 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 = -1; -+ resp->ssl = NULL; -+ resp->ssl_ctx = NULL; -+ return GEMINI_OK; -+ } -+ free(scheme); -+ -+ res = gemini_request(browser->plain_url, &browser->opts, resp); -+ if (res != GEMINI_OK) { -+ fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); -+ requesting = false; -+ resp->status = 70 + res; -+ break; -+ } -+ -+ char *input; -+ switch (gemini_response_class(resp->status)) { -+ case GEMINI_STATUS_CLASS_INPUT: -+ input = get_input(resp, browser->tty); -+ if (!input) { -+ requesting = false; -+ break; -+ } -+ if (input[0] == '\0' && browser->history->prev) { -+ free(input); -+ browser->history = browser->history->prev; -+ set_url(browser, browser->history->url, NULL); -+ break; -+ } -+ -+ char *new_url = gemini_input_url( -+ browser->plain_url, input); -+ free(input); -+ assert(new_url); -+ set_url(browser, new_url, NULL); -+ free(new_url); -+ break; -+ case GEMINI_STATUS_CLASS_REDIRECT: -+ if (++nredir >= 5) { -+ requesting = false; -+ fprintf(stderr, "Error: maximum redirects (5) exceeded\n"); -+ break; -+ } -+ set_url(browser, resp->meta, NULL); -+ break; -+ case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: -+ assert(0); // TODO -+ case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: -+ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: -+ requesting = false; -+ fprintf(stderr, "Server returned %s %d %s\n", -+ resp->status / 10 == 4 ? -+ "TEMPORARY FAILURE" : "PERMANENT FALIURE", -+ resp->status, resp->meta); -+ break; -+ case GEMINI_STATUS_CLASS_SUCCESS: -+ return res; -+ } -+ -+ if (requesting) { -+ gemini_response_finish(resp); -+ } -+ } -+ -+ return res; -+} -+ - static enum prompt_result - do_prompts(const char *prompt, struct browser *browser) - { -@@ -678,6 +839,9 @@ - static bool - display_response(struct browser *browser, struct gemini_response *resp) - { -+ if (gemini_response_class(resp->status) != GEMINI_STATUS_CLASS_SUCCESS) { -+ return false; -+ } - if (strcmp(resp->meta, "text/gemini") == 0 - || strncmp(resp->meta, "text/gemini;", 12) == 0) { - return display_gemini(browser, resp); -@@ -688,170 +852,6 @@ } - assert(0); // TODO: Deal with other mimetypes - } - --static char * --get_input(const struct gemini_response *resp, FILE *source) --{ -- int r = 0; -- struct termios attrs; -- bool tty = fileno(source) != -1 && isatty(fileno(source)); -- char *input = NULL; -- if (tty) { -- fprintf(stderr, "%s: ", resp->meta); -- if (resp->status == GEMINI_STATUS_SENSITIVE_INPUT) { -- r = tcgetattr(fileno(source), &attrs); -- struct termios new_attrs; -- r = tcgetattr(fileno(source), &new_attrs); -- if (r != -1) { -- new_attrs.c_lflag &= ~ECHO; -- tcsetattr(fileno(source), TCSANOW, &new_attrs); -- } -- } -- } -- size_t s = 0; -- ssize_t n = getline(&input, &s, source); -- if (n == -1) { -- fprintf(stderr, "Error reading input: %s\n", -- feof(source) ? "EOF" : strerror(ferror(source))); -- return NULL; -- } -- input[n - 1] = '\0'; // Drop LF -- if (tty && resp->status == GEMINI_STATUS_SENSITIVE_INPUT && r != -1) { -- attrs.c_lflag &= ~ECHO; -- tcsetattr(fileno(source), TCSANOW, &attrs); -- } -- 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; --} -- --// Returns true to skip prompting --static bool --do_requests(struct browser *browser, struct gemini_response *resp) --{ -- int nredir = 0; -- bool requesting = true; -- while (requesting) { -- char *scheme; -- CURLUcode uc = curl_url_get(browser->url, -- CURLUPART_SCHEME, &scheme, 0); -- assert(uc == CURLUE_OK); // Invariant -- if (strcmp(scheme, "file") == 0) { -- free(scheme); -- 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; -- } -- -- FILE *fp = fopen(path, "r"); -- if (!fp) { -- 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->bio = NULL; -- resp->ssl = NULL; -- resp->ssl_ctx = NULL; -- resp->meta = NULL; -- resp->fd = -1; -- free(path); -- break; -- } -- -- BIO *file = BIO_new_fp(fp, BIO_CLOSE); -- resp->bio = BIO_new(BIO_f_buffer()); -- BIO_push(resp->bio, file); -- 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 = -1; -- resp->ssl = NULL; -- resp->ssl_ctx = NULL; -- return display_response(browser, resp); -- } -- free(scheme); -- -- enum gemini_result res = gemini_request(browser->plain_url, -- &browser->opts, resp); -- if (res != GEMINI_OK) { -- fprintf(stderr, "Error: %s\n", gemini_strerr(res, resp)); -- requesting = false; -- resp->status = 70 + res; -- break; -- } -- -- char *input; -- switch (gemini_response_class(resp->status)) { -- case GEMINI_STATUS_CLASS_INPUT: -- input = get_input(resp, browser->tty); -- if (!input) { -- requesting = false; -- break; -- } -- if (input[0] == '\0' && browser->history->prev) { -- free(input); -- browser->history = browser->history->prev; -- set_url(browser, browser->history->url, NULL); -- break; -- } -- -- char *new_url = gemini_input_url( -- browser->plain_url, input); -- free(input); -- assert(new_url); -- set_url(browser, new_url, NULL); -- free(new_url); -- break; -- case GEMINI_STATUS_CLASS_REDIRECT: -- if (++nredir >= 5) { -- requesting = false; -- fprintf(stderr, "Error: maximum redirects (5) exceeded\n"); -- break; -- } -- fprintf(stderr, "Following redirect to %s\n", resp->meta); -- set_url(browser, resp->meta, NULL); -- break; -- case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED: -- assert(0); // TODO -- case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE: -- case GEMINI_STATUS_CLASS_PERMANENT_FAILURE: -- requesting = false; -- fprintf(stderr, "Server returned %s %d %s\n", -- resp->status / 10 == 4 ? -- "TEMPORARY FAILURE" : "PERMANENT FALIURE", -- resp->status, resp->meta); -- break; -- case GEMINI_STATUS_CLASS_SUCCESS: -- requesting = false; -- return display_response(browser, resp); -- } -- -- if (requesting) { -- gemini_response_finish(resp); -- } -- } -- -- return false; --} -- - static enum tofu_action - tofu_callback(enum tofu_error error, const char *fingerprint, - struct known_host *khost, void *data) -@@ -1001,7 +1001,8 @@ struct gemini_response resp; - browser.running = true; - while (browser.running) { - static char prompt[4096]; -- bool skip_prompt = do_requests(&browser, &resp); -+ bool skip_prompt = do_requests(&browser, &resp) == GEMINI_OK -+ && display_response(&browser, &resp); - if (browser.meta) { - free(browser.meta); - } diff --git a/sources/cgmnlm.git/commits/9ef33fb102426d0bf56e93ceebcd81eb24171a9e.patch b/sources/cgmnlm.git/commits/9ef33fb102426d0bf56e93ceebcd81eb24171a9e.patch @@ -1,108 +0,0 @@ -diff --git a/README.md b/README.md -index 80695023b563475331438ce368d70f77ff6f24ca..eabc77ea5a6dd0f73bd908fa5a52b03fd53ddcce 100644 ---- a/README.md -+++ b/README.md -@@ -28,7 +28,7 @@ - heading 3: light green - - gemini link on same capsule: light cyan - - gemini link to another capsule: dark cyan - - non-gemini link: light magenta --- quote: light gray -+- preformatted text: light gray - - Besides this rendering adjustments i'll try to keep track of upstream changes or send patches to upstream. - -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 8e85d091e54e3c3c11dbd6980ae660be6838c7c8..66db54c7e0cf7dd6a1f58872621cc28e69944134 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -23,7 +23,7 @@ #define ANSI_COLOR_RED "\x1b[91m" - #define ANSI_COLOR_GREEN "\x1b[92m" - #define ANSI_COLOR_YELLOW "\x1b[93m" - #define ANSI_COLOR_BLUE "\x1b[94m" --#define ANSI_COLOR_MAGENTA "\x1b[95m" -+#define ANSI_COLOR_MAGENTA "\x1b[35m" - #define ANSI_COLOR_CYAN "\x1b[36m" - #define ANSI_COLOR_LCYAN "\x1b[96m" - #define ANSI_COLOR_GRAY "\x1b[37m" -@@ -70,7 +70,7 @@ - const char *default_bookmarks = - "# Welcome to cgmnlm\n\n" - "Links:\n\n" -- "=> https://src.clttr.info/rwa/cgmnlm The cgmnlm browser\n" -+ "=> https://gmn.clttr.info/cgmnln.gmi The colorful line mode client\n" - "=> gemini://gemini.circumlunar.space The gemini protocol\n\n" - "This file can be found at %s and may be edited at your pleasure.\n\n" - "Bookmarks:\n" -@@ -809,20 +809,20 @@ while (text != NULL || gemini_parser_next(&p, &tok) == 0) { - repeat: - switch (tok.token) { - case GEMINI_TEXT: -- col += fprintf(out, " "); -+ col += fprintf(out, " "); - if (text == NULL) { - text = tok.text; - } - break; - case GEMINI_LINK: - if (text == NULL) { -- col += fprintf(out, "%2d) %s", nlinks++, (!strncmp("gemini://", tok.link.url, 9)) ? ANSI_COLOR_CYAN : ((strstr(tok.link.url, "://") == NULL) ? ANSI_COLOR_LCYAN : ANSI_COLOR_MAGENTA)); -+ col += fprintf(out, "%3d) %s", nlinks++, (!strncmp("gemini://", tok.link.url, 9)) ? ANSI_COLOR_CYAN : ((strstr(tok.link.url, "://") == NULL) ? ANSI_COLOR_LCYAN : ANSI_COLOR_MAGENTA)); - text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); - *next = calloc(1, sizeof(struct link)); - (*next)->url = strdup(trim_ws(tok.link.url)); - next = &(*next)->next; - } else { -- col += fprintf(out, " "); -+ col += fprintf(out, " "); - } - break; - case GEMINI_PREFORMATTED_BEGIN: -@@ -832,7 +832,7 @@ case GEMINI_PREFORMATTED_END: - continue; // Not used - case GEMINI_PREFORMATTED_TEXT: - if (text == NULL) { -- fprintf(out, " "); -+ fprintf(out, " %s", ANSI_COLOR_GRAY); - text = tok.preformatted; - } - break; -@@ -843,32 +843,31 @@ } - if (text == NULL) { - switch (tok.heading.level) { - case 1: -- col += fprintf(out, "%s%s", " # ", ANSI_COLOR_RED); -+ col += fprintf(out, " # %s", ANSI_COLOR_RED); - break; - case 2: -- col += fprintf(out, "%s%s", " ## ", ANSI_COLOR_YELLOW); -+ col += fprintf(out, " ## %s", ANSI_COLOR_YELLOW); - break; - case 3: -- col += fprintf(out, "%s%s", "### ", ANSI_COLOR_GREEN); -+ col += fprintf(out, " ### %s", ANSI_COLOR_GREEN); - break; - } - text = trim_ws(tok.heading.title); - } else { -- col += fprintf(out, " "); -+ col += fprintf(out, " "); - } - break; - case GEMINI_LIST_ITEM: - if (text == NULL) { -- col += fprintf(out, " %s ", -+ col += fprintf(out, " %s ", - browser->unicode ? "•" : "*"); - text = trim_ws(tok.list_item); - } else { -- col += fprintf(out, " "); -+ col += fprintf(out, " "); - } - break; - case GEMINI_QUOTE: -- col += fprintf(out, " %s%s %s", ANSI_COLOR_RESET, -- browser->unicode ? "┃" : ">", ANSI_COLOR_GRAY); -+ col += fprintf(out, " %s ", browser->unicode ? "┃" : ">"); - if (text == NULL) { - text = trim_ws(tok.quote_text); - } diff --git a/sources/cgmnlm.git/commits/9f98e013a6cd966cf4dc2d98187d6f0ba6f7fb5c.patch b/sources/cgmnlm.git/commits/9f98e013a6cd966cf4dc2d98187d6f0ba6f7fb5c.patch @@ -1,27 +0,0 @@ -diff --git a/config.sh b/config.sh -index 52931ab241c3177285b56258bf9b67ac4b63a7ea..2a94bc6feada62bcfda616fa573d0a12db0a4502 100644 ---- a/config.sh -+++ b/config.sh -@@ -88,6 +88,8 @@ CFLAGS="$CFLAGS $(pkg-config --cflags "$pc")" - LIBS="$LIBS $(pkg-config --libs "$pc")" - } - -+docs() { true; } -+ - run_configure() { - mkdir -p $outdir - -@@ -133,8 +135,11 @@ CFLAGS+=-DLIBDIR='"\$(LIBDIR)"' - - all: ${all} - EOF -- gmni >>"$outdir"/config.mk -- gmnlm >>"$outdir"/config.mk -+ -+ for target in $all -+ do -+ $target >>"$outdir"/config.mk -+ done - echo done - - touch $outdir/cppcache diff --git a/sources/cgmnlm.git/commits/a3d5169d71f181efaa59a619e7362911a6c048b7.patch b/sources/cgmnlm.git/commits/a3d5169d71f181efaa59a619e7362911a6c048b7.patch @@ -1,28 +0,0 @@ -diff --git a/README.md b/README.md -new file mode 100644 -index 0000000000000000000000000000000000000000..c08d5b2e62d4ec740ffda4acfccbc198ad9726d4 ---- /dev/null -+++ b/README.md -@@ -0,0 +1,22 @@ -+# gmni - A Gemini client -+ -+This is a [Gemini](https://gemini.circumlunar.space/) client. -+ -+Dependencies: -+ -+- A POSIX-like system and a C11 compiler -+- OpenSSL -+- [scdoc](https://sr.ht/~sircmpwn/scdoc/) (optional) -+ -+## Compiling -+ -+``` -+$ mkdir build && cd build -+$ ../configure -+$ make -+# make install -+``` -+ -+## Usage -+ -+See `gmni(1)` for the CLI usage. diff --git a/sources/cgmnlm.git/commits/a5eae7ea6b35f7b2540fefdf4613a86916f0a0b0.patch b/sources/cgmnlm.git/commits/a5eae7ea6b35f7b2540fefdf4613a86916f0a0b0.patch @@ -1,20 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index b4f20de66d50fe9f0287c74b9a9292fe8ddd48d3..0546486340b7299be3c97266dd534a1354bf31a3 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -188,6 +188,15 @@ { - char *path_fmt = get_data_pathfmt(); - static char path[PATH_MAX+1]; - snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ if (mkdirs(dirname(path), 0755) != 0) { -+ snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); -+ free(path_fmt); -+ fprintf(stderr, "Error creating directory %s: %s\n", -+ dirname(path), strerror(errno)); -+ return; -+ } -+ -+ snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi"); - free(path_fmt); - - struct stat buf; diff --git a/sources/cgmnlm.git/commits/a61a75f837239bed3aa74331699d301fb93d9da8.patch b/sources/cgmnlm.git/commits/a61a75f837239bed3aa74331699d301fb93d9da8.patch @@ -1,84 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 5d6ce3cc5ff97884bb10c2e9e7f6e870b5d2f54a..e6910607646fdd4f4d5a04a052715eda35147c30 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -146,6 +146,45 @@ for (; *in && isspace(*in); ++in); - return in; - } - -+static int -+wrap(FILE *f, char *s, struct winsize *ws, int *row, int *col) -+{ -+ if (!s[0]) { -+ fprintf(f, "\n"); -+ return 0; -+ } -+ for (int i = 0; s[i]; ++i) { -+ // TODO: Other control sequences, and eat ANSI escapes before -+ // they become a problem -+ switch (s[i]) { -+ case '\n': -+ assert(0); // Not supposed to happen -+ case '\t': -+ *col = *col + (8 - *col % 8); -+ break; -+ default: -+ *col += 1; -+ break; -+ } -+ -+ if (*col >= ws->ws_col) { -+ int j = i--; -+ while (&s[i] != s && !isspace(s[i])) --i; -+ if (&s[i] == s) { -+ i = j; -+ } -+ char c = s[i]; -+ s[i] = 0; -+ int n = fprintf(f, "%s\n", s); -+ s[i] = c; -+ *row += 1; -+ *col = 0; -+ return n; -+ } -+ } -+ return fprintf(f, "%s\n", s) - 1; -+} -+ - static bool - display_gemini(struct browser *browser, struct gemini_response *resp) - { -@@ -157,15 +196,29 @@ - struct winsize ws; - ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); - -+ char *text = NULL; - int row = 0, col = 0; - struct gemini_token tok; - struct link **next = &browser->links; -- while (gemini_parser_next(&p, &tok) == 0) { -+ while (text != NULL || gemini_parser_next(&p, &tok) == 0) { - switch (tok.token) { - case GEMINI_TEXT: -- // TODO: word wrap -- col += fprintf(browser->tty, " %s\n", -- trim_ws(tok.text)); -+ if (text == NULL) { -+ text = tok.text; -+ } -+ -+ do { -+ col += fprintf(browser->tty, " "); -+ int w = wrap(browser->tty, text, &ws, &row, &col); -+ text += w; -+ if (row >= ws.ws_row - 4) { -+ break; -+ } -+ } while (text[0]); -+ -+ if (!text[0]) { -+ text = NULL; -+ } - break; - case GEMINI_LINK: - col += fprintf(browser->tty, "%d) %s\n", nlinks++, diff --git a/sources/cgmnlm.git/commits/a6e0326291eee1e1f8ee723ac1e8467ed0561e86.patch b/sources/cgmnlm.git/commits/a6e0326291eee1e1f8ee723ac1e8467ed0561e86.patch @@ -1,37 +0,0 @@ -diff --git a/README.md b/README.md -index 9564df898ce60d9895b818f95ae020c39edb83af..80695023b563475331438ce368d70f77ff6f24ca 100644 ---- a/README.md -+++ b/README.md -@@ -25,7 +25,8 @@ The actual colors used depend on your terminal palette: - - heading 1: light red - - heading 2: light yellow - - heading 3: light green --- gemini link: light cyan -+- gemini link on same capsule: light cyan -+- gemini link to another capsule: dark cyan - - non-gemini link: light magenta - - quote: light gray - -diff --git a/src/cgmnlm.c b/src/cgmnlm.c -index 39be8ad24bdf97f87c3e51f4b5ce92e8bf1b421b..a021a97298bb41bff8cdbbceca172f561b64c7a5 100644 ---- a/src/cgmnlm.c -+++ b/src/cgmnlm.c -@@ -24,7 +24,8 @@ #define ANSI_COLOR_GREEN "\x1b[92m" - #define ANSI_COLOR_YELLOW "\x1b[93m" - #define ANSI_COLOR_BLUE "\x1b[94m" - #define ANSI_COLOR_MAGENTA "\x1b[95m" --#define ANSI_COLOR_CYAN "\x1b[96m" -+#define ANSI_COLOR_CYAN "\x1b[36m" -+#define ANSI_COLOR_LCYAN "\x1b[96m" - #define ANSI_COLOR_GRAY "\x1b[37m" - #define ANSI_COLOR_RESET "\x1b[0m" - -@@ -815,7 +816,7 @@ } - break; - case GEMINI_LINK: - if (text == NULL) { -- col += fprintf(out, "%2d) %s", nlinks++, (!strncmp("gemini://", tok.link.url, 9) || strstr(tok.link.url, "://") == NULL) ? ANSI_COLOR_CYAN : ANSI_COLOR_MAGENTA); -+ col += fprintf(out, "%2d) %s", nlinks++, (!strncmp("gemini://", tok.link.url, 9)) ? ANSI_COLOR_CYAN : ((strstr(tok.link.url, "://") == NULL) ? ANSI_COLOR_LCYAN : ANSI_COLOR_MAGENTA)); - text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); - *next = calloc(1, sizeof(struct link)); - (*next)->url = strdup(trim_ws(tok.link.url)); diff --git a/sources/cgmnlm.git/commits/ab66dd2be92931bef04cbccdb3aa008615bd8eba.patch b/sources/cgmnlm.git/commits/ab66dd2be92931bef04cbccdb3aa008615bd8eba.patch @@ -1,19 +0,0 @@ -diff --git a/src/util.c b/src/util.c -index 573e8a717efbe843fce003126cb932928b15f716..2f62c29ce40ac012993e1d208d65491b56d204aa 100644 ---- a/src/util.c -+++ b/src/util.c -@@ -19,13 +19,8 @@ - assert(strlen(path) <= PATH_MAX); - - strcpy(p, path); -- t = dirname(path); -+ t = dirname(p); - memmove(dname, t, strlen(t) + 1); -- -- /* restore the path if dirname worked in-place */ -- if (t == path && path != dname) { -- strcpy(path, p); -- } - } - - /** Make directory and all of its parents */ diff --git a/sources/cgmnlm.git/commits/abcb9caf86020a7cdd9f502fe01eb5db3c70c685.patch b/sources/cgmnlm.git/commits/abcb9caf86020a7cdd9f502fe01eb5db3c70c685.patch @@ -1,2412 +0,0 @@ -diff --git a/config.sh b/config.sh -index fc1134c0c98d9f57cb2cfdbb3b9d6faf35001dfd..70c4489bf5fd48fc0eb636bf739a5d82dcff8663 100644 ---- a/config.sh -+++ b/config.sh -@@ -5,6 +5,7 @@ AS=${AS:-as} - CC=${CC:-cc} - CFLAGS=${CFLAGS:-} - LD=${LD:-ld} -+LIBSSL= - - for arg - do -@@ -12,6 +13,9 @@ # TODO: Add args for install directories - case "$arg" in - --prefix=*) - PREFIX=${arg#*=} -+ ;; -+ --with-libssl=*) -+ LIBSSL=${arg#*=} - ;; - esac - done -@@ -72,12 +76,27 @@ return 1 - fi - } - -+find_library() { -+ name="$1" -+ pc="$2" -+ printf "Checking for %s... " "$name" -+ if ! pkg-config "$pc" 2>/dev/null -+ then -+ printf "NOT FOUND\n" -+ printf "Tried pkg-config %s\n" "$pc" -+ return 1 -+ fi -+ printf "OK\n" -+ CFLAGS="$CFLAGS $(pkg-config --cflags "$pc")" -+ LIBS="$LIBS $(pkg-config --libs "$pc")" -+} -+ - run_configure() { - mkdir -p $outdir - - for flag in -g -std=c11 -D_XOPEN_SOURCE=700 -Wall -Wextra -Werror -pedantic - do -- printf "Checking for $flag... " -+ printf "Checking for %s... " "$flag" - if test_cflags "$flag" - then - echo yes -@@ -86,9 +105,13 @@ echo no - fi - done - -+ find_library OpenSSL libssl -+ find_library OpenSSL libcrypto -+ - printf "Creating $outdir/config.mk... " - cat <<-EOF > "$outdir"/config.mk - CC=$CC -+ LIBS=$LIBS - PREFIX=${PREFIX:-/usr/local} - OUTDIR=${outdir} - _INSTDIR=\$(DESTDIR)\$(PREFIX) -diff --git a/configure b/configure -index 7b1a48b785b7ab1a8811cb36ea9ee00a68fc9ff8..680b57fc9e319c2707e0cc2ded0bc22597544d78 100755 ---- a/configure -+++ b/configure -@@ -4,7 +4,10 @@ eval ". $srcdir/config.sh" - - gmni() { - genrules gmnic \ -- src/gmnic.c -+ src/client.c \ -+ src/escape.c \ -+ src/gmnic.c \ -+ src/url.c - } - - all="gmnic" -diff --git a/include/client.h b/include/client.h -new file mode 100644 -index 0000000000000000000000000000000000000000..dbd73234311331ea52650d59dbd9cf535b73298b ---- /dev/null -+++ b/include/client.h -@@ -0,0 +1,67 @@ -+#ifndef GEMINI_CLIENT_H -+#define GEMINI_CLIENT_H -+#include <netdb.h> -+#include <openssl/ssl.h> -+#include <sys/socket.h> -+ -+struct gemini_response { -+ int status; -+ char *meta; -+ -+ // Response body may be read from here if appropriate: -+ BIO *bio; -+ -+ // Connection state -+ SSL_CTX *ssl_ctx; -+ SSL *ssl; -+ int fd; -+}; -+ -+struct gemini_options { -+ // If NULL, an SSL context will be created. If unset, the ssl field -+ // must also be NULL. -+ SSL_CTX *ssl_ctx; -+ -+ // If NULL, an SSL connection will be established. If set, it is -+ // presumed that the caller pre-established the SSL connection. -+ SSL *ssl; -+ -+ // If ai_family != AF_UNSPEC (the default value on most systems), the -+ // client will connect to this address and skip name resolution. -+ struct addrinfo *addr; -+ -+ // If non-NULL, these hints are provided to getaddrinfo. Useful, for -+ // example, to force IPv4/IPv6. -+ struct addrinfo *hints; -+}; -+ -+enum gemini_result { -+ GEMINI_OK, -+ GEMINI_ERR_OOM, -+ GEMINI_ERR_INVALID_URL, -+ // status is set to the return value from getaddrinfo -+ GEMINI_ERR_RESOLVE, -+ // status is set to errno -+ GEMINI_ERR_CONNECT, -+ // use SSL_get_error(resp->ssl, resp->status) to get details -+ GEMINI_ERR_SSL, -+ GEMINI_ERR_IO, -+}; -+ -+// Requests the specified URL via the gemini protocol. If options is non-NULL, -+// it may specify some additional configuration to adjust client behavior. -+// -+// Returns a value indicating the success of the request. If GEMINI_OK is -+// returned, the response details shall be written to the gemini_response -+// argument. -+enum gemini_result gemini_request(const char *url, -+ struct gemini_options *options, -+ struct gemini_response *resp); -+ -+// Must be called after gemini_request in order to free up the resources -+// allocated during the request. If you intend to re-use the SSL_CTX provided by -+// gemini_options, set the ctx pointer to NULL before calling -+// gemini_response_finish. -+void gemini_response_finish(struct gemini_response *resp); -+ -+#endif -diff --git a/include/escape.h b/include/escape.h -new file mode 100644 -index 0000000000000000000000000000000000000000..a1184ba3141b2992b0b18e44105bdc4474a7d8c9 ---- /dev/null -+++ b/include/escape.h -@@ -0,0 +1,175 @@ -+#ifndef ESCAPE_H -+#define ESCAPE_H -+/*************************************************************************** -+ * _ _ ____ _ -+ * Project ___| | | | _ \| | -+ * / __| | | | |_) | | -+ * | (__| |_| | _ <| |___ -+ * \___|\___/|_| \_\_____| -+ * -+ * Copyright (C) 1998 - 2018, Daniel Stenberg, <daniel@haxx.se>, et al. -+ * -+ * This software is licensed as described in the file COPYING, which -+ * you should have received as part of this distribution. The terms -+ * are also available at https://curl.haxx.se/docs/copyright.html. -+ * -+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell -+ * copies of the Software, and permit persons to whom the Software is -+ * furnished to do so, under the terms of the COPYING file. -+ * -+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY -+ * KIND, either express or implied. -+ * -+ ***************************************************************************/ -+ -+/* from curl.h */ -+typedef enum { -+ CURLE_OK = 0, -+ CURLE_UNSUPPORTED_PROTOCOL, /* 1 */ -+ CURLE_FAILED_INIT, /* 2 */ -+ CURLE_URL_MALFORMAT, /* 3 */ -+ CURLE_NOT_BUILT_IN, /* 4 - [was obsoleted in August 2007 for -+ 7.17.0, reused in April 2011 for 7.21.5] */ -+ CURLE_COULDNT_RESOLVE_PROXY, /* 5 */ -+ CURLE_COULDNT_RESOLVE_HOST, /* 6 */ -+ CURLE_COULDNT_CONNECT, /* 7 */ -+ CURLE_WEIRD_SERVER_REPLY, /* 8 */ -+ CURLE_REMOTE_ACCESS_DENIED, /* 9 a service was denied by the server -+ due to lack of access - when login fails -+ this is not returned. */ -+ CURLE_FTP_ACCEPT_FAILED, /* 10 - [was obsoleted in April 2006 for -+ 7.15.4, reused in Dec 2011 for 7.24.0]*/ -+ CURLE_FTP_WEIRD_PASS_REPLY, /* 11 */ -+ CURLE_FTP_ACCEPT_TIMEOUT, /* 12 - timeout occurred accepting server -+ [was obsoleted in August 2007 for 7.17.0, -+ reused in Dec 2011 for 7.24.0]*/ -+ CURLE_FTP_WEIRD_PASV_REPLY, /* 13 */ -+ CURLE_FTP_WEIRD_227_FORMAT, /* 14 */ -+ CURLE_FTP_CANT_GET_HOST, /* 15 */ -+ CURLE_HTTP2, /* 16 - A problem in the http2 framing layer. -+ [was obsoleted in August 2007 for 7.17.0, -+ reused in July 2014 for 7.38.0] */ -+ CURLE_FTP_COULDNT_SET_TYPE, /* 17 */ -+ CURLE_PARTIAL_FILE, /* 18 */ -+ CURLE_FTP_COULDNT_RETR_FILE, /* 19 */ -+ CURLE_OBSOLETE20, /* 20 - NOT USED */ -+ CURLE_QUOTE_ERROR, /* 21 - quote command failure */ -+ CURLE_HTTP_RETURNED_ERROR, /* 22 */ -+ CURLE_WRITE_ERROR, /* 23 */ -+ CURLE_OBSOLETE24, /* 24 - NOT USED */ -+ CURLE_UPLOAD_FAILED, /* 25 - failed upload "command" */ -+ CURLE_READ_ERROR, /* 26 - couldn't open/read from file */ -+ CURLE_OUT_OF_MEMORY, /* 27 */ -+ /* Note: CURLE_OUT_OF_MEMORY may sometimes indicate a conversion error -+ instead of a memory allocation error if CURL_DOES_CONVERSIONS -+ is defined -+ */ -+ CURLE_OPERATION_TIMEDOUT, /* 28 - the timeout time was reached */ -+ CURLE_OBSOLETE29, /* 29 - NOT USED */ -+ CURLE_FTP_PORT_FAILED, /* 30 - FTP PORT operation failed */ -+ CURLE_FTP_COULDNT_USE_REST, /* 31 - the REST command failed */ -+ CURLE_OBSOLETE32, /* 32 - NOT USED */ -+ CURLE_RANGE_ERROR, /* 33 - RANGE "command" didn't work */ -+ CURLE_HTTP_POST_ERROR, /* 34 */ -+ CURLE_SSL_CONNECT_ERROR, /* 35 - wrong when connecting with SSL */ -+ CURLE_BAD_DOWNLOAD_RESUME, /* 36 - couldn't resume download */ -+ CURLE_FILE_COULDNT_READ_FILE, /* 37 */ -+ CURLE_LDAP_CANNOT_BIND, /* 38 */ -+ CURLE_LDAP_SEARCH_FAILED, /* 39 */ -+ CURLE_OBSOLETE40, /* 40 - NOT USED */ -+ CURLE_FUNCTION_NOT_FOUND, /* 41 - NOT USED starting with 7.53.0 */ -+ CURLE_ABORTED_BY_CALLBACK, /* 42 */ -+ CURLE_BAD_FUNCTION_ARGUMENT, /* 43 */ -+ CURLE_OBSOLETE44, /* 44 - NOT USED */ -+ CURLE_INTERFACE_FAILED, /* 45 - CURLOPT_INTERFACE failed */ -+ CURLE_OBSOLETE46, /* 46 - NOT USED */ -+ CURLE_TOO_MANY_REDIRECTS, /* 47 - catch endless re-direct loops */ -+ CURLE_UNKNOWN_OPTION, /* 48 - User specified an unknown option */ -+ CURLE_TELNET_OPTION_SYNTAX, /* 49 - Malformed telnet option */ -+ CURLE_OBSOLETE50, /* 50 - NOT USED */ -+ CURLE_OBSOLETE51, /* 51 - NOT USED */ -+ CURLE_GOT_NOTHING, /* 52 - when this is a specific error */ -+ CURLE_SSL_ENGINE_NOTFOUND, /* 53 - SSL crypto engine not found */ -+ CURLE_SSL_ENGINE_SETFAILED, /* 54 - can not set SSL crypto engine as -+ default */ -+ CURLE_SEND_ERROR, /* 55 - failed sending network data */ -+ CURLE_RECV_ERROR, /* 56 - failure in receiving network data */ -+ CURLE_OBSOLETE57, /* 57 - NOT IN USE */ -+ CURLE_SSL_CERTPROBLEM, /* 58 - problem with the local certificate */ -+ CURLE_SSL_CIPHER, /* 59 - couldn't use specified cipher */ -+ CURLE_PEER_FAILED_VERIFICATION, /* 60 - peer's certificate or fingerprint -+ wasn't verified fine */ -+ CURLE_BAD_CONTENT_ENCODING, /* 61 - Unrecognized/bad encoding */ -+ CURLE_LDAP_INVALID_URL, /* 62 - Invalid LDAP URL */ -+ CURLE_FILESIZE_EXCEEDED, /* 63 - Maximum file size exceeded */ -+ CURLE_USE_SSL_FAILED, /* 64 - Requested FTP SSL level failed */ -+ CURLE_SEND_FAIL_REWIND, /* 65 - Sending the data requires a rewind -+ that failed */ -+ CURLE_SSL_ENGINE_INITFAILED, /* 66 - failed to initialise ENGINE */ -+ CURLE_LOGIN_DENIED, /* 67 - user, password or similar was not -+ accepted and we failed to login */ -+ CURLE_TFTP_NOTFOUND, /* 68 - file not found on server */ -+ CURLE_TFTP_PERM, /* 69 - permission problem on server */ -+ CURLE_REMOTE_DISK_FULL, /* 70 - out of disk space on server */ -+ CURLE_TFTP_ILLEGAL, /* 71 - Illegal TFTP operation */ -+ CURLE_TFTP_UNKNOWNID, /* 72 - Unknown transfer ID */ -+ CURLE_REMOTE_FILE_EXISTS, /* 73 - File already exists */ -+ CURLE_TFTP_NOSUCHUSER, /* 74 - No such user */ -+ CURLE_CONV_FAILED, /* 75 - conversion failed */ -+ CURLE_CONV_REQD, /* 76 - caller must register conversion -+ callbacks using curl_easy_setopt options -+ CURLOPT_CONV_FROM_NETWORK_FUNCTION, -+ CURLOPT_CONV_TO_NETWORK_FUNCTION, and -+ CURLOPT_CONV_FROM_UTF8_FUNCTION */ -+ CURLE_SSL_CACERT_BADFILE, /* 77 - could not load CACERT file, missing -+ or wrong format */ -+ CURLE_REMOTE_FILE_NOT_FOUND, /* 78 - remote file not found */ -+ CURLE_SSH, /* 79 - error from the SSH layer, somewhat -+ generic so the error message will be of -+ interest when this has happened */ -+ -+ CURLE_SSL_SHUTDOWN_FAILED, /* 80 - Failed to shut down the SSL -+ connection */ -+ CURLE_AGAIN, /* 81 - socket is not ready for send/recv, -+ wait till it's ready and try again (Added -+ in 7.18.2) */ -+ CURLE_SSL_CRL_BADFILE, /* 82 - could not load CRL file, missing or -+ wrong format (Added in 7.19.0) */ -+ CURLE_SSL_ISSUER_ERROR, /* 83 - Issuer check failed. (Added in -+ 7.19.0) */ -+ CURLE_FTP_PRET_FAILED, /* 84 - a PRET command failed */ -+ CURLE_RTSP_CSEQ_ERROR, /* 85 - mismatch of RTSP CSeq numbers */ -+ CURLE_RTSP_SESSION_ERROR, /* 86 - mismatch of RTSP Session Ids */ -+ CURLE_FTP_BAD_FILE_LIST, /* 87 - unable to parse FTP file list */ -+ CURLE_CHUNK_FAILED, /* 88 - chunk callback reported error */ -+ CURLE_NO_CONNECTION_AVAILABLE, /* 89 - No connection available, the -+ session will be queued */ -+ CURLE_SSL_PINNEDPUBKEYNOTMATCH, /* 90 - specified pinned public key did not -+ match */ -+ CURLE_SSL_INVALIDCERTSTATUS, /* 91 - invalid certificate status */ -+ CURLE_HTTP2_STREAM, /* 92 - stream error in HTTP/2 framing layer -+ */ -+ CURLE_RECURSIVE_API_CALL, /* 93 - an api function was called from -+ inside a callback */ -+ CURL_LAST /* never use! */ -+} CURLcode; -+ -+/* Escape and unescape URL encoding in strings. The functions return a new -+ * allocated string or NULL if an error occurred. */ -+ -+bool Curl_isunreserved(unsigned char in); -+CURLcode Curl_urldecode(const char *string, size_t length, -+ char **ostring, size_t *olen, -+ bool reject_crlf); -+ -+char *curl_easy_escape(const char *string, int length); -+ -+char *curl_escape(const char *string, int length); -+ -+char *curl_easy_unescape(const char *string, -+ int length, int *outlength); -+ -+char *curl_unescape(const char *string, int length); -+ -+ -+#endif /* HEADER_CURL_ESCAPE_H */ -diff --git a/include/url.h b/include/url.h -new file mode 100644 -index 0000000000000000000000000000000000000000..155fd55740dbe47a498062713aca217ab259734f ---- /dev/null -+++ b/include/url.h -@@ -0,0 +1,103 @@ -+#ifndef URLAPI_H -+#define URLAPI_H -+/*************************************************************************** -+ * _ _ ____ _ -+ * Project ___| | | | _ \| | -+ * / __| | | | |_) | | -+ * | (__| |_| | _ <| |___ -+ * \___|\___/|_| \_\_____| -+ * -+ * Copyright (C) 2018, Daniel Stenberg, <daniel@haxx.se>, et al. -+ * -+ * This software is licensed as described in the file COPYING, which -+ * you should have received as part of this distribution. The terms -+ * are also available at https://curl.haxx.se/docs/copyright.html. -+ * -+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell -+ * copies of the Software, and permit persons to whom the Software is -+ * furnished to do so, under the terms of the COPYING file. -+ * -+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY -+ * KIND, either express or implied. -+ * -+ ***************************************************************************/ -+ -+/* the error codes for the URL API */ -+typedef enum { -+ CURLUE_OK, -+ CURLUE_BAD_HANDLE, /* 1 */ -+ CURLUE_BAD_PARTPOINTER, /* 2 */ -+ CURLUE_MALFORMED_INPUT, /* 3 */ -+ CURLUE_BAD_PORT_NUMBER, /* 4 */ -+ CURLUE_UNSUPPORTED_SCHEME, /* 5 */ -+ CURLUE_URLDECODE, /* 6 */ -+ CURLUE_OUT_OF_MEMORY, /* 7 */ -+ CURLUE_USER_NOT_ALLOWED, /* 8 */ -+ CURLUE_UNKNOWN_PART, /* 9 */ -+ CURLUE_NO_SCHEME, /* 10 */ -+ CURLUE_NO_USER, /* 11 */ -+ CURLUE_NO_PASSWORD, /* 12 */ -+ CURLUE_NO_OPTIONS, /* 13 */ -+ CURLUE_NO_HOST, /* 14 */ -+ CURLUE_NO_PORT, /* 15 */ -+ CURLUE_NO_QUERY, /* 16 */ -+ CURLUE_NO_FRAGMENT /* 17 */ -+} CURLUcode; -+ -+typedef enum { -+ CURLUPART_URL, -+ CURLUPART_SCHEME, -+ CURLUPART_USER, -+ CURLUPART_PASSWORD, -+ CURLUPART_OPTIONS, -+ CURLUPART_HOST, -+ CURLUPART_PORT, -+ CURLUPART_PATH, -+ CURLUPART_QUERY, -+ CURLUPART_FRAGMENT -+} CURLUPart; -+ -+#define CURLU_PATH_AS_IS (1<<4) /* leave dot sequences */ -+#define CURLU_DISALLOW_USER (1<<5) /* no user+password allowed */ -+#define CURLU_URLDECODE (1<<6) /* URL decode on get */ -+#define CURLU_URLENCODE (1<<7) /* URL encode on set */ -+#define CURLU_APPENDQUERY (1<<8) /* append a form style part */ -+ -+typedef struct Curl_URL CURLU; -+ -+/* -+ * curl_url() creates a new CURLU handle and returns a pointer to it. -+ * Must be freed with curl_url_cleanup(). -+ */ -+struct Curl_URL *curl_url(void); -+ -+/* -+ * curl_url_cleanup() frees the CURLU handle and related resources used for -+ * the URL parsing. It will not free strings previously returned with the URL -+ * API. -+ */ -+void curl_url_cleanup(struct Curl_URL *handle); -+ -+/* -+ * curl_url_dup() duplicates a CURLU handle and returns a new copy. The new -+ * handle must also be freed with curl_url_cleanup(). -+ */ -+struct Curl_URL *curl_url_dup(struct Curl_URL *in); -+ -+/* -+ * curl_url_get() extracts a specific part of the URL from a CURLU -+ * handle. Returns error code. The returned pointer MUST be freed with -+ * free() afterwards. -+ */ -+CURLUcode curl_url_get(struct Curl_URL *handle, CURLUPart what, -+ char **part, unsigned int flags); -+ -+/* -+ * curl_url_set() sets a specific part of the URL in a CURLU handle. Returns -+ * error code. The passed in string will be copied. Passing a NULL instead of -+ * a part string, clears that part. -+ */ -+CURLUcode curl_url_set(struct Curl_URL *handle, CURLUPart what, -+ const char *part, unsigned int flags); -+ -+#endif -diff --git a/src/client.c b/src/client.c -new file mode 100644 -index 0000000000000000000000000000000000000000..5f2debb52c310f88e82ad83ca2251ba233d6a704 ---- /dev/null -+++ b/src/client.c -@@ -0,0 +1,190 @@ -+#include <assert.h> -+#include <errno.h> -+#include <netdb.h> -+#include <openssl/bio.h> -+#include <openssl/ssl.h> -+#include <stdlib.h> -+#include <string.h> -+#include <sys/socket.h> -+#include <sys/types.h> -+#include <unistd.h> -+#include "client.h" -+#include "url.h" -+ -+static enum gemini_result -+gemini_get_addrinfo(struct Curl_URL *uri, struct gemini_options *options, -+ struct gemini_response *resp, struct addrinfo **addr) -+{ -+ int port = 1965; -+ char *uport; -+ if (curl_url_get(uri, CURLUPART_PORT, &uport, 0) == CURLUE_OK) { -+ port = (int)strtol(uport, NULL, 10); -+ free(uport); -+ } -+ -+ if (options && options->addr->ai_family != AF_UNSPEC) { -+ *addr = options->addr; -+ } else { -+ struct addrinfo hints = {0}; -+ if (options && options->hints) { -+ hints = *options->hints; -+ } else { -+ hints.ai_family = AF_UNSPEC; -+ hints.ai_socktype = SOCK_STREAM; -+ } -+ -+ char pbuf[7]; -+ snprintf(pbuf, sizeof(pbuf), "%d", port); -+ -+ char *domain; -+ CURLUcode uc = curl_url_get(uri, CURLUPART_HOST, &domain, 0); -+ assert(uc == CURLUE_OK); -+ -+ int r = getaddrinfo(domain, pbuf, &hints, addr); -+ free(domain); -+ if (r != 0) { -+ resp->status = r; -+ return GEMINI_ERR_RESOLVE; -+ } -+ } -+ -+ return GEMINI_OK; -+} -+ -+static enum gemini_result -+gemini_connect(struct Curl_URL *uri, struct gemini_options *options, -+ struct gemini_response *resp, int *sfd) -+{ -+ struct addrinfo *addr; -+ enum gemini_result res = gemini_get_addrinfo(uri, options, resp, &addr); -+ if (res != GEMINI_OK) { -+ goto cleanup; -+ } -+ -+ struct addrinfo *rp; -+ for (rp = addr; rp != NULL; rp = rp->ai_next) { -+ *sfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); -+ if (*sfd == -1) { -+ continue; -+ } -+ if (connect(*sfd, rp->ai_addr, rp->ai_addrlen) != -1) { -+ break; -+ } -+ close(*sfd); -+ } -+ if (rp == NULL) { -+ resp->status = errno; -+ res = GEMINI_ERR_CONNECT; -+ return res; -+ } -+ -+cleanup: -+ if (!options || !options->addr) { -+ freeaddrinfo(addr); -+ } -+ return res; -+} -+ -+#define GEMINI_META_MAXLEN 1024 -+#define GEMINI_STATUS_MAXLEN 2 -+ -+enum gemini_result -+gemini_request(const char *url, struct gemini_options *options, -+ struct gemini_response *resp) -+{ -+ assert(url); -+ assert(resp); -+ resp->meta = NULL; -+ if (strlen(url) > 1024) { -+ return GEMINI_ERR_INVALID_URL; -+ } -+ -+ struct Curl_URL *uri = curl_url(); -+ if (!uri) { -+ return GEMINI_ERR_OOM; -+ } -+ if (curl_url_set(uri, CURLUPART_URL, url, 0) != CURLUE_OK) { -+ return GEMINI_ERR_INVALID_URL; -+ } -+ -+ enum gemini_result res = GEMINI_OK; -+ if (options && options->ssl_ctx) { -+ resp->ssl_ctx = options->ssl_ctx; -+ SSL_CTX_up_ref(options->ssl_ctx); -+ } else { -+ resp->ssl_ctx = SSL_CTX_new(TLS_method()); -+ assert(resp->ssl_ctx); -+ } -+ -+ BIO *sbio = BIO_new(BIO_f_ssl()); -+ if (options && options->ssl) { -+ resp->ssl = options->ssl; -+ SSL_up_ref(resp->ssl); -+ BIO_set_ssl(sbio, resp->ssl, 0); -+ resp->fd = -1; -+ } else { -+ res = gemini_connect(uri, options, resp, &resp->fd); -+ if (res != GEMINI_OK) { -+ goto cleanup; -+ } -+ -+ resp->ssl = SSL_new(resp->ssl_ctx); -+ assert(resp->ssl); -+ int r = SSL_set_fd(resp->ssl, resp->fd); -+ if (r != 1) { -+ resp->status = r; -+ res = GEMINI_ERR_SSL; -+ goto cleanup; -+ } -+ r = SSL_connect(resp->ssl); -+ if (r != 1) { -+ resp->status = r; -+ res = GEMINI_ERR_SSL; -+ goto cleanup; -+ } -+ BIO_set_ssl(sbio, resp->ssl, 0); -+ } -+ -+ resp->bio = BIO_new(BIO_f_buffer()); -+ BIO_push(resp->bio, sbio); -+ -+ char req[1024 + 3]; -+ int r = snprintf(req, sizeof(req), "%s\r\n", url); -+ assert(r > 0); -+ -+ r = BIO_puts(sbio, req); -+ if (r == -1) { -+ res = GEMINI_ERR_IO; -+ goto cleanup; -+ } -+ assert(r == (int)strlen(req)); -+ -+ char buf[GEMINI_META_MAXLEN -+ + GEMINI_STATUS_MAXLEN -+ + 2 /* CRLF */ + 1 /* NUL */]; -+ r = BIO_gets(resp->bio, buf, sizeof(buf)); -+ if (r == -1) { -+ res = GEMINI_ERR_IO; -+ goto cleanup; -+ } -+ -+cleanup: -+ curl_url_cleanup(uri); -+ return res; -+} -+ -+void -+gemini_response_finish(struct gemini_response *resp) -+{ -+ if (!resp) { -+ return; -+ } -+ if (resp->fd != -1) { -+ close(resp->fd); -+ } -+ BIO_free(BIO_pop(resp->bio)); // ssl bio -+ BIO_free(resp->bio); // buffered bio -+ SSL_free(resp->ssl); -+ SSL_CTX_free(resp->ssl_ctx); -+ free(resp->meta); -+} -diff --git a/src/escape.c b/src/escape.c -new file mode 100644 -index 0000000000000000000000000000000000000000..d083da699d96f01a94a6ce8a86e41f0a47c54ffe ---- /dev/null -+++ b/src/escape.c -@@ -0,0 +1,213 @@ -+/*************************************************************************** -+ * _ _ ____ _ -+ * Project ___| | | | _ \| | -+ * / __| | | | |_) | | -+ * | (__| |_| | _ <| |___ -+ * \___|\___/|_| \_\_____| -+ * -+ * Copyright (C) 1998 - 2018, Daniel Stenberg, <daniel@haxx.se>, et al. -+ * -+ * This software is licensed as described in the file COPYING, which -+ * you should have received as part of this distribution. The terms -+ * are also available at https://curl.haxx.se/docs/copyright.html. -+ * -+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell -+ * copies of the Software, and permit persons to whom the Software is -+ * furnished to do so, under the terms of the COPYING file. -+ * -+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY -+ * KIND, either express or implied. -+ * -+ ***************************************************************************/ -+ -+/* Escape and unescape URL encoding in strings. The functions return a new -+ * allocated string or NULL if an error occurred. */ -+#include <ctype.h> -+#include <limits.h> -+#include <stdbool.h> -+#include <stddef.h> -+#include <stdio.h> -+#include <stdlib.h> -+#include <string.h> -+#include "escape.h" -+ -+/* Portable character check (remember EBCDIC). Do not use isalnum() because -+ its behavior is altered by the current locale. -+ See https://tools.ietf.org/html/rfc3986#section-2.3 -+*/ -+bool Curl_isunreserved(unsigned char in) -+{ -+ switch(in) { -+ case '0': case '1': case '2': case '3': case '4': -+ case '5': case '6': case '7': case '8': case '9': -+ case 'a': case 'b': case 'c': case 'd': case 'e': -+ case 'f': case 'g': case 'h': case 'i': case 'j': -+ case 'k': case 'l': case 'm': case 'n': case 'o': -+ case 'p': case 'q': case 'r': case 's': case 't': -+ case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': -+ case 'A': case 'B': case 'C': case 'D': case 'E': -+ case 'F': case 'G': case 'H': case 'I': case 'J': -+ case 'K': case 'L': case 'M': case 'N': case 'O': -+ case 'P': case 'Q': case 'R': case 'S': case 'T': -+ case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': -+ case '-': case '.': case '_': case '~': -+ return true; -+ default: -+ break; -+ } -+ return false; -+} -+ -+/* for ABI-compatibility with previous versions */ -+char *curl_escape(const char *string, int inlength) -+{ -+ return curl_easy_escape(string, inlength); -+} -+ -+/* for ABI-compatibility with previous versions */ -+char *curl_unescape(const char *string, int length) -+{ -+ return curl_easy_unescape(string, length, NULL); -+} -+ -+char *curl_easy_escape(const char *string, int inlength) -+{ -+ size_t alloc; -+ char *ns; -+ char *testing_ptr = NULL; -+ size_t newlen; -+ size_t strindex = 0; -+ size_t length; -+ -+ if(inlength < 0) -+ return NULL; -+ -+ alloc = (inlength?(size_t)inlength:strlen(string)) + 1; -+ newlen = alloc; -+ -+ ns = malloc(alloc); -+ if(!ns) -+ return NULL; -+ -+ length = alloc-1; -+ while(length--) { -+ unsigned char in = *string; /* we need to treat the characters unsigned */ -+ -+ if(Curl_isunreserved(in)) -+ /* just copy this */ -+ ns[strindex++] = in; -+ else { -+ /* encode it */ -+ newlen += 2; /* the size grows with two, since this'll become a %XX */ -+ if(newlen > alloc) { -+ alloc *= 2; -+ testing_ptr = realloc(ns, alloc); -+ if(!testing_ptr) -+ return NULL; -+ ns = testing_ptr; -+ } -+ -+ snprintf(&ns[strindex], 4, "%%%02X", in); -+ -+ strindex += 3; -+ } -+ string++; -+ } -+ ns[strindex] = 0; /* terminate it */ -+ return ns; -+} -+ -+/* -+ * Curl_urldecode() URL decodes the given string. -+ * -+ * Optionally detects control characters (byte codes lower than 32) in the -+ * data and rejects such data. -+ * -+ * Returns a pointer to a malloced string in *ostring with length given in -+ * *olen. If length == 0, the length is assumed to be strlen(string). -+ */ -+CURLcode Curl_urldecode(const char *string, size_t length, -+ char **ostring, size_t *olen, -+ bool reject_ctrl) -+{ -+ size_t alloc = (length?length:strlen(string)) + 1; -+ char *ns = malloc(alloc); -+ size_t strindex = 0; -+ unsigned long hex; -+ -+ if(!ns) -+ return CURLE_OUT_OF_MEMORY; -+ -+ while(--alloc > 0) { -+ unsigned char in = *string; -+ if(('%' == in) && (alloc > 2) && -+ isxdigit(string[1]) && isxdigit(string[2])) { -+ /* this is two hexadecimal digits following a '%' */ -+ char hexstr[3]; -+ char *ptr; -+ hexstr[0] = string[1]; -+ hexstr[1] = string[2]; -+ hexstr[2] = 0; -+ -+ hex = strtoul(hexstr, &ptr, 16); -+ -+ in = (unsigned char)hex; /* this long is never bigger than 255 anyway */ -+ -+ string += 2; -+ alloc -= 2; -+ } -+ -+ if(reject_ctrl && (in < 0x20)) { -+ free(ns); -+ return CURLE_URL_MALFORMAT; -+ } -+ -+ ns[strindex++] = in; -+ string++; -+ } -+ ns[strindex] = 0; /* terminate it */ -+ -+ if(olen) -+ /* store output size */ -+ *olen = strindex; -+ -+ /* store output string */ -+ *ostring = ns; -+ -+ return CURLE_OK; -+} -+ -+/* -+ * Unescapes the given URL escaped string of given length. Returns a -+ * pointer to a malloced string with length given in *olen. -+ * If length == 0, the length is assumed to be strlen(string). -+ * If olen == NULL, no output length is stored. -+ */ -+char *curl_easy_unescape(const char *string, int length, int *olen) -+{ -+ char *str = NULL; -+ if(length >= 0) { -+ size_t inputlen = length; -+ size_t outputlen; -+ CURLcode res = Curl_urldecode(string, inputlen, &str, &outputlen, false); -+ if(res) -+ return NULL; -+ -+ if(olen) { -+ if(outputlen <= (size_t) INT_MAX) -+ *olen = (int)outputlen; -+ else -+ /* too large to return in an int, fail! */ -+ free(str); -+ } -+ } -+ return str; -+} -+ -+/* For operating systems/environments that use different malloc/free -+ systems for the app and for this library, we provide a free that uses -+ the library's memory system */ -+void curl_free(void *p) -+{ -+ free(p); -+} -diff --git a/src/gmnic.c b/src/gmnic.c -index 35a49493cc6af2486d055c9bde5a576942c2e40a..7b2eb183ebfe67dce0aaf8c6010bf6785b503df6 100644 ---- a/src/gmnic.c -+++ b/src/gmnic.c -@@ -1,8 +1,91 @@ -+#include <assert.h> -+#include <getopt.h> -+#include <openssl/err.h> -+#include <stdbool.h> - #include <stdio.h> -+#include <stdlib.h> -+#include "client.h" -+ -+static void -+usage(char *argv_0) -+{ -+ fprintf(stderr, -+ "usage: %s [-LI] [-C cert] [-d input] gemini://...\n", -+ argv_0); -+} - - int --main(int argc, char *argv[]) { -- (void)argc; (void)argv; -- printf("Hello, world!\n"); -+main(int argc, char *argv[]) -+{ -+ bool headers = false, follow_redirect = false; -+ char *certificate = NULL, *input = NULL; -+ -+ int c; -+ while ((c = getopt(argc, argv, "46C:d:hLI")) != -1) { -+ switch (c) { -+ case '4': -+ assert(0); // TODO -+ break; -+ case '6': -+ assert(0); // TODO -+ break; -+ case 'C': -+ certificate = optarg; -+ break; -+ case 'd': -+ input = optarg; -+ break; -+ case 'h': -+ usage(argv[0]); -+ return 0; -+ case 'L': -+ follow_redirect = true; -+ break; -+ case 'I': -+ headers = true; -+ break; -+ default: -+ fprintf(stderr, "fatal: unknown flag %c", c); -+ return 1; -+ } -+ } -+ if (optind != argc - 1) { -+ usage(argv[0]); -+ return 1; -+ } -+ -+ SSL_load_error_strings(); -+ ERR_load_crypto_strings(); -+ -+ struct gemini_response resp; -+ enum gemini_result r = gemini_request(argv[optind], NULL, &resp); -+ switch (r) { -+ case GEMINI_OK: -+ printf("OK\n"); -+ break; -+ case GEMINI_ERR_OOM: -+ printf("OOM\n"); -+ break; -+ case GEMINI_ERR_INVALID_URL: -+ printf("INVALID_URL\n"); -+ break; -+ case GEMINI_ERR_RESOLVE: -+ printf("RESOLVE\n"); -+ break; -+ case GEMINI_ERR_CONNECT: -+ printf("CONNECT\n"); -+ break; -+ case GEMINI_ERR_SSL: -+ fprintf(stderr, "SSL error: %s\n", ERR_error_string( -+ SSL_get_error(resp.ssl, resp.status), NULL)); -+ break; -+ } -+ -+ gemini_response_finish(&resp); -+ -+ (void)headers; -+ (void)follow_redirect; -+ (void)certificate; -+ (void)input; - return 0; - } -diff --git a/src/url.c b/src/url.c -new file mode 100644 -index 0000000000000000000000000000000000000000..47e31b5fcbeeecb5bc3962585311b20e3518bb73 ---- /dev/null -+++ b/src/url.c -@@ -0,0 +1,1448 @@ -+/*************************************************************************** -+ * _ _ ____ _ -+ * Project ___| | | | _ \| | -+ * / __| | | | |_) | | -+ * | (__| |_| | _ <| |___ -+ * \___|\___/|_| \_\_____| -+ * -+ * Copyright (C) 1998 - 2018, Daniel Stenberg, <daniel@haxx.se>, et al. -+ * -+ * This software is licensed as described in the file COPYING, which -+ * you should have received as part of this distribution. The terms -+ * are also available at https://curl.haxx.se/docs/copyright.html. -+ * -+ * You may opt to use, copy, modify, merge, publish, distribute and/or sell -+ * copies of the Software, and permit persons to whom the Software is -+ * furnished to do so, under the terms of the COPYING file. -+ * -+ * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY -+ * KIND, either express or implied. -+ * -+ ***************************************************************************/ -+ -+#define MAX_SCHEME_LEN 8 -+ -+#include <assert.h> -+#include <ctype.h> -+#include <stdarg.h> -+#include <stdbool.h> -+#include <stdio.h> -+#include <stdlib.h> -+#include <string.h> -+#include <strings.h> -+#include "escape.h" -+#include "url.h" -+ -+/* Provided by gmni */ -+static char * -+aprintf(const char *fmt, ...) -+{ -+ va_list ap; -+ va_start(ap, fmt); -+ int n = vsnprintf(NULL, 0, fmt, ap); -+ va_end(ap); -+ -+ char *strp = calloc(n + 1, 1); -+ assert(strp); -+ -+ va_start(ap, fmt); -+ n = vsnprintf(strp, n + 1, fmt, ap); -+ va_end(ap); -+ return strp; -+} -+ -+/* via lib/dotdot.c */ -+char *Curl_dedotdotify(const char *input) -+{ -+ size_t inlen = strlen(input); -+ char *clone; -+ size_t clen = inlen; /* the length of the cloned input */ -+ char *out = malloc(inlen + 1); -+ char *outptr; -+ char *orgclone; -+ char *queryp; -+ if(!out) -+ return NULL; /* out of memory */ -+ -+ *out = 0; /* zero terminates, for inputs like "./" */ -+ -+ /* get a cloned copy of the input */ -+ clone = strdup(input); -+ if(!clone) { -+ free(out); -+ return NULL; -+ } -+ orgclone = clone; -+ outptr = out; -+ -+ if(!*clone) { -+ /* zero length string, return that */ -+ free(out); -+ return clone; -+ } -+ -+ /* -+ * To handle query-parts properly, we must find it and remove it during the -+ * dotdot-operation and then append it again at the end to the output -+ * string. -+ */ -+ queryp = strchr(clone, '?'); -+ if(queryp) -+ *queryp = 0; -+ -+ do { -+ -+ /* A. If the input buffer begins with a prefix of "../" or "./", then -+ remove that prefix from the input buffer; otherwise, */ -+ -+ if(!strncmp("./", clone, 2)) { -+ clone += 2; -+ clen -= 2; -+ } -+ else if(!strncmp("../", clone, 3)) { -+ clone += 3; -+ clen -= 3; -+ } -+ -+ /* B. if the input buffer begins with a prefix of "/./" or "/.", where -+ "." is a complete path segment, then replace that prefix with "/" in -+ the input buffer; otherwise, */ -+ else if(!strncmp("/./", clone, 3)) { -+ clone += 2; -+ clen -= 2; -+ } -+ else if(!strcmp("/.", clone)) { -+ clone[1]='/'; -+ clone++; -+ clen -= 1; -+ } -+ -+ /* C. if the input buffer begins with a prefix of "/../" or "/..", where -+ ".." is a complete path segment, then replace that prefix with "/" in -+ the input buffer and remove the last segment and its preceding "/" (if -+ any) from the output buffer; otherwise, */ -+ -+ else if(!strncmp("/../", clone, 4)) { -+ clone += 3; -+ clen -= 3; -+ /* remove the last segment from the output buffer */ -+ while(outptr > out) { -+ outptr--; -+ if(*outptr == '/') -+ break; -+ } -+ *outptr = 0; /* zero-terminate where it stops */ -+ } -+ else if(!strcmp("/..", clone)) { -+ clone[2]='/'; -+ clone += 2; -+ clen -= 2; -+ /* remove the last segment from the output buffer */ -+ while(outptr > out) { -+ outptr--; -+ if(*outptr == '/') -+ break; -+ } -+ *outptr = 0; /* zero-terminate where it stops */ -+ } -+ -+ /* D. if the input buffer consists only of "." or "..", then remove -+ that from the input buffer; otherwise, */ -+ -+ else if(!strcmp(".", clone) || !strcmp("..", clone)) { -+ *clone = 0; -+ *out = 0; -+ } -+ -+ else { -+ /* E. move the first path segment in the input buffer to the end of -+ the output buffer, including the initial "/" character (if any) and -+ any subsequent characters up to, but not including, the next "/" -+ character or the end of the input buffer. */ -+ -+ do { -+ *outptr++ = *clone++; -+ clen--; -+ } while(*clone && (*clone != '/')); -+ *outptr = 0; -+ } -+ -+ } while(*clone); -+ -+ if(queryp) { -+ size_t qlen; -+ /* There was a query part, append that to the output. The 'clone' string -+ may now have been altered so we copy from the original input string -+ from the correct index. */ -+ size_t oindex = queryp - orgclone; -+ qlen = strlen(&input[oindex]); -+ memcpy(outptr, &input[oindex], qlen + 1); /* include the end zero byte */ -+ } -+ -+ free(orgclone); -+ return out; -+} -+ -+/* via lib/url.c */ -+CURLcode Curl_parse_login_details(const char *login, const size_t len, -+ char **userp, char **passwdp, -+ char **optionsp) -+{ -+ CURLcode result = CURLE_OK; -+ char *ubuf = NULL; -+ char *pbuf = NULL; -+ char *obuf = NULL; -+ const char *psep = NULL; -+ const char *osep = NULL; -+ size_t ulen; -+ size_t plen; -+ size_t olen; -+ -+ /* Attempt to find the password separator */ -+ if(passwdp) { -+ psep = strchr(login, ':'); -+ -+ /* Within the constraint of the login string */ -+ if(psep >= login + len) -+ psep = NULL; -+ } -+ -+ /* Attempt to find the options separator */ -+ if(optionsp) { -+ osep = strchr(login, ';'); -+ -+ /* Within the constraint of the login string */ -+ if(osep >= login + len) -+ osep = NULL; -+ } -+ -+ /* Calculate the portion lengths */ -+ ulen = (psep ? -+ (size_t)(osep && psep > osep ? osep - login : psep - login) : -+ (osep ? (size_t)(osep - login) : len)); -+ plen = (psep ? -+ (osep && osep > psep ? (size_t)(osep - psep) : -+ (size_t)(login + len - psep)) - 1 : 0); -+ olen = (osep ? -+ (psep && psep > osep ? (size_t)(psep - osep) : -+ (size_t)(login + len - osep)) - 1 : 0); -+ -+ /* Allocate the user portion buffer */ -+ if(userp && ulen) { -+ ubuf = malloc(ulen + 1); -+ if(!ubuf) -+ result = CURLE_OUT_OF_MEMORY; -+ } -+ -+ /* Allocate the password portion buffer */ -+ if(!result && passwdp && plen) { -+ pbuf = malloc(plen + 1); -+ if(!pbuf) { -+ free(ubuf); -+ result = CURLE_OUT_OF_MEMORY; -+ } -+ } -+ -+ /* Allocate the options portion buffer */ -+ if(!result && optionsp && olen) { -+ obuf = malloc(olen + 1); -+ if(!obuf) { -+ free(pbuf); -+ free(ubuf); -+ result = CURLE_OUT_OF_MEMORY; -+ } -+ } -+ -+ if(!result) { -+ /* Store the user portion if necessary */ -+ if(ubuf) { -+ memcpy(ubuf, login, ulen); -+ ubuf[ulen] = '\0'; -+ free(*userp); -+ *userp = ubuf; -+ } -+ -+ /* Store the password portion if necessary */ -+ if(pbuf) { -+ memcpy(pbuf, psep + 1, plen); -+ pbuf[plen] = '\0'; -+ free(*passwdp); -+ *passwdp = pbuf; -+ } -+ -+ /* Store the options portion if necessary */ -+ if(obuf) { -+ memcpy(obuf, osep + 1, olen); -+ obuf[olen] = '\0'; -+ free(*optionsp); -+ *optionsp = obuf; -+ } -+ } -+ -+ return result; -+} -+ -+/* Internal representation of CURLU. Point to URL-encoded strings. */ -+struct Curl_URL { -+ char *scheme; -+ char *user; -+ char *password; -+ char *options; /* IMAP only? */ -+ char *host; -+ char *port; -+ char *path; -+ char *query; -+ char *fragment; -+ -+ char *scratch; /* temporary scratch area */ -+ long portnum; /* the numerical version */ -+}; -+ -+#define DEFAULT_SCHEME "https" -+ -+static void free_urlhandle(struct Curl_URL *u) -+{ -+ free(u->scheme); -+ free(u->user); -+ free(u->password); -+ free(u->options); -+ free(u->host); -+ free(u->port); -+ free(u->path); -+ free(u->query); -+ free(u->fragment); -+ free(u->scratch); -+} -+ -+/* move the full contents of one handle onto another and -+ free the original */ -+static void mv_urlhandle(struct Curl_URL *from, -+ struct Curl_URL *to) -+{ -+ free_urlhandle(to); -+ *to = *from; -+ free(from); -+} -+ -+/* -+ * Find the separator at the end of the host name, or the '?' in cases like -+ * http://www.url.com?id=2380 -+ */ -+static const char *find_host_sep(const char *url) -+{ -+ const char *sep; -+ const char *query; -+ -+ /* Find the start of the hostname */ -+ sep = strstr(url, "//"); -+ if(!sep) -+ sep = url; -+ else -+ sep += 2; -+ -+ query = strchr(sep, '?'); -+ sep = strchr(sep, '/'); -+ -+ if(!sep) -+ sep = url + strlen(url); -+ -+ if(!query) -+ query = url + strlen(url); -+ -+ return sep < query ? sep : query; -+} -+ -+/* -+ * Decide in an encoding-independent manner whether a character in an -+ * URL must be escaped. The same criterion must be used in strlen_url() -+ * and strcpy_url(). -+ */ -+static bool urlchar_needs_escaping(int c) -+{ -+ return !(iscntrl(c) || isspace(c) || isgraph(c)); -+} -+ -+/* -+ * strlen_url() returns the length of the given URL if the spaces within the -+ * URL were properly URL encoded. -+ * URL encoding should be skipped for host names, otherwise IDN resolution -+ * will fail. -+ */ -+size_t Curl_strlen_url(const char *url, bool relative) -+{ -+ const unsigned char *ptr; -+ size_t newlen = 0; -+ bool left = true; /* left side of the ? */ -+ const unsigned char *host_sep = (const unsigned char *) url; -+ -+ if(!relative) -+ host_sep = (const unsigned char *) find_host_sep(url); -+ -+ for(ptr = (unsigned char *)url; *ptr; ptr++) { -+ -+ if(ptr < host_sep) { -+ ++newlen; -+ continue; -+ } -+ -+ switch(*ptr) { -+ case '?': -+ left = false; -+ /* FALLTHROUGH */ -+ default: -+ if(urlchar_needs_escaping(*ptr)) -+ newlen += 2; -+ newlen++; -+ break; -+ case ' ': -+ if(left) -+ newlen += 3; -+ else -+ newlen++; -+ break; -+ } -+ } -+ return newlen; -+} -+ -+/* strcpy_url() copies a url to a output buffer and URL-encodes the spaces in -+ * the source URL accordingly. -+ * URL encoding should be skipped for host names, otherwise IDN resolution -+ * will fail. -+ */ -+void Curl_strcpy_url(char *output, const char *url, bool relative) -+{ -+ /* we must add this with whitespace-replacing */ -+ bool left = true; -+ const unsigned char *iptr; -+ char *optr = output; -+ const unsigned char *host_sep = (const unsigned char *) url; -+ -+ if(!relative) -+ host_sep = (const unsigned char *) find_host_sep(url); -+ -+ for(iptr = (unsigned char *)url; /* read from here */ -+ *iptr; /* until zero byte */ -+ iptr++) { -+ -+ if(iptr < host_sep) { -+ *optr++ = *iptr; -+ continue; -+ } -+ -+ switch(*iptr) { -+ case '?': -+ left = false; -+ /* FALLTHROUGH */ -+ default: -+ if(urlchar_needs_escaping(*iptr)) { -+ snprintf(optr, 4, "%%%02x", *iptr); -+ optr += 3; -+ } -+ else -+ *optr++=*iptr; -+ break; -+ case ' ': -+ if(left) { -+ *optr++='%'; /* add a '%' */ -+ *optr++='2'; /* add a '2' */ -+ *optr++='0'; /* add a '0' */ -+ } -+ else -+ *optr++='+'; /* add a '+' here */ -+ break; -+ } -+ } -+ *optr = 0; /* zero terminate output buffer */ -+ -+} -+ -+/* -+ * Returns true if the given URL is absolute (as opposed to relative) within -+ * the buffer size. Returns the scheme in the buffer if true and 'buf' is -+ * non-NULL. -+ */ -+bool Curl_is_absolute_url(const char *url, char *buf, size_t buflen) -+{ -+ size_t i; -+ for(i = 0; i < buflen && url[i]; ++i) { -+ char s = url[i]; -+ if((s == ':') && (url[i + 1] == '/')) { -+ if(buf) -+ buf[i] = 0; -+ return true; -+ } -+ /* RFC 3986 3.1 explains: -+ scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) -+ */ -+ else if(isalnum(s) || (s == '+') || (s == '-') || (s == '.') ) { -+ if(buf) -+ buf[i] = (char)tolower(s); -+ } -+ else -+ break; -+ } -+ return false; -+} -+ -+/* -+ * Concatenate a relative URL to a base URL making it absolute. -+ * URL-encodes any spaces. -+ * The returned pointer must be freed by the caller unless NULL -+ * (returns NULL on out of memory). -+ */ -+char *Curl_concat_url(const char *base, const char *relurl) -+{ -+ /*** -+ TRY to append this new path to the old URL -+ to the right of the host part. Oh crap, this is doomed to cause -+ problems in the future... -+ */ -+ char *newest; -+ char *protsep; -+ char *pathsep; -+ size_t newlen; -+ bool host_changed = false; -+ -+ const char *useurl = relurl; -+ size_t urllen; -+ -+ /* we must make our own copy of the URL to play with, as it may -+ point to read-only data */ -+ char *url_clone = strdup(base); -+ -+ if(!url_clone) -+ return NULL; /* skip out of this NOW */ -+ -+ /* protsep points to the start of the host name */ -+ protsep = strstr(url_clone, "//"); -+ if(!protsep) -+ protsep = url_clone; -+ else -+ protsep += 2; /* pass the slashes */ -+ -+ if('/' != relurl[0]) { -+ int level = 0; -+ -+ /* First we need to find out if there's a ?-letter in the URL, -+ and cut it and the right-side of that off */ -+ pathsep = strchr(protsep, '?'); -+ if(pathsep) -+ *pathsep = 0; -+ -+ /* we have a relative path to append to the last slash if there's one -+ available, or if the new URL is just a query string (starts with a -+ '?') we append the new one at the end of the entire currently worked -+ out URL */ -+ if(useurl[0] != '?') { -+ pathsep = strrchr(protsep, '/'); -+ if(pathsep) -+ *pathsep = 0; -+ } -+ -+ /* Check if there's any slash after the host name, and if so, remember -+ that position instead */ -+ pathsep = strchr(protsep, '/'); -+ if(pathsep) -+ protsep = pathsep + 1; -+ else -+ protsep = NULL; -+ -+ /* now deal with one "./" or any amount of "../" in the newurl -+ and act accordingly */ -+ -+ if((useurl[0] == '.') && (useurl[1] == '/')) -+ useurl += 2; /* just skip the "./" */ -+ -+ while((useurl[0] == '.') && -+ (useurl[1] == '.') && -+ (useurl[2] == '/')) { -+ level++; -+ useurl += 3; /* pass the "../" */ -+ } -+ -+ if(protsep) { -+ while(level--) { -+ /* cut off one more level from the right of the original URL */ -+ pathsep = strrchr(protsep, '/'); -+ if(pathsep) -+ *pathsep = 0; -+ else { -+ *protsep = 0; -+ break; -+ } -+ } -+ } -+ } -+ else { -+ /* We got a new absolute path for this server */ -+ -+ if((relurl[0] == '/') && (relurl[1] == '/')) { -+ /* the new URL starts with //, just keep the protocol part from the -+ original one */ -+ *protsep = 0; -+ useurl = &relurl[2]; /* we keep the slashes from the original, so we -+ skip the new ones */ -+ host_changed = true; -+ } -+ else { -+ /* cut off the original URL from the first slash, or deal with URLs -+ without slash */ -+ pathsep = strchr(protsep, '/'); -+ if(pathsep) { -+ /* When people use badly formatted URLs, such as -+ "http://www.url.com?dir=/home/daniel" we must not use the first -+ slash, if there's a ?-letter before it! */ -+ char *sep = strchr(protsep, '?'); -+ if(sep && (sep < pathsep)) -+ pathsep = sep; -+ *pathsep = 0; -+ } -+ else { -+ /* There was no slash. Now, since we might be operating on a badly -+ formatted URL, such as "http://www.url.com?id=2380" which doesn't -+ use a slash separator as it is supposed to, we need to check for a -+ ?-letter as well! */ -+ pathsep = strchr(protsep, '?'); -+ if(pathsep) -+ *pathsep = 0; -+ } -+ } -+ } -+ -+ /* If the new part contains a space, this is a mighty stupid redirect -+ but we still make an effort to do "right". To the left of a '?' -+ letter we replace each space with %20 while it is replaced with '+' -+ on the right side of the '?' letter. -+ */ -+ newlen = Curl_strlen_url(useurl, !host_changed); -+ -+ urllen = strlen(url_clone); -+ -+ newest = malloc(urllen + 1 + /* possible slash */ -+ newlen + 1 /* zero byte */); -+ -+ if(!newest) { -+ free(url_clone); /* don't leak this */ -+ return NULL; -+ } -+ -+ /* copy over the root url part */ -+ memcpy(newest, url_clone, urllen); -+ -+ /* check if we need to append a slash */ -+ if(('/' == useurl[0]) || (protsep && !*protsep) || ('?' == useurl[0])) -+ ; -+ else -+ newest[urllen++]='/'; -+ -+ /* then append the new piece on the right side */ -+ Curl_strcpy_url(&newest[urllen], useurl, !host_changed); -+ -+ free(url_clone); -+ -+ return newest; -+} -+ -+/* -+ * parse_hostname_login() -+ * -+ * Parse the login details (user name, password and options) from the URL and -+ * strip them out of the host name -+ * -+ */ -+static CURLUcode parse_hostname_login(struct Curl_URL *u, -+ char **hostname, -+ unsigned int flags) -+{ -+ CURLUcode result = CURLUE_OK; -+ CURLcode ccode; -+ char *userp = NULL; -+ char *passwdp = NULL; -+ char *optionsp = NULL; -+ -+ /* At this point, we're hoping all the other special cases have -+ * been taken care of, so conn->host.name is at most -+ * [user[:password][;options]]@]hostname -+ * -+ * We need somewhere to put the embedded details, so do that first. -+ */ -+ -+ char *ptr = strchr(*hostname, '@'); -+ char *login = *hostname; -+ -+ if(!ptr) -+ goto out; -+ -+ /* We will now try to extract the -+ * possible login information in a string like: -+ * ftp://user:password@ftp.my.site:8021/README */ -+ *hostname = ++ptr; -+ -+ /* We could use the login information in the URL so extract it. Only parse -+ options if the handler says we should. Note that 'h' might be NULL! */ -+ ccode = Curl_parse_login_details(login, ptr - login - 1, -+ &userp, &passwdp, NULL); -+ if(ccode) { -+ result = CURLUE_MALFORMED_INPUT; -+ goto out; -+ } -+ -+ if(userp) { -+ if(flags & CURLU_DISALLOW_USER) { -+ /* Option DISALLOW_USER is set and url contains username. */ -+ result = CURLUE_USER_NOT_ALLOWED; -+ goto out; -+ } -+ -+ u->user = userp; -+ } -+ -+ if(passwdp) -+ u->password = passwdp; -+ -+ if(optionsp) -+ u->options = optionsp; -+ -+ return CURLUE_OK; -+ out: -+ -+ free(userp); -+ free(passwdp); -+ free(optionsp); -+ -+ return result; -+} -+ -+static CURLUcode parse_port(struct Curl_URL *u, char *hostname) -+{ -+ char *portptr; -+ char endbracket; -+ int len; -+ -+ if((1 == sscanf(hostname, "[%*45[0123456789abcdefABCDEF:.%%]%c%n", -+ &endbracket, &len)) && -+ (']' == endbracket)) { -+ /* this is a RFC2732-style specified IP-address */ -+ portptr = &hostname[len]; -+ if(*portptr) { -+ if(*portptr != ':') -+ return CURLUE_MALFORMED_INPUT; -+ } -+ else -+ portptr = NULL; -+ } -+ else -+ portptr = strchr(hostname, ':'); -+ -+ if(portptr) { -+ char *rest; -+ long port; -+ char portbuf[7]; -+ -+ if(!isdigit(portptr[1])) -+ return CURLUE_BAD_PORT_NUMBER; -+ -+ port = strtol(portptr + 1, &rest, 10); /* Port number must be decimal */ -+ -+ if((port <= 0) || (port > 0xffff)) -+ /* Single unix standard says port numbers are 16 bits long, but we don't -+ treat port zero as OK. */ -+ return CURLUE_BAD_PORT_NUMBER; -+ -+ if(rest[0]) -+ return CURLUE_BAD_PORT_NUMBER; -+ -+ if(rest != &portptr[1]) { -+ *portptr++ = '\0'; /* cut off the name there */ -+ *rest = 0; -+ /* generate a new to get rid of leading zeroes etc */ -+ snprintf(portbuf, sizeof(portbuf), "%ld", port); -+ u->portnum = port; -+ u->port = strdup(portbuf); -+ if(!u->port) -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ else { -+ /* Browser behavior adaptation. If there's a colon with no digits after, -+ just cut off the name there which makes us ignore the colon and just -+ use the default port. Firefox and Chrome both do that. */ -+ *portptr = '\0'; -+ } -+ } -+ -+ return CURLUE_OK; -+} -+ -+/* scan for byte values < 31 or 127 */ -+static CURLUcode junkscan(char *part) -+{ -+ char badbytes[]={ -+ /* */ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, -+ 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, -+ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, -+ 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, -+ 0x7f, -+ 0x00 /* zero terminate */ -+ }; -+ if(part) { -+ size_t n = strlen(part); -+ size_t nfine = strcspn(part, badbytes); -+ if(nfine != n) -+ /* since we don't know which part is scanned, return a generic error -+ code */ -+ return CURLUE_MALFORMED_INPUT; -+ } -+ return CURLUE_OK; -+} -+ -+static CURLUcode hostname_check(char *hostname, unsigned int flags) -+{ -+ const char *l = NULL; /* accepted characters */ -+ size_t len; -+ size_t hlen = strlen(hostname); -+ (void)flags; -+ -+ if(hostname[0] == '[') { -+ hostname++; -+ l = "0123456789abcdefABCDEF::.%"; -+ hlen -= 2; -+ } -+ -+ if(l) { -+ /* only valid letters are ok */ -+ len = strspn(hostname, l); -+ if(hlen != len) -+ /* hostname with bad content */ -+ return CURLUE_MALFORMED_INPUT; -+ } -+ else { -+ /* letters from the second string is not ok */ -+ len = strcspn(hostname, " "); -+ if(hlen != len) -+ /* hostname with bad content */ -+ return CURLUE_MALFORMED_INPUT; -+ } -+ return CURLUE_OK; -+} -+ -+#define HOSTNAME_END(x) (((x) == '/') || ((x) == '?') || ((x) == '#')) -+ -+static CURLUcode seturl(const char *url, struct Curl_URL *u, unsigned int flags) -+{ -+ char *path; -+ bool path_alloced = false; -+ char *hostname; -+ char *query = NULL; -+ char *fragment = NULL; -+ CURLUcode result; -+ bool url_has_scheme = false; -+ char schemebuf[MAX_SCHEME_LEN]; -+ char *schemep = NULL; -+ size_t schemelen = 0; -+ size_t urllen; -+ -+ if(!url) -+ return CURLUE_MALFORMED_INPUT; -+ -+ /************************************************************* -+ * Parse the URL. -+ ************************************************************/ -+ /* allocate scratch area */ -+ urllen = strlen(url); -+ path = u->scratch = malloc(urllen * 2 + 2); -+ if(!path) -+ return CURLUE_OUT_OF_MEMORY; -+ -+ hostname = &path[urllen + 1]; -+ hostname[0] = 0; -+ -+ if(Curl_is_absolute_url(url, schemebuf, sizeof(schemebuf))) { -+ url_has_scheme = true; -+ schemelen = strlen(schemebuf); -+ } -+ -+ /* handle the file: scheme */ -+ if(url_has_scheme && strcasecmp(schemebuf, "file") == 0) { -+ /* path has been allocated large enough to hold this */ -+ strcpy(path, &url[5]); -+ -+ hostname = NULL; /* no host for file: URLs */ -+ u->scheme = strdup("file"); -+ if(!u->scheme) -+ return CURLUE_OUT_OF_MEMORY; -+ -+ /* Extra handling URLs with an authority component (i.e. that start with -+ * "file://") -+ * -+ * We allow omitted hostname (e.g. file:/<path>) -- valid according to -+ * RFC 8089, but not the (current) WHAT-WG URL spec. -+ */ -+ if(path[0] == '/' && path[1] == '/') { -+ /* swallow the two slashes */ -+ char *ptr = &path[2]; -+ path = ptr; -+ } -+ } -+ else { -+ /* clear path */ -+ const char *p; -+ const char *hostp; -+ size_t len; -+ path[0] = 0; -+ -+ if(url_has_scheme) { -+ int i = 0; -+ p = &url[schemelen + 1]; -+ while(p && (*p == '/') && (i < 4)) { -+ p++; -+ i++; -+ } -+ if((i < 1) || (i>3)) -+ /* less than one or more than three slashes */ -+ return CURLUE_MALFORMED_INPUT; -+ -+ schemep = schemebuf; -+ if(junkscan(schemep)) -+ return CURLUE_MALFORMED_INPUT; -+ } -+ else { -+ /* no scheme! */ -+ return CURLUE_MALFORMED_INPUT; -+ } -+ hostp = p; /* host name starts here */ -+ -+ while(*p && !HOSTNAME_END(*p)) /* find end of host name */ -+ p++; -+ -+ len = p - hostp; -+ if(!len) -+ return CURLUE_MALFORMED_INPUT; -+ -+ memcpy(hostname, hostp, len); -+ hostname[len] = 0; -+ -+ len = strlen(p); -+ memcpy(path, p, len); -+ path[len] = 0; -+ -+ u->scheme = strdup(schemep); -+ if(!u->scheme) -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ -+ if(junkscan(path)) -+ return CURLUE_MALFORMED_INPUT; -+ -+ query = strchr(path, '?'); -+ if(query) -+ *query++ = 0; -+ -+ fragment = strchr(query?query:path, '#'); -+ if(fragment) -+ *fragment++ = 0; -+ -+ if(!path[0]) -+ /* if there's no path set, unset */ -+ path = NULL; -+ else if(!(flags & CURLU_PATH_AS_IS)) { -+ /* sanitise paths and remove ../ and ./ sequences according to RFC3986 */ -+ char *newp = Curl_dedotdotify(path); -+ if(!newp) -+ return CURLUE_OUT_OF_MEMORY; -+ -+ if(strcmp(newp, path)) { -+ /* if we got a new version */ -+ path = newp; -+ path_alloced = true; -+ } -+ else -+ free(newp); -+ } -+ if(path) { -+ u->path = path_alloced?path:strdup(path); -+ if(!u->path) -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ -+ if(hostname) { -+ /* -+ * Parse the login details and strip them out of the host name. -+ */ -+ if(junkscan(hostname)) -+ return CURLUE_MALFORMED_INPUT; -+ -+ result = parse_hostname_login(u, &hostname, flags); -+ if(result) -+ return result; -+ -+ result = parse_port(u, hostname); -+ if(result) -+ return result; -+ -+ result = hostname_check(hostname, flags); -+ if(result) -+ return result; -+ -+ u->host = strdup(hostname); -+ if(!u->host) -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ -+ if(query && query[0]) { -+ u->query = strdup(query); -+ if(!u->query) -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ if(fragment && fragment[0]) { -+ u->fragment = strdup(fragment); -+ if(!u->fragment) -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ -+ free(u->scratch); -+ u->scratch = NULL; -+ -+ return CURLUE_OK; -+} -+ -+/* -+ * Parse the URL and set the relevant members of the Curl_URL struct. -+ */ -+static CURLUcode parseurl(const char *url, struct Curl_URL *u, unsigned int flags) -+{ -+ CURLUcode result = seturl(url, u, flags); -+ if(result) { -+ free_urlhandle(u); -+ memset(u, 0, sizeof(struct Curl_URL)); -+ } -+ return result; -+} -+ -+/* -+ */ -+struct Curl_URL *curl_url(void) -+{ -+ return calloc(sizeof(struct Curl_URL), 1); -+} -+ -+void curl_url_cleanup(struct Curl_URL *u) -+{ -+ if(u) { -+ free_urlhandle(u); -+ free(u); -+ } -+} -+ -+#define DUP(dest, src, name) \ -+ if(src->name) { \ -+ dest->name = strdup(src->name); \ -+ if(!dest->name) \ -+ goto fail; \ -+ } -+ -+struct Curl_URL *curl_url_dup(struct Curl_URL *in) -+{ -+ struct Curl_URL *u = calloc(sizeof(struct Curl_URL), 1); -+ if(u) { -+ DUP(u, in, scheme); -+ DUP(u, in, user); -+ DUP(u, in, password); -+ DUP(u, in, options); -+ DUP(u, in, host); -+ DUP(u, in, port); -+ DUP(u, in, path); -+ DUP(u, in, query); -+ DUP(u, in, fragment); -+ u->portnum = in->portnum; -+ } -+ return u; -+ fail: -+ curl_url_cleanup(u); -+ return NULL; -+} -+ -+CURLUcode curl_url_get(struct Curl_URL *u, CURLUPart what, -+ char **part, unsigned int flags) -+{ -+ char *ptr; -+ CURLUcode ifmissing = CURLUE_UNKNOWN_PART; -+ bool urldecode = (flags & CURLU_URLDECODE)?1:0; -+ bool plusdecode = false; -+ (void)flags; -+ if(!u) -+ return CURLUE_BAD_HANDLE; -+ if(!part) -+ return CURLUE_BAD_PARTPOINTER; -+ *part = NULL; -+ -+ switch(what) { -+ case CURLUPART_SCHEME: -+ ptr = u->scheme; -+ ifmissing = CURLUE_NO_SCHEME; -+ urldecode = false; /* never for schemes */ -+ break; -+ case CURLUPART_USER: -+ ptr = u->user; -+ ifmissing = CURLUE_NO_USER; -+ break; -+ case CURLUPART_PASSWORD: -+ ptr = u->password; -+ ifmissing = CURLUE_NO_PASSWORD; -+ break; -+ case CURLUPART_OPTIONS: -+ ptr = u->options; -+ ifmissing = CURLUE_NO_OPTIONS; -+ break; -+ case CURLUPART_HOST: -+ ptr = u->host; -+ ifmissing = CURLUE_NO_HOST; -+ break; -+ case CURLUPART_PORT: -+ ptr = u->port; -+ ifmissing = CURLUE_NO_PORT; -+ urldecode = false; /* never for port */ -+ break; -+ case CURLUPART_PATH: -+ ptr = u->path; -+ if(!ptr) { -+ ptr = u->path = strdup("/"); -+ if(!u->path) -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ break; -+ case CURLUPART_QUERY: -+ ptr = u->query; -+ ifmissing = CURLUE_NO_QUERY; -+ plusdecode = urldecode; -+ break; -+ case CURLUPART_FRAGMENT: -+ ptr = u->fragment; -+ ifmissing = CURLUE_NO_FRAGMENT; -+ break; -+ case CURLUPART_URL: { -+ char *url; -+ char *scheme; -+ char *options = u->options; -+ char *port = u->port; -+ if(u->scheme && strcasecmp("file", u->scheme) == 0) { -+ url = aprintf("file://%s%s%s", -+ u->path, -+ u->fragment? "#": "", -+ u->fragment? u->fragment : ""); -+ } -+ else if(!u->host) -+ return CURLUE_NO_HOST; -+ else { -+ if(u->scheme) -+ scheme = u->scheme; -+ else -+ return CURLUE_NO_SCHEME; -+ -+ options = NULL; -+ -+ url = aprintf("%s://%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s", -+ scheme, -+ u->user ? u->user : "", -+ u->password ? ":": "", -+ u->password ? u->password : "", -+ options ? ";" : "", -+ options ? options : "", -+ (u->user || u->password || options) ? "@": "", -+ u->host, -+ port ? ":": "", -+ port ? port : "", -+ (u->path && (u->path[0] != '/')) ? "/": "", -+ u->path ? u->path : "/", -+ u->query? "?": "", -+ u->query? u->query : "", -+ u->fragment? "#": "", -+ u->fragment? u->fragment : ""); -+ } -+ if(!url) -+ return CURLUE_OUT_OF_MEMORY; -+ *part = url; -+ return CURLUE_OK; -+ break; -+ } -+ default: -+ ptr = NULL; -+ } -+ if(ptr) { -+ *part = strdup(ptr); -+ if(!*part) -+ return CURLUE_OUT_OF_MEMORY; -+ if(plusdecode) { -+ /* convert + to space */ -+ char *plus; -+ for(plus = *part; *plus; ++plus) { -+ if(*plus == '+') -+ *plus = ' '; -+ } -+ } -+ if(urldecode) { -+ char *decoded; -+ size_t dlen; -+ CURLcode res = Curl_urldecode(*part, 0, &decoded, &dlen, true); -+ free(*part); -+ if(res) { -+ *part = NULL; -+ return CURLUE_URLDECODE; -+ } -+ *part = decoded; -+ } -+ return CURLUE_OK; -+ } -+ else -+ return ifmissing; -+} -+ -+CURLUcode curl_url_set(struct Curl_URL *u, CURLUPart what, -+ const char *part, unsigned int flags) -+{ -+ char **storep = NULL; -+ long port = 0; -+ bool urlencode = (flags & CURLU_URLENCODE)? 1 : 0; -+ bool plusencode = false; -+ bool urlskipslash = false; -+ bool appendquery = false; -+ bool equalsencode = false; -+ -+ if(!u) -+ return CURLUE_BAD_HANDLE; -+ if(!part) { -+ /* setting a part to NULL clears it */ -+ switch(what) { -+ case CURLUPART_URL: -+ break; -+ case CURLUPART_SCHEME: -+ storep = &u->scheme; -+ break; -+ case CURLUPART_USER: -+ storep = &u->user; -+ break; -+ case CURLUPART_PASSWORD: -+ storep = &u->password; -+ break; -+ case CURLUPART_OPTIONS: -+ storep = &u->options; -+ break; -+ case CURLUPART_HOST: -+ storep = &u->host; -+ break; -+ case CURLUPART_PORT: -+ storep = &u->port; -+ break; -+ case CURLUPART_PATH: -+ storep = &u->path; -+ break; -+ case CURLUPART_QUERY: -+ storep = &u->query; -+ break; -+ case CURLUPART_FRAGMENT: -+ storep = &u->fragment; -+ break; -+ default: -+ return CURLUE_UNKNOWN_PART; -+ } -+ if(storep && *storep) { -+ free(*storep); -+ *storep = NULL; -+ } -+ return CURLUE_OK; -+ } -+ -+ switch(what) { -+ case CURLUPART_SCHEME: -+ storep = &u->scheme; -+ urlencode = false; /* never */ -+ break; -+ case CURLUPART_USER: -+ storep = &u->user; -+ break; -+ case CURLUPART_PASSWORD: -+ storep = &u->password; -+ break; -+ case CURLUPART_OPTIONS: -+ storep = &u->options; -+ break; -+ case CURLUPART_HOST: -+ storep = &u->host; -+ break; -+ case CURLUPART_PORT: -+ urlencode = false; /* never */ -+ port = strtol(part, NULL, 10); /* Port number must be decimal */ -+ if((port <= 0) || (port > 0xffff)) -+ return CURLUE_BAD_PORT_NUMBER; -+ storep = &u->port; -+ break; -+ case CURLUPART_PATH: -+ urlskipslash = true; -+ storep = &u->path; -+ break; -+ case CURLUPART_QUERY: -+ plusencode = urlencode; -+ appendquery = (flags & CURLU_APPENDQUERY)?1:0; -+ equalsencode = appendquery; -+ storep = &u->query; -+ break; -+ case CURLUPART_FRAGMENT: -+ storep = &u->fragment; -+ break; -+ case CURLUPART_URL: { -+ /* -+ * Allow a new URL to replace the existing (if any) contents. -+ * -+ * If the existing contents is enough for a URL, allow a relative URL to -+ * replace it. -+ */ -+ CURLUcode result; -+ char *oldurl; -+ char *redired_url; -+ struct Curl_URL *handle2; -+ -+ if(Curl_is_absolute_url(part, NULL, MAX_SCHEME_LEN)) { -+ handle2 = curl_url(); -+ if(!handle2) -+ return CURLUE_OUT_OF_MEMORY; -+ result = parseurl(part, handle2, flags); -+ if(!result) -+ mv_urlhandle(handle2, u); -+ else -+ curl_url_cleanup(handle2); -+ return result; -+ } -+ /* extract the full "old" URL to do the redirect on */ -+ result = curl_url_get(u, CURLUPART_URL, &oldurl, flags); -+ if(result) { -+ /* couldn't get the old URL, just use the new! */ -+ handle2 = curl_url(); -+ if(!handle2) -+ return CURLUE_OUT_OF_MEMORY; -+ result = parseurl(part, handle2, flags); -+ if(!result) -+ mv_urlhandle(handle2, u); -+ else -+ curl_url_cleanup(handle2); -+ return result; -+ } -+ -+ /* apply the relative part to create a new URL */ -+ redired_url = Curl_concat_url(oldurl, part); -+ free(oldurl); -+ if(!redired_url) -+ return CURLUE_OUT_OF_MEMORY; -+ -+ /* now parse the new URL */ -+ handle2 = curl_url(); -+ if(!handle2) { -+ free(redired_url); -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ result = parseurl(redired_url, handle2, flags); -+ free(redired_url); -+ if(!result) -+ mv_urlhandle(handle2, u); -+ else -+ curl_url_cleanup(handle2); -+ return result; -+ } -+ default: -+ return CURLUE_UNKNOWN_PART; -+ } -+ if(storep) { -+ const char *newp = part; -+ size_t nalloc = strlen(part); -+ -+ if(urlencode) { -+ const char *i; -+ char *o; -+ bool free_part = false; -+ char *enc = malloc(nalloc * 3 + 1); /* for worst case! */ -+ if(!enc) -+ return CURLUE_OUT_OF_MEMORY; -+ if(plusencode) { -+ /* space to plus */ -+ i = part; -+ for(o = enc; *i; ++o, ++i) -+ *o = (*i == ' ') ? '+' : *i; -+ *o = 0; /* zero terminate */ -+ part = strdup(enc); -+ if(!part) { -+ free(enc); -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ free_part = true; -+ } -+ for(i = part, o = enc; *i; i++) { -+ if(Curl_isunreserved(*i) || -+ ((*i == '/') && urlskipslash) || -+ ((*i == '=') && equalsencode) || -+ ((*i == '+') && plusencode)) { -+ if((*i == '=') && equalsencode) -+ /* only skip the first equals sign */ -+ equalsencode = false; -+ *o = *i; -+ o++; -+ } -+ else { -+ snprintf(o, 4, "%%%02x", *i); -+ o += 3; -+ } -+ } -+ *o = 0; /* zero terminate */ -+ newp = enc; -+ if(free_part) -+ free((char *)part); -+ } -+ else { -+ char *p; -+ newp = strdup(part); -+ if(!newp) -+ return CURLUE_OUT_OF_MEMORY; -+ p = (char *)newp; -+ while(*p) { -+ /* make sure percent encoded are lower case */ -+ if((*p == '%') && isxdigit(p[1]) && isxdigit(p[2]) && -+ (isupper(p[1]) || isupper(p[2]))) { -+ p[1] = (char)tolower(p[1]); -+ p[2] = (char)tolower(p[2]); -+ p += 3; -+ } -+ else -+ p++; -+ } -+ } -+ -+ if(appendquery) { -+ /* Append the string onto the old query. Add a '&' separator if none is -+ present at the end of the exsting query already */ -+ size_t querylen = u->query ? strlen(u->query) : 0; -+ bool addamperand = querylen && (u->query[querylen -1] != '&'); -+ if(querylen) { -+ size_t newplen = strlen(newp); -+ char *p = malloc(querylen + addamperand + newplen + 1); -+ if(!p) { -+ free((char *)newp); -+ return CURLUE_OUT_OF_MEMORY; -+ } -+ strcpy(p, u->query); /* original query */ -+ if(addamperand) -+ p[querylen] = '&'; /* ampersand */ -+ strcpy(&p[querylen + addamperand], newp); /* new suffix */ -+ free((char *)newp); -+ free(*storep); -+ *storep = p; -+ return CURLUE_OK; -+ } -+ } -+ -+ free(*storep); -+ *storep = (char *)newp; -+ } -+ /* set after the string, to make it not assigned if the allocation above -+ fails */ -+ if(port) -+ u->portnum = port; -+ return CURLUE_OK; -+} diff --git a/sources/cgmnlm.git/commits/ac86b2f9fece0e39be57b81e02cb9946a10df570.patch b/sources/cgmnlm.git/commits/ac86b2f9fece0e39be57b81e02cb9946a10df570.patch @@ -1,35 +0,0 @@ -diff --git a/src/util.c b/src/util.c -index 0a479af3ea734ce25d0806aa086e61bef6b8953b..573e8a717efbe843fce003126cb932928b15f716 100644 ---- a/src/util.c -+++ b/src/util.c -@@ -68,15 +68,15 @@ int - download_resp(FILE *out, struct gemini_response resp, const char *path, - char *url) - { -- char buf[PATH_MAX]; -+ char path_buf[PATH_MAX]; - assert(path); - if (path[0] == '\0') { - path = "./"; - } - if (path[strlen(path)-1] == '/') { -- strncat(strncpy(&buf[0], path, sizeof(buf)), basename(url), -- sizeof(buf)); -- path = &buf[0]; -+ int n = snprintf(path_buf, sizeof(path_buf), "%s%s", path, basename(url)); -+ assert((size_t)n < sizeof(path_buf)); -+ path = path_buf; - } - FILE *f = fopen(path, "w"); - if (f == NULL) { -@@ -85,8 +85,9 @@ path, strerror(errno)); - return 1; - } - fprintf(out, "Downloading %s to %s\n", url, path); -+ char buf[BUFSIZ]; - for (int n = 1; n > 0;) { -- n = BIO_read(resp.bio, buf, BUFSIZ); -+ n = BIO_read(resp.bio, buf, sizeof(buf)); - if (n == -1) { - fprintf(stderr, "Error: read\n"); - return 1; diff --git a/sources/cgmnlm.git/commits/ae43b9190e1a18796222b94ec1e78b35f5826964.patch b/sources/cgmnlm.git/commits/ae43b9190e1a18796222b94ec1e78b35f5826964.patch @@ -1,111 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 82d7abc15562ad7ee4fe5f7e5349be477f719ae5..cb7e2843798f66f596e071823d9e41153a555fcc 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -19,6 +19,14 @@ #include <gmni/tofu.h> - #include <gmni/url.h> - #include "util.h" - -+#define ANSI_COLOR_RED "\x1b[31m" -+#define ANSI_COLOR_GREEN "\x1b[32m" -+#define ANSI_COLOR_YELLOW "\x1b[33m" -+#define ANSI_COLOR_BLUE "\x1b[34m" -+#define ANSI_COLOR_MAGENTA "\x1b[35m" -+#define ANSI_COLOR_CYAN "\x1b[36m" -+#define ANSI_COLOR_RESET "\x1b[0m" -+ - struct link { - char *url; - struct link *next; -@@ -767,7 +775,7 @@ if (searching) { - out = fopen("/dev/null", "w+"); - } - -- fprintf(out, "\n\n"); -+ fprintf(out, "\n"); - char *text = NULL; - int row = 0, col = 0; - struct gemini_token tok; -@@ -776,20 +784,20 @@ while (text != NULL || gemini_parser_next(&p, &tok) == 0) { - repeat: - switch (tok.token) { - case GEMINI_TEXT: -- col += fprintf(out, " "); -+ col += fprintf(out, " "); - if (text == NULL) { - text = tok.text; - } - break; - case GEMINI_LINK: - if (text == NULL) { -- col += fprintf(out, "%d) ", nlinks++); -+ col += fprintf(out, "%2d> %s", nlinks++, ANSI_COLOR_CYAN); - text = trim_ws(tok.link.text ? tok.link.text : tok.link.url); - *next = calloc(1, sizeof(struct link)); - (*next)->url = strdup(trim_ws(tok.link.url)); - next = &(*next)->next; - } else { -- col += fprintf(out, " "); -+ col += fprintf(out, " "); - } - break; - case GEMINI_PREFORMATTED_BEGIN: -@@ -799,6 +807,7 @@ case GEMINI_PREFORMATTED_END: - continue; // Not used - case GEMINI_PREFORMATTED_TEXT: - if (text == NULL) { -+ fprintf(out, " "); - text = tok.preformatted; - } - break; -@@ -807,34 +816,33 @@ if (!browser->page_title) { - browser->page_title = strdup(tok.heading.title); - } - if (text == NULL) { -- for (int n = tok.heading.level; n; --n) { -- col += fprintf(out, "#"); -- } - switch (tok.heading.level) { - case 1: -- col += fprintf(out, " "); -+ col += fprintf(out, "%s%s", " # ", ANSI_COLOR_RED); - break; - case 2: -+ col += fprintf(out, "%s%s", " ## ", ANSI_COLOR_YELLOW); -+ break; - case 3: -- col += fprintf(out, " "); -+ col += fprintf(out, "%s%s", "### ", ANSI_COLOR_GREEN); - break; - } - text = trim_ws(tok.heading.title); - } else { -- col += fprintf(out, " "); -+ col += fprintf(out, " "); - } - break; - case GEMINI_LIST_ITEM: - if (text == NULL) { -- col += fprintf(out, " %s ", -+ col += fprintf(out, " %s ", - browser->unicode ? "•" : "*"); - text = trim_ws(tok.list_item); - } else { -- col += fprintf(out, " "); -+ col += fprintf(out, " "); - } - break; - case GEMINI_QUOTE: -- col += fprintf(out, " %s ", -+ col += fprintf(out, " %s ", - browser->unicode ? "┃" : ">"); - if (text == NULL) { - text = trim_ws(tok.quote_text); -@@ -878,6 +886,7 @@ ++row; - } - ++row; col = 0; - -+ fprintf(out, ANSI_COLOR_RESET); - if (browser->pagination && row >= ws.ws_row - 4) { - char prompt[4096]; - char *end = NULL; diff --git a/sources/cgmnlm.git/commits/afab58cb64f205ce9f469a328a7477b808b0c76c.patch b/sources/cgmnlm.git/commits/afab58cb64f205ce9f469a328a7477b808b0c76c.patch @@ -1,30 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 5d7892b83f7914da46b08100014db6211e36173a..245ec85fa59fbcabde7e49c7d4d6e978aefa5f9a 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -908,10 +908,21 @@ int row = 0, col = 0; - ioctl(fileno(browser->tty), TIOCGWINSZ, &ws); - - char buf[BUFSIZ]; -- int n; -- while ((n = BIO_read(resp->bio, buf, sizeof(buf)) != 0)) { -- while (n) { -- n -= fwrite(buf, 1, n, browser->tty); -+ for (int n = 1; n > 0;) { -+ n = BIO_read(resp->bio, buf, BUFSIZ); -+ if (n == -1) { -+ fprintf(stderr, "Error: read\n"); -+ return 1; -+ } -+ 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; - } - } - diff --git a/sources/cgmnlm.git/commits/b050b9e467589561b1203f99e9f58c990c824b1a.patch b/sources/cgmnlm.git/commits/b050b9e467589561b1203f99e9f58c990c824b1a.patch @@ -1,24 +0,0 @@ -diff --git a/src/gmnlm.c b/src/gmnlm.c -index 92e866a2a58a222f56993e57849c348ead658ab4..25afbaf0ce2621c4559fd34a035a9f944485aa3d 100644 ---- a/src/gmnlm.c -+++ b/src/gmnlm.c -@@ -696,11 +696,19 @@ if (!input) { - requesting = false; - break; - } -+ if (input[0] == '\0' && browser->history->prev) { -+ free(input); -+ browser->history = browser->history->prev; -+ set_url(browser, browser->history->url, NULL); -+ break; -+ } - - char *new_url = gemini_input_url( - browser->plain_url, input); -+ free(input); - assert(new_url); - set_url(browser, new_url, NULL); -+ free(new_url); - break; - case GEMINI_STATUS_CLASS_REDIRECT: - if (++nredir >= 5) { diff --git a/sources/cgmnlm.git/commits/b136eea98a33ffcaf2d965e907dd6799078d2110.patch b/sources/cgmnlm.git/commits/b136eea98a33ffcaf2d965e907dd6799078d2110.patch @@ -1,144 +0,0 @@ -diff --git a/Makefile b/Makefile -index 091f426bd85ae2880fb07f3df9d8e2fa63621412..0070b703ea0af36743878d2264259bc04edb6b70 100644 ---- a/Makefile -+++ b/Makefile -@@ -8,7 +8,9 @@ gmnic: $(gmnic_objects) - @printf 'CCLD\t$@\n' - @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmnic_objects) - --.SUFFIXES: .c .o -+doc/gmnic.1: doc/gmnic.scd -+ -+.SUFFIXES: .c .o .scd .1 - - .c.o: - @printf 'CC\t$@\n' -@@ -17,10 +19,16 @@ @grep $< $(OUTDIR)/cppcache >/dev/null || \ - $(CPP) $(CFLAGS) -MM -MT $@ $< >> $(OUTDIR)/cppcache - @$(CC) -c $(CFLAGS) -o $@ $< - -+.scd.1: -+ @printf 'SCDOC\t$@\n' -+ @$(SCDOC) < $< > $@ -+ -+docs: doc/gmnic.1 -+ - clean: - @rm -f gmnic - - distclean: clean - @rm -rf "$(OUTDIR)" - --.PHONY: clean distclean -+.PHONY: clean distclean docs -diff --git a/config.sh b/config.sh -index ef533ec79238744458fb2e69b95e3d34333c0e5a..b93815ada4a25ec508e7a86cc79b9e9be3eba428 100644 ---- a/config.sh -+++ b/config.sh -@@ -5,6 +5,7 @@ AS=${AS:-as} - CC=${CC:-cc} - CFLAGS=${CFLAGS:-} - LD=${LD:-ld} -+SCDOC=${SCDOC:-scdoc} - - for arg - do -@@ -104,9 +105,19 @@ - find_library OpenSSL libssl - find_library OpenSSL libcrypto - -+ printf "Checking for scdoc... " -+ if scdoc -v >/dev/null 2>&1 -+ then -+ echo yes -+ all="$all docs" -+ else -+ echo no -+ fi -+ - printf "Creating $outdir/config.mk... " - cat <<-EOF > "$outdir"/config.mk - CC=$CC -+ SCDOC=$SCDOC - LIBS=$LIBS - PREFIX=${PREFIX:-/usr/local} - OUTDIR=${outdir} -@@ -153,6 +164,7 @@ - printf "Populating build dir... " - populate "$srcdir/include" - populate "$srcdir/src&qu