Make the serving process interruptible - quark - quark web server
 (HTM) git clone git://git.suckless.org/quark
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) LICENSE
       ---
 (DIR) commit 2714819dfc639098d0531eb3d4f0f5f23708059a
 (DIR) parent 0823ba4c3e480fb5e2c246b8ac6c4783d866ab87
 (HTM) Author: Laslo Hunhold <dev@frign.de>
       Date:   Mon, 14 Sep 2020 13:45:24 +0200
       
       Make the serving process interruptible
       
       Ever since I joined suckless and found out that there had been an
       (inofficial and cancelled) effort to turn quark into a polling-webserver
       (instead of a forking-webserver), I was intrigued to pick up the task
       and make it happen.
       
       Back then, my C skills weren't nearly as good, and I had no hopes of
       making it possible. Now, this commit marks a major step towards this
       goal.
       
       Given the static nature of quark, I wanted to try something out that
       is not really possible with a "dynamic" server: Making the serving
       process interruptible in constant memory (except dir-listings of
       course). This can easily be extended to a polling architecture later
       on, but it most importantly warrants a non-blocking I/O scheme and
       makes the server more or less immune to sloth attacks (i.e. clients
       sending requests very slowly), and provides a more flexible approach to
       connections. Any thread can pick up a connection and continue work on
       it, without requiring a separate process for each (which might hit the
       forking limit at some point). If we hit a point where all connections
       are busy (due to many sloth attacks), one can apply arbitrary complex
       logic to "cancel" connections that show malicious behaviour (e.g. taking
       a long time to send the request header, etc.).
       
       The following aspects were added/changed to introduce the
       interruptibility.
       
        - Define a general purpose "buffer" struct with a buffer_appendf()
          utility function.
        - Change http_send_header() to http_prepare_header_buf() and separate
          the sending part into a general-purpose function http_send_buf().
        - Modify the data_* functions to be based on a progress and operate
          on buffers. This way, we can indefinitely "interrupt" request
          serving and always "pick up" where we left off.
        - Refactor http_recv_header() to operate on the buffer struct instead
          of "raw" parameters.
        - Refactor serve() in main.c accordingly.
        - Introduce BUFFER_SIZE in config.h, which controls the buffer size each
          connection has.
        - Refactor Makefile dependencies and employ strict first-level-header-
          usage (i.e. we explicitly specify what we use with includes in each
          compilation unit, so make(1) can figure the dependencies out; most
          prominently, this moves the arg.h-include into main.c, and requires
          ifdef-guards for config.h).
       
       Signed-off-by: Laslo Hunhold <dev@frign.de>
       
       Diffstat:
         M Makefile                            |       6 +++---
         M config.def.h                        |       9 +++++++--
         M data.c                              |     131 ++++++++++++++++++-------------
         M data.h                              |      13 ++++++++++---
         M http.c                              |     120 ++++++++++++++++++-------------
         M http.h                              |      18 ++++++------------
         M main.c                              |      61 ++++++++++++++++++++++++++-----
         M util.c                              |      24 ++++++++++++++++++++++++
         M util.h                              |      11 ++++++++++-
       
       9 files changed, 260 insertions(+), 133 deletions(-)
       ---
 (DIR) diff --git a/Makefile b/Makefile
       @@ -8,9 +8,9 @@ COMPONENTS = data http sock util
        
        all: quark
        
       -data.o: data.c data.h util.h http.h config.mk
       -http.o: http.c http.h util.h http.h data.h config.h config.mk
       -main.o: main.c util.h sock.h http.h arg.h config.h config.mk
       +data.o: data.c data.h http.h util.h config.mk
       +http.o: http.c config.h http.h util.h config.mk
       +main.o: main.c arg.h data.h http.h sock.h util.h config.mk
        sock.o: sock.c sock.h util.h config.mk
        util.o: util.c util.h config.mk
        
 (DIR) diff --git a/config.def.h b/config.def.h
       @@ -1,5 +1,8 @@
       -#define HEADER_MAX 4096
       -#define FIELD_MAX  200
       +#ifndef CONFIG_H
       +#define CONFIG_H
       +
       +#define BUFFER_SIZE 4096
       +#define FIELD_MAX   200
        
        /* mime-types */
        static const struct {
       @@ -32,3 +35,5 @@ static const struct {
                { "ogv",   "video/ogg" },
                { "webm",  "video/webm" },
        };
       +
       +#endif /* CONFIG_H */
 (DIR) diff --git a/data.c b/data.c
       @@ -7,10 +7,17 @@
        #include <time.h>
        #include <unistd.h>
        
       -#include "http.h"
        #include "data.h"
       +#include "http.h"
        #include "util.h"
        
       +enum status (* const data_fct[])(const struct response *,
       +                                 struct buffer *, size_t *) = {
       +        [RESTYPE_ERROR]      = data_prepare_error_buf,
       +        [RESTYPE_FILE]       = data_prepare_file_buf,
       +        [RESTYPE_DIRLISTING] = data_prepare_dirlisting_buf,
       +};
       +
        static int
        compareent(const struct dirent **d1, const struct dirent **d2)
        {
       @@ -84,7 +91,8 @@ html_escape(const char *src, char *dst, size_t dst_siz)
        }
        
        enum status
       -data_send_dirlisting(int fd, const struct response *res)
       +data_prepare_dirlisting_buf(const struct response *res,
       +                            struct buffer *buf, size_t *progress)
        {
                enum status ret = 0;
                struct dirent **e;
       @@ -92,24 +100,29 @@ data_send_dirlisting(int fd, const struct response *res)
                int dirlen;
                char esc[PATH_MAX /* > NAME_MAX */ * 6]; /* strlen("&...;") <= 6 */
        
       +        /* reset buffer */
       +        memset(buf, 0, sizeof(*buf));
       +
                /* read directory */
                if ((dirlen = scandir(res->path, &e, NULL, compareent)) < 0) {
                        return S_FORBIDDEN;
                }
        
       -        /* listing header (we use esc because sizeof(esc) >= PATH_MAX) */
       -        html_escape(res->uri, esc, MIN(PATH_MAX, sizeof(esc)));
       -        if (dprintf(fd,
       -                    "<!DOCTYPE html>\n<html>\n\t<head>"
       -                    "<title>Index of %s</title></head>\n"
       -                    "\t<body>\n\t\t<a href=\"..\">..</a>",
       -                    esc) < 0) {
       -                ret = S_REQUEST_TIMEOUT;
       -                goto cleanup;
       +        if (*progress == 0) {
       +                /* write listing header (sizeof(esc) >= PATH_MAX) */
       +                html_escape(res->uri, esc, MIN(PATH_MAX, sizeof(esc)));
       +                if (buffer_appendf(buf,
       +                                   "<!DOCTYPE html>\n<html>\n\t<head>"
       +                                   "<title>Index of %s</title></head>\n"
       +                                   "\t<body>\n\t\t<a href=\"..\">..</a>",
       +                                   esc) < 0) {
       +                        ret = S_REQUEST_TIMEOUT;
       +                        goto cleanup;
       +                }
                }
        
       -        /* listing */
       -        for (i = 0; i < (size_t)dirlen; i++) {
       +        /* listing entries */
       +        for (i = *progress; i < (size_t)dirlen; i++) {
                        /* skip hidden files, "." and ".." */
                        if (e[i]->d_name[0] == '.') {
                                continue;
       @@ -117,20 +130,25 @@ data_send_dirlisting(int fd, const struct response *res)
        
                        /* entry line */
                        html_escape(e[i]->d_name, esc, sizeof(esc));
       -                if (dprintf(fd, "<br />\n\t\t<a href=\"%s%s\">%s%s</a>",
       -                            esc,
       -                            (e[i]->d_type == DT_DIR) ? "/" : "",
       -                            esc,
       -                            suffix(e[i]->d_type)) < 0) {
       -                        ret = S_REQUEST_TIMEOUT;
       -                        goto cleanup;
       +                if (buffer_appendf(buf,
       +                                   "<br />\n\t\t<a href=\"%s%s\">%s%s</a>",
       +                                   esc,
       +                                   (e[i]->d_type == DT_DIR) ? "/" : "",
       +                                   esc,
       +                                   suffix(e[i]->d_type))) {
       +                        /* buffer full */
       +                        break;
                        }
                }
       +        *progress = i;
        
       -        /* listing footer */
       -        if (dprintf(fd, "\n\t</body>\n</html>\n") < 0) {
       -                ret = S_REQUEST_TIMEOUT;
       -                goto cleanup;
       +        if (*progress == (size_t)dirlen) {
       +                /* listing footer */
       +                if (buffer_appendf(buf, "\n\t</body>\n</html>\n") < 0) {
       +                        ret = S_REQUEST_TIMEOUT;
       +                        goto cleanup;
       +                }
       +                (*progress)++;
                }
        
        cleanup:
       @@ -143,28 +161,40 @@ cleanup:
        }
        
        enum status
       -data_send_error(int fd, const struct response *res)
       +data_prepare_error_buf(const struct response *res, struct buffer *buf,
       +                   size_t *progress)
        {
       -        if (dprintf(fd,
       -                    "<!DOCTYPE html>\n<html>\n\t<head>\n"
       -                    "\t\t<title>%d %s</title>\n\t</head>\n\t<body>\n"
       -                    "\t\t<h1>%d %s</h1>\n\t</body>\n</html>\n",
       -                    res->status, status_str[res->status],
       -                    res->status, status_str[res->status]) < 0) {
       -                return S_REQUEST_TIMEOUT;
       +        /* reset buffer */
       +        memset(buf, 0, sizeof(*buf));
       +
       +        if (*progress == 0) {
       +                /* write error body */
       +                if (buffer_appendf(buf,
       +                                   "<!DOCTYPE html>\n<html>\n\t<head>\n"
       +                                   "\t\t<title>%d %s</title>\n\t</head>\n"
       +                                   "\t<body>\n\t\t<h1>%d %s</h1>\n"
       +                                   "\t</body>\n</html>\n",
       +                                   res->status, status_str[res->status],
       +                                   res->status, status_str[res->status])) {
       +                        return S_INTERNAL_SERVER_ERROR;
       +                }
       +                (*progress)++;
                }
        
                return 0;
        }
        
        enum status
       -data_send_file(int fd, const struct response *res)
       +data_prepare_file_buf(const struct response *res, struct buffer *buf,
       +                  size_t *progress)
        {
                FILE *fp;
                enum status ret = 0;
       -        ssize_t bread, bwritten;
       +        ssize_t r;
                size_t remaining;
       -        static char buf[BUFSIZ], *p;
       +
       +        /* reset buffer */
       +        memset(buf, 0, sizeof(*buf));
        
                /* open file */
                if (!(fp = fopen(res->path, "r"))) {
       @@ -172,33 +202,26 @@ data_send_file(int fd, const struct response *res)
                        goto cleanup;
                }
        
       -        /* seek to lower bound */
       -        if (fseek(fp, res->file.lower, SEEK_SET)) {
       +        /* seek to lower bound + progress */
       +        if (fseek(fp, res->file.lower + *progress, SEEK_SET)) {
                        ret = S_INTERNAL_SERVER_ERROR;
                        goto cleanup;
                }
        
       -        /* write data until upper bound is hit */
       -        remaining = res->file.upper - res->file.lower + 1;
       -
       -        while ((bread = fread(buf, 1, MIN(sizeof(buf),
       -                              remaining), fp))) {
       -                if (bread < 0) {
       +        /* read data into buf */
       +        remaining = res->file.upper - res->file.lower + 1 - *progress;
       +        while ((r = fread(buf->data + buf->len, 1,
       +                          MIN(sizeof(buf->data) - buf->len,
       +                          remaining), fp))) {
       +                if (r < 0) {
                                ret = S_INTERNAL_SERVER_ERROR;
                                goto cleanup;
                        }
       -                remaining -= bread;
       -                p = buf;
       -                while (bread > 0) {
       -                        bwritten = write(fd, p, bread);
       -                        if (bwritten <= 0) {
       -                                ret = S_REQUEST_TIMEOUT;
       -                                goto cleanup;
       -                        }
       -                        bread -= bwritten;
       -                        p += bwritten;
       -                }
       +                buf->len += r;
       +                *progress += r;
       +                remaining -= r;
                }
       +
        cleanup:
                if (fp) {
                        fclose(fp);
 (DIR) diff --git a/data.h b/data.h
       @@ -3,9 +3,16 @@
        #define DATA_H
        
        #include "http.h"
       +#include "util.h"
        
       -enum status data_send_dirlisting(int, const struct response *);
       -enum status data_send_error(int, const struct response *);
       -enum status data_send_file(int, const struct response *);
       +extern enum status (* const data_fct[])(const struct response *,
       +                                        struct buffer *, size_t *);
       +
       +enum status data_prepare_dirlisting_buf(const struct response *,
       +                                    struct buffer *, size_t *);
       +enum status data_prepare_error_buf(const struct response *,
       +                                   struct buffer *, size_t *);
       +enum status data_prepare_file_buf(const struct response *,
       +                              struct buffer *, size_t *);
        
        #endif /* DATA_H */
 (DIR) diff --git a/http.c b/http.c
       @@ -17,7 +17,6 @@
        #include <unistd.h>
        
        #include "config.h"
       -#include "data.h"
        #include "http.h"
        #include "util.h"
        
       @@ -58,43 +57,69 @@ const char *res_field_str[] = {
                [RES_CONTENT_TYPE]   = "Content-Type",
        };
        
       -enum status (* const body_fct[])(int, const struct response *) = {
       -        [RESTYPE_ERROR]      = data_send_error,
       -        [RESTYPE_FILE]       = data_send_file,
       -        [RESTYPE_DIRLISTING] = data_send_dirlisting,
       -};
       -
        enum status
       -http_send_header(int fd, const struct response *res)
       +http_prepare_header_buf(const struct response *res, struct buffer *buf)
        {
       -        char t[FIELD_MAX];
       +        char tstmp[FIELD_MAX];
                size_t i;
        
       -        if (timestamp(t, sizeof(t), time(NULL))) {
       -                return S_INTERNAL_SERVER_ERROR;
       +        /* reset buffer */
       +        memset(buf, 0, sizeof(*buf));
       +
       +        /* generate timestamp */
       +        if (timestamp(tstmp, sizeof(tstmp), time(NULL))) {
       +                goto err;
                }
        
       -        if (dprintf(fd,
       -                    "HTTP/1.1 %d %s\r\n"
       -                    "Date: %s\r\n"
       -                    "Connection: close\r\n",
       -                    res->status, status_str[res->status], t) < 0) {
       -                return S_REQUEST_TIMEOUT;
       +        /* write data */
       +        if (buffer_appendf(buf,
       +                           "HTTP/1.1 %d %s\r\n"
       +                           "Date: %s\r\n"
       +                           "Connection: close\r\n",
       +                           res->status, status_str[res->status], tstmp)) {
       +                goto err;
                }
        
                for (i = 0; i < NUM_RES_FIELDS; i++) {
       -                if (res->field[i][0] != '\0') {
       -                        if (dprintf(fd, "%s: %s\r\n", res_field_str[i],
       -                                    res->field[i]) < 0) {
       -                                return S_REQUEST_TIMEOUT;
       -                        }
       +                if (res->field[i][0] != '\0' &&
       +                    buffer_appendf(buf, "%s: %s\r\n", res_field_str[i],
       +                                   res->field[i])) {
       +                        goto err;
                        }
                }
        
       -        if (dprintf(fd, "\r\n") < 0) {
       -                return S_REQUEST_TIMEOUT;
       +        if (buffer_appendf(buf, "\r\n")) {
       +                goto err;
       +        }
       +
       +        return 0;
       +err:
       +        memset(buf, 0, sizeof(*buf));
       +        return S_INTERNAL_SERVER_ERROR;
       +}
       +
       +enum status
       +http_send_buf(int fd, struct buffer *buf)
       +{
       +        size_t remaining;
       +        ssize_t r;
       +
       +        if (buf == NULL || buf->off > sizeof(buf->data)) {
       +                return S_INTERNAL_SERVER_ERROR;
       +        }
       +
       +        remaining = buf->len - buf->off;
       +        while (remaining > 0) {
       +                if ((r = write(fd, buf->data + buf->off, remaining)) <= 0) {
       +                        return S_REQUEST_TIMEOUT;
       +                }
       +                buf->off += r;
       +                remaining -= r;
                }
        
       +        /* set off to 0 to indicate that we have finished */
       +        buf->off = 0;
       +
                return 0;
        }
        
       @@ -117,38 +142,48 @@ decode(const char src[PATH_MAX], char dest[PATH_MAX])
        }
        
        enum status
       -http_recv_header(int fd, char *h, size_t hsiz, size_t *off)
       +http_recv_header(int fd, struct buffer *buf)
        {
       +        enum status s;
                ssize_t r;
        
       -        if (h == NULL || off == NULL || *off > hsiz) {
       -                return S_INTERNAL_SERVER_ERROR;
       +        if (buf->off > sizeof(buf->data)) {
       +                s = S_INTERNAL_SERVER_ERROR;
       +                goto err;
                }
        
                while (1) {
       -                if ((r = read(fd, h + *off, hsiz - *off)) <= 0) {
       -                        return S_REQUEST_TIMEOUT;
       +                if ((r = read(fd, buf->data + buf->off,
       +                              sizeof(buf->data) - buf->off)) <= 0) {
       +                        s = S_REQUEST_TIMEOUT;
       +                        goto err;
                        }
       -                *off += r;
       +                buf->off += r;
        
                        /* check if we are done (header terminated) */
       -                if (*off >= 4 && !memcmp(h + *off - 4, "\r\n\r\n", 4)) {
       +                if (buf->off >= 4 && !memcmp(buf->data + buf->off - 4,
       +                                             "\r\n\r\n", 4)) {
                                break;
                        }
        
                        /* buffer is full or read over, but header is not terminated */
       -                if (r == 0 || *off == hsiz) {
       -                        return S_REQUEST_TOO_LARGE;
       +                if (r == 0 || buf->off == sizeof(buf->data)) {
       +                        s = S_REQUEST_TOO_LARGE;
       +                        goto err;
                        }
                }
        
                /* header is complete, remove last \r\n and null-terminate */
       -        h[*off - 2] = '\0';
       +        buf->data[buf->off - 2] = '\0';
        
       -        /* set *off to 0 to indicate we are finished */
       -        *off = 0;
       +        /* set buffer length to length and offset to 0 to indicate success */
       +        buf->len = buf->off - 2;
       +        buf->off = 0;
        
                return 0;
       +err:
       +        memset(buf, 0, sizeof(*buf));
       +        return s;
        }
        
        enum status
       @@ -840,16 +875,3 @@ http_prepare_error_response(const struct request *req,
                        }
                }
        }
       -
       -enum status
       -http_send_body(int fd, const struct response *res,
       -               const struct request *req)
       -{
       -        enum status s;
       -
       -        if (req->method == M_GET && (s = body_fct[res->type](fd, res))) {
       -                return s;
       -        }
       -
       -        return 0;
       -}
 (DIR) diff --git a/http.h b/http.h
       @@ -5,11 +5,9 @@
        #include <limits.h>
        #include <sys/socket.h>
        
       +#include "config.h"
        #include "util.h"
        
       -#define HEADER_MAX 4096
       -#define FIELD_MAX 200
       -
        enum req_field {
                REQ_HOST,
                REQ_RANGE,
       @@ -83,8 +81,6 @@ struct response {
                } file;
        };
        
       -extern enum status (* const body_fct[])(int, const struct response *);
       -
        enum conn_state {
                C_VACANT,
                C_RECV_HEADER,
       @@ -97,21 +93,19 @@ struct connection {
                enum conn_state state;
                int fd;
                struct sockaddr_storage ia;
       -        char header[HEADER_MAX]; /* general req/res-header buffer */
       -        size_t off;              /* general offset (header/file/dir) */
                struct request req;
                struct response res;
       +        struct buffer buf;
       +        size_t progress;
        };
        
       -enum status http_send_header(int, const struct response *);
       -enum status http_send_status(int, enum status);
       -enum status http_recv_header(int, char *, size_t, size_t *);
       +enum status http_prepare_header_buf(const struct response *, struct buffer *);
       +enum status http_send_buf(int, struct buffer *);
       +enum status http_recv_header(int, struct buffer *);
        enum status http_parse_header(const char *, struct request *);
        void http_prepare_response(const struct request *, struct response *,
                                   const struct server *);
        void http_prepare_error_response(const struct request *,
                                         struct response *, enum status);
       -enum status http_send_body(int, const struct response *,
       -                           const struct request *);
        
        #endif /* HTTP_H */
 (DIR) diff --git a/main.c b/main.c
       @@ -16,6 +16,7 @@
        #include <time.h>
        #include <unistd.h>
        
       +#include "arg.h"
        #include "data.h"
        #include "http.h"
        #include "sock.h"
       @@ -53,24 +54,66 @@ serve(struct connection *c, const struct server *srv)
        
                /* set connection timeout */
                if (sock_set_timeout(c->fd, 30)) {
       -                goto cleanup;
       +                warn("sock_set_timeout: Failed");
                }
        
       -        /* handle request */
       -        if ((s = http_recv_header(c->fd, c->header, LEN(c->header), &c->off)) ||
       -            (s = http_parse_header(c->header, &c->req))) {
       +        /* read header */
       +        memset(&c->buf, 0, sizeof(c->buf));
       +        if ((s = http_recv_header(c->fd, &c->buf))) {
                        http_prepare_error_response(&c->req, &c->res, s);
       -        } else {
       -                http_prepare_response(&c->req, &c->res, srv);
       +                goto response;
                }
        
       -        if ((s = http_send_header(c->fd, &c->res)) ||
       -            (s = http_send_body(c->fd, &c->res, &c->req))) {
       +        /* parse header */
       +        if ((s = http_parse_header(c->buf.data, &c->req))) {
       +                http_prepare_error_response(&c->req, &c->res, s);
       +                goto response;
       +        }
       +
       +        /* prepare response struct */
       +        http_prepare_response(&c->req, &c->res, srv);
       +
       +response:
       +        /* generate response header */
       +        if ((s = http_prepare_header_buf(&c->res, &c->buf))) {
       +                http_prepare_error_response(&c->req, &c->res, s);
       +                if ((s = http_prepare_header_buf(&c->res, &c->buf))) {
       +                        /* couldn't generate the header, we failed for good */
       +                        c->res.status = s;
       +                        goto err;
       +                }
       +        }
       +
       +        /* send header */
       +        if ((s = http_send_buf(c->fd, &c->buf))) {
                        c->res.status = s;
       +                goto err;
                }
        
       +        /* send body */
       +        if (c->req.method == M_GET) {
       +                for (;;) {
       +                        /* fill buffer with body data */
       +                        if ((s = data_fct[c->res.type](&c->res, &c->buf,
       +                                                       &c->progress))) {
       +                                c->res.status = s;
       +                                goto err;
       +                        }
       +
       +                        /* if done, exit loop */
       +                        if (c->buf.len == 0) {
       +                                break;
       +                        }
       +
       +                        /* send buffer */
       +                        if ((s = http_send_buf(c->fd, &c->buf))) {
       +                                c->res.status = s;
       +                        }
       +                }
       +        }
       +err:
                logmsg(c);
       -cleanup:
       +
                /* clean up and finish */
                shutdown(c->fd, SHUT_RD);
                shutdown(c->fd, SHUT_WR);
 (DIR) diff --git a/util.c b/util.c
       @@ -182,3 +182,27 @@ reallocarray(void *optr, size_t nmemb, size_t size)
                }
                return realloc(optr, size * nmemb);
        }
       +
       +int
       +buffer_appendf(struct buffer *buf, const char *suffixfmt, ...)
       +{
       +        va_list ap;
       +        int ret;
       +
       +        va_start(ap, suffixfmt);
       +        ret = vsnprintf(buf->data + buf->len,
       +                        sizeof(buf->data) - buf->len, suffixfmt, ap);
       +        va_end(ap);
       +
       +        if (ret < 0 || (size_t)ret >= (sizeof(buf->data) - buf->len)) {
       +                /* truncation occured, discard and error out */
       +                memset(buf->data + buf->len, 0,
       +                       sizeof(buf->data) - buf->len);
       +                return 1;
       +        }
       +
       +        /* increase buffer length by number of bytes written */
       +        buf->len += ret;
       +
       +        return 0;
       +}
 (DIR) diff --git a/util.h b/util.h
       @@ -6,7 +6,7 @@
        #include <stddef.h>
        #include <time.h>
        
       -#include "arg.h"
       +#include "config.h"
        
        /* main server struct */
        struct vhost {
       @@ -34,6 +34,13 @@ struct server {
                size_t map_len;
        };
        
       +/* general purpose buffer */
       +struct buffer {
       +        char data[BUFFER_SIZE];
       +        size_t len;
       +        size_t off;
       +};
       +
        #undef MIN
        #define MIN(x,y)  ((x) < (y) ? (x) : (y))
        #undef MAX
       @@ -56,4 +63,6 @@ int prepend(char *, size_t, const char *);
        void *reallocarray(void *, size_t, size_t);
        long long strtonum(const char *, long long, long long, const char **);
        
       +int buffer_appendf(struct buffer *, const char *, ...);
       +
        #endif /* UTIL_H */