Commit Diff


commit - 83991185828f6429132ea6dc924774b2c73d7c27
commit + 18906bcc2b3035e3483bcff2371e771da0004cae
blob - 8ad3eb7e54a09f151c2bda965bdb7b3dd61b807b
blob + e8755e42ab9311cfc4b7fe0d1ab154408818523c
--- fcgi.c
+++ fcgi.c
@@ -37,6 +37,8 @@
 #include "galileo.h"
 
 #define MIN(a, b)	((a) < (b) ? (a) : (b))
+
+#define FCGI_MAX_CONTENT_SIZE	65535
 
 struct fcgi_header {
 	unsigned char version;
@@ -576,7 +578,8 @@ fcgi_read(struct bufferevent *bev, void *d)
 				break;
 			}
 
-			clt->clt_tp = template(clt, clt_tp_puts, clt_tp_putc);
+			clt->clt_tp = template(clt, clt_write, clt->clt_buf,
+			    sizeof(clt->clt_buf));
 			if (clt->clt_tp == NULL) {
 				free(clt);
 				log_warn("template");
@@ -686,139 +689,66 @@ fcgi_free(struct fcgi *fcgi)
 
 int
 clt_flush(struct client *clt)
+{
+	return (template_flush(clt->clt_tp));
+}
+
+static inline int
+dowrite(struct client *clt, const void *data, size_t len)
 {
 	struct fcgi		*fcgi = clt->clt_fcgi;
 	struct bufferevent	*bev = fcgi->fcg_bev;
 	struct fcgi_header	 hdr;
 
-	if (clt->clt_buflen == 0)
-		return (0);
-
 	memset(&hdr, 0, sizeof(hdr));
 	hdr.version = FCGI_VERSION_1;
 	hdr.type = FCGI_STDOUT;
 	hdr.req_id0 = (clt->clt_id & 0xFF);
 	hdr.req_id1 = (clt->clt_id >> 8);
-	hdr.content_len0 = (clt->clt_buflen & 0xFF);
-	hdr.content_len1 = (clt->clt_buflen >> 8);
+	hdr.content_len0 = (len & 0xFF);
+	hdr.content_len1 = (len >> 8);
 
 	if (bufferevent_write(bev, &hdr, sizeof(hdr)) == -1 ||
-	    bufferevent_write(bev, clt->clt_buf, clt->clt_buflen) == -1) {
+	    bufferevent_write(bev, clt->clt_buf, len) == -1) {
 		fcgi_error(bev, EV_WRITE, fcgi);
 		return (-1);
 	}
 
-	clt->clt_buflen = 0;
-
 	return (0);
 }
 
 int
-clt_write(struct client *clt, const uint8_t *buf, size_t len)
+clt_write(void *arg, const void *d, size_t len)
 {
-	size_t			 left, copy;
+	struct client	*clt = arg;
+	const char	*data = d;
+	size_t		 avail;
 
 	while (len > 0) {
-		left = sizeof(clt->clt_buf) - clt->clt_buflen;
-		if (left == 0) {
-			if (clt_flush(clt) == -1)
-				return (-1);
-			left = sizeof(clt->clt_buf);
-		}
-
-		copy = MIN(left, len);
-
-		memcpy(&clt->clt_buf[clt->clt_buflen], buf, copy);
-		clt->clt_buflen += copy;
-		buf += copy;
-		len -= copy;
+		avail = MIN(len, FCGI_MAX_CONTENT_SIZE);
+		if (dowrite(clt, data, avail) == -1)
+			return (-1);
+		data += avail;
+		len -= avail;
 	}
 
 	return (0);
 }
 
 int
-clt_putc(struct client *clt, char ch)
-{
-	return (clt_write(clt, &ch, 1));
-}
-
-int
-clt_puts(struct client *clt, const char *str)
-{
-	return (clt_write(clt, str, strlen(str)));
-}
-
-int
 clt_write_bufferevent(struct client *clt, struct bufferevent *bev)
 {
 	struct evbuffer		*src = EVBUFFER_INPUT(bev);
-	size_t			 len, left, copy;
+	size_t			 len;
+	int			 ret;
 
 	len = EVBUFFER_LENGTH(src);
-	while (len > 0) {
-		left = sizeof(clt->clt_buf) - clt->clt_buflen;
-		if (left == 0) {
-			if (clt_flush(clt) == -1)
-				return (-1);
-			left = sizeof(clt->clt_buf);
-		}
-
-		copy = bufferevent_read(bev, &clt->clt_buf[clt->clt_buflen],
-		    MIN(left, len));
-		clt->clt_buflen += copy;
-
-		len = EVBUFFER_LENGTH(src);
-	}
-
-	return (0);
+	ret = clt_write(clt, EVBUFFER_DATA(src), len);
+	evbuffer_drain(src, len);
+	return (ret);
 }
 
 int
-clt_printf(struct client *clt, const char *fmt, ...)
-{
-	struct fcgi		*fcgi = clt->clt_fcgi;
-	struct bufferevent	*bev = fcgi->fcg_bev;
-	char			*str;
-	va_list			 ap;
-	int			 r;
-
-	va_start(ap, fmt);
-	r = vasprintf(&str, fmt, ap);
-	va_end(ap);
-	if (r == -1) {
-		fcgi_error(bev, EV_WRITE, fcgi);
-		return (-1);
-	}
-
-	r = clt_write(clt, str, r);
-	free(str);
-	return (r);
-}
-
-int
-clt_tp_puts(struct template *tp, const char *str)
-{
-	struct client		*clt = tp->tp_arg;
-
-	if (clt_puts(clt, str) == -1)
-		return (-1);
-
-	return (0);
-}
-
-int
-clt_tp_putc(struct template *tp, int c)
-{
-	struct client		*clt = tp->tp_arg;
-
-	if (clt_putc(clt, c) == -1)
-		return (-1);
-
-	return (0);
-}
-
-int
 fcgi_cmp(struct fcgi *a, struct fcgi *b)
 {
 	return ((int)a->fcg_id - b->fcg_id);
blob - 05a8126ae966aec99082f671e6dc52680c168fd9
blob + 7c07219997055de3dd12bd37dfdead4e69fa894c
--- galileo.h
+++ galileo.h
@@ -103,7 +103,6 @@ struct client {
 	int			 clt_translate;
 
 	char			 clt_buf[1024];
-	size_t			 clt_buflen;
 
 	SPLAY_ENTRY(client)	 clt_nodes;
 };
@@ -185,16 +184,9 @@ void	 fcgi_read(struct bufferevent *, void *);
 void	 fcgi_write(struct bufferevent *, void *);
 void	 fcgi_error(struct bufferevent *, short error, void *);
 void	 fcgi_free(struct fcgi *);
-int	 clt_putc(struct client *, char);
-int	 clt_puts(struct client *, const char *);
 int	 clt_write_bufferevent(struct client *, struct bufferevent *);
 int	 clt_flush(struct client *);
-int	 clt_write(struct client *, const uint8_t *, size_t);
-int	 clt_printf(struct client *, const char *, ...)
-	     __attribute__((__format__(printf, 2, 3)))
-	     __attribute__((__nonnull__(2)));
-int	 clt_tp_puts(struct template *, const char *);
-int	 clt_tp_putc(struct template *, int);
+int	 clt_write(void *, const void *, size_t);
 int	 fcgi_cmp(struct fcgi *, struct fcgi *);
 int	 fcgi_client_cmp(struct client *, struct client *);
 
blob - e68c29ebb94b7838ee92572d741ba5903e4c35fe
blob + 7b554d2017bcdfad1c15b6473a1b93f322ad9e4d
--- proxy.c
+++ proxy.c
@@ -256,6 +256,7 @@ match_image_heur(const char *url)
 static int
 gemtext_translate_line(struct client *clt, char *line)
 {
+	struct template	*tp = clt->clt_tp;
 	char		 buf[1025];
 	char		*url;
 
@@ -268,32 +269,32 @@ gemtext_translate_line(struct client *clt, char *line)
 
 		if (tp_htmlescape(clt->clt_tp, line) == -1)
 			return (-1);
-		return (clt_putc(clt, '\n'));
+		return (tp_write(tp, "\n", 1));
 	}
 
 	/* bullet */
 	if (!strncmp(line, "* ", 2)) {
 		if (clt->clt_translate & TR_NAV) {
-			if (clt_puts(clt, "</ul></nav>") == -1)
+			if (tp_writes(tp, "</ul></nav>") == -1)
 				return (-1);
 			clt->clt_translate &= ~TR_NAV;
 		}
 
 		if (!(clt->clt_translate & TR_LIST)) {
-			if (clt_puts(clt, "<ul>") == -1)
+			if (tp_writes(tp, "<ul>") == -1)
 				return (-1);
 			clt->clt_translate |= TR_LIST;
 		}
 
-		if (clt_puts(clt, "<li>") == -1 ||
+		if (tp_writes(tp, "<li>") == -1 ||
 		    tp_htmlescape(clt->clt_tp, line + 2) == -1 ||
-		    clt_puts(clt, "</li>") == -1)
+		    tp_writes(tp, "</li>") == -1)
 			return (-1);
 		return (0);
 	}
 
 	if (clt->clt_translate & TR_LIST) {
-		if (clt_puts(clt, "</ul>") == -1)
+		if (tp_writes(tp, "</ul>") == -1)
 			return (-1);
 		clt->clt_translate &= ~TR_LIST;
 	}
@@ -323,37 +324,37 @@ gemtext_translate_line(struct client *clt, char *line)
 		if (!(clt->clt_pc->flags & PROXY_NO_IMGPRV) &&
 		    match_image_heur(url)) {
 			if (clt->clt_translate & TR_NAV) {
-				if (clt_puts(clt, "</ul></nav>") == -1)
+				if (tp_writes(tp, "</ul></nav>") == -1)
 					return (-1);
 				clt->clt_translate &= ~TR_NAV;
 			}
 
-			if (tp_figure(clt->clt_tp, url, label) == -1)
+			if (tp_figure(tp, url, label) == -1)
 				return (-1);
 
 			return (0);
 		}
 
 		if (!(clt->clt_translate & TR_NAV)) {
-			if (clt_puts(clt, "<nav><ul>") == -1)
+			if (tp_writes(tp, "<nav><ul>") == -1)
 				return (-1);
 			clt->clt_translate |= TR_NAV;
 		}
 
-		if (clt_puts(clt, "<li><a href='") == -1)
+		if (tp_writes(tp, "<li><a href='") == -1)
 			return (-1);
 
 		if (tp_urlescape(clt->clt_tp, url) == -1 ||
-		    clt_puts(clt, "'>") == -1 ||
+		    tp_writes(tp, "'>") == -1 ||
 		    tp_htmlescape(clt->clt_tp, label) == -1 ||
-		    clt_puts(clt, "</a></li>") == -1)
+		    tp_writes(tp, "</a></li>") == -1)
 			return (-1);
 
 		return (0);
 	}
 
 	if (clt->clt_translate & TR_NAV) {
-		if (clt_puts(clt, "</ul></nav>") == -1)
+		if (tp_writes(tp, "</ul></nav>") == -1)
 			return (-1);
 		clt->clt_translate &= ~TR_NAV;
 	}
@@ -364,37 +365,37 @@ gemtext_translate_line(struct client *clt, char *line)
 		line += strspn(line, " \t");
 
 		clt->clt_translate |= TR_PRE;
-		return (tp_pre_open(clt->clt_tp, line));
+		return (tp_pre_open(tp, line));
 	}
 
 	/* citation block */
 	if (*line == '>') {
-		if (clt_puts(clt, "<blockquote>") == -1 ||
+		if (tp_writes(tp, "<blockquote>") == -1 ||
 		    tp_htmlescape(clt->clt_tp, line + 1) == -1 ||
-		    clt_puts(clt, "</blockquote>") == -1)
+		    tp_writes(tp, "</blockquote>") == -1)
 			return (-1);
 		return (0);
 	}
 
 	/* headings */
 	if (!strncmp(line, "###", 3)) {
-		if (clt_puts(clt, "<h3>") == -1 ||
+		if (tp_writes(tp, "<h3>") == -1 ||
 		    tp_htmlescape(clt->clt_tp, line + 3) == -1 ||
-		    clt_puts(clt, "</h3>") == -1)
+		    tp_writes(tp, "</h3>") == -1)
 			return (-1);
 		return (0);
 	}
 	if (!strncmp(line, "##", 2)) {
-		if (clt_puts(clt, "<h2>") == -1 ||
+		if (tp_writes(tp, "<h2>") == -1 ||
 		    tp_htmlescape(clt->clt_tp, line + 2) == -1 ||
-		    clt_puts(clt, "</h2>") == -1)
+		    tp_writes(tp, "</h2>") == -1)
 			return (-1);
 		return (0);
 	}
 	if (!strncmp(line, "#", 1)) {
-		if (clt_puts(clt, "<h1>") == -1 ||
+		if (tp_writes(tp, "<h1>") == -1 ||
 		    tp_htmlescape(clt->clt_tp, line + 1) == -1 ||
-		    clt_puts(clt, "</h1>") == -1)
+		    tp_writes(tp, "</h1>") == -1)
 			return (-1);
 		return (0);
 	}
@@ -404,9 +405,9 @@ gemtext_translate_line(struct client *clt, char *line)
 		return (0);
 
 	/* paragraph */
-	if (clt_puts(clt, "<p>") == -1 ||
+	if (tp_writes(tp, "<p>") == -1 ||
 	    tp_htmlescape(clt->clt_tp, line) == -1 ||
-	    clt_puts(clt, "</p>") == -1)
+	    tp_writes(tp, "</p>") == -1)
 		return (-1);
 
 	return (0);
@@ -721,23 +722,24 @@ parse_mime(struct client *clt, char *mime, char *lang,
 int
 proxy_start_reply(struct client *clt, int status, const char *ctype)
 {
+	struct template	*tp = clt->clt_tp;
 	const char	*csp;
 
 	csp = "Content-Security-Policy: default-src 'self'; "
 	    "script-src 'none'; object-src 'none';\r\n";
 
 	if (status != 200 &&
-	    clt_printf(clt, "Status: %d\r\n", status) == -1)
+	    tp_writef(tp, "Status: %d\r\n", status) == -1)
 		return (-1);
 
-	if (clt_puts(clt, csp) == -1)
+	if (tp_writes(tp, csp) == -1)
 		return (-1);
 
 	if (status == 302) {
 		/* use "ctype" as redirect target */
-		if (clt_printf(clt, "Location: %s\r\n", ctype) == -1)
+		if (tp_writef(tp, "Location: %s\r\n", ctype) == -1)
 			return (-1);
-		if (clt_puts(clt, "\r\n") == -1)
+		if (tp_writes(tp, "\r\n") == -1)
 			return (-1);
 		return (0);
 	}
@@ -745,12 +747,12 @@ proxy_start_reply(struct client *clt, int status, cons
 	if (ctype != NULL) {
 		if (!strcmp(ctype, "text/html"))
 			ctype = "text/html;charset=utf-8";
-		if (clt_printf(clt, "Content-Type: %s\r\n", ctype)
+		if (tp_writef(tp, "Content-Type: %s\r\n", ctype)
 		    == -1)
 			return (-1);
 	}
 
-	if (clt_puts(clt, "\r\n") == -1)
+	if (tp_writes(tp, "\r\n") == -1)
 		return (-1);
 
 	return (0);
@@ -837,7 +839,7 @@ proxy_read(struct bufferevent *bev, void *d)
 	else
 		ctype = mime;
 
-	if (clt_printf(clt, "Content-Type: %s\r\n\r\n", ctype) == -1)
+	if (tp_writef(clt->clt_tp, "Content-Type: %s\r\n\r\n", ctype) == -1)
 		goto err;
 
 	clt->clt_headersdone = 1;
@@ -868,6 +870,7 @@ void
 proxy_error(struct bufferevent *bev, short err, void *d)
 {
 	struct client		*clt = d;
+	struct template		*tp = clt->clt_tp;
 	int			 status = !(err & EVBUFFER_EOF);
 
 	log_debug("proxy error, shutting down the connection (err: %x)",
@@ -886,13 +889,13 @@ proxy_error(struct bufferevent *bev, short err, void *
 		}
 
 		if (clt->clt_translate & TR_LIST) {
-			if (clt_puts(clt, "</ul>") == -1)
+			if (tp_writes(tp, "</ul>") == -1)
 				return;
 			clt->clt_translate &= ~TR_LIST;
 		}
 
 		if (clt->clt_translate & TR_NAV) {
-			if (clt_puts(clt, "</ul></nav>") == -1)
+			if (tp_writes(tp, "</ul></nav>") == -1)
 				return;
 			clt->clt_translate &= ~TR_NAV;
 		}
blob - c97478128fa19c9cddcf687a802db929cfa6bd36
blob + 59cf6888097e2e08fde77614c4ea7f9517e3f4ac
--- template/parse.y
+++ template/parse.y
@@ -139,9 +139,10 @@ verbatims	: /* empty */
 
 raw		: nstring {
 			dbg();
-			fprintf(fp, "if ((tp_ret = tp->tp_puts(tp, ");
+			fprintf(fp, "if ((tp_ret = tp_write(tp, ");
 			printq($1);
-			fputs(")) == -1) goto err;\n", fp);
+			fprintf(fp, ", %zu)) == -1) goto err;\n",
+			    strlen($1));
 
 			free($1);
 		}
@@ -189,7 +190,7 @@ special		: '{' RENDER string '}' {
 		| '{' string '|' UNSAFE '}' {
 			dbg();
 			fprintf(fp,
-			    "if ((tp_ret = tp->tp_puts(tp, %s)) == -1)\n",
+			    "if ((tp_ret = tp_writes(tp, %s)) == -1)\n",
 			    $2);
 			fputs("goto err;\n", fp);
 			free($2);
@@ -205,7 +206,7 @@ special		: '{' RENDER string '}' {
 		| '{' string '}' {
 			dbg();
 			fprintf(fp,
-			    "if ((tp_ret = tp->tp_escape(tp, %s)) == -1)\n",
+			    "if ((tp_ret = tp_htmlescape(tp, %s)) == -1)\n",
 			    $2);
 			fputs("goto err;\n", fp);
 			free($2);
@@ -218,7 +219,7 @@ printf		: '{' PRINTF {
 		} printfargs '}' {
 			fputs(") == -1)\n", fp);
 			fputs("goto err;\n", fp);
-			fputs("if ((tp_ret = tp->tp_escape(tp, tp->tp_tmp)) "
+			fputs("if ((tp_ret = tp_htmlescape(tp, tp->tp_tmp)) "
 			    "== -1)\n", fp);
 			fputs("goto err;\n", fp);
 			fputs("free(tp->tp_tmp);\n", fp);
blob - 0d5556832752616b4191a8cf21835e45f65b16b4
blob + a26f893929de81045bf85ad76631aa136e7d86b3
--- template/regress/runbase.c
+++ template/regress/runbase.c
@@ -21,42 +21,36 @@
 #include "tmpl.h"
 
 int	 base(struct template *, const char *title);
+int	 my_write(void *, const void *, size_t);
 
 int
-my_putc(struct template *tp, int c)
+my_write(void *arg, const void *s, size_t len)
 {
-	FILE	*fp = tp->tp_arg;
+	FILE	*fp = arg;
 
-	if (putc(c, fp) < 0)
+	if (fwrite(s, 1, len, fp) < 0)
 		return (-1);
 
 	return (0);
 }
 
 int
-my_puts(struct template *tp, const char *s)
-{
-	FILE	*fp = tp->tp_arg;
-
-	if (fputs(s, fp) < 0)
-		return (-1);
-
-	return (0);
-}
-
-int
 main(int argc, char **argv)
 {
-	struct template *tp;
+	struct template	*tp;
+	char		 buf[3];
+	/* use a ridiculously small buffer in regress */
 
-	if ((tp = template(stdout, my_puts, my_putc)) == NULL)
+	if ((tp = template(stdout, my_write, buf, sizeof(buf))) == NULL)
 		err(1, "template");
 
-	if (base(tp, " *hello* ") == -1)
+	if (base(tp, " *hello* ") == -1 ||
+	    template_flush(tp) == -1)
 		return (1);
 	puts("");
 
-	if (base(tp, "<hello>") == -1)
+	if (base(tp, "<hello>") == -1 ||
+	    template_flush(tp) == -1)
 		return (1);
 	puts("");
 
blob - 70491763b5ea06841ec7b67897202e9bfb3c3223
blob + 648bcc07fa18e09307682160f77408c3d644530c
--- template/regress/runlist.c
+++ template/regress/runlist.c
@@ -22,38 +22,30 @@
 #include "lists.h"
 
 int	base(struct template *, struct tailhead *);
+int	my_write(void *, const void *, size_t);
 
 int
-my_putc(struct template *tp, int c)
+my_write(void *arg, const void *s, size_t len)
 {
-	FILE	*fp = tp->tp_arg;
+	FILE	*fp = arg;
 
-	if (putc(c, fp) < 0)
+	if (fwrite(s, 1, len, fp) < 0)
 		return (-1);
 
 	return (0);
 }
 
 int
-my_puts(struct template *tp, const char *s)
-{
-	FILE	*fp = tp->tp_arg;
-
-	if (fputs(s, fp) < 0)
-		return (-1);
-
-	return (0);
-}
-
-int
 main(int argc, char **argv)
 {
 	struct template	*tp;
 	struct tailhead	 head;
 	struct entry	*np;
 	int		 i;
+	char		 buf[3];
+	/* use a ridiculously small buffer in regress */
 
-	if ((tp = template(stdout, my_puts, my_putc)) == NULL)
+	if ((tp = template(stdout, my_write, buf, sizeof(buf))) == NULL)
 		err(1, "template");
 
 	TAILQ_INIT(&head);
@@ -65,7 +57,8 @@ main(int argc, char **argv)
 		TAILQ_INSERT_TAIL(&head, np, entries);
 	}
 
-	if (base(tp, &head) == -1)
+	if (base(tp, &head) == -1 ||
+	    template_flush(tp) == -1)
 		return (1);
 	puts("");
 
@@ -75,7 +68,8 @@ main(int argc, char **argv)
 		free(np);
 	}
 
-	if (base(tp, &head) == -1)
+	if (base(tp, &head) == -1 ||
+	    template_flush(tp) == -1)
 		return (1);
 	puts("");
 
blob - 18ee428516dc3b90ef925c5ac135e90278c0d5f9
blob + 5f1093f5488dc59cedb4551421207df0fb7e025d
--- template/tmpl.c
+++ template/tmpl.c
@@ -15,12 +15,62 @@
  */
 
 #include <ctype.h>
+#include <stdarg.h>
 #include <stdio.h>
 #include <stdlib.h>
+#include <string.h>
 
 #include "tmpl.h"
 
 int
+tp_write(struct template *tp, const char *str, size_t len)
+{
+	size_t	 avail;
+
+	while (len > 0) {
+		avail = tp->tp_cap - tp->tp_len;
+		if (avail == 0) {
+			if (template_flush(tp) == -1)
+				return (-1);
+			avail = tp->tp_cap;
+		}
+
+		if (len < avail)
+			avail = len;
+
+		memcpy(tp->tp_buf + tp->tp_len, str, avail);
+		tp->tp_len += avail;
+		str += avail;
+		len -= avail;
+	}
+
+	return (0);
+}
+
+int
+tp_writes(struct template *tp, const char *str)
+{
+	return (tp_write(tp, str, strlen(str)));
+}
+
+int
+tp_writef(struct template *tp, const char *fmt, ...)
+{
+	va_list	 ap;
+	char	*str;
+	int	 r;
+
+	va_start(ap, fmt);
+	r = vasprintf(&str, fmt, ap);
+	va_end(ap);
+	if (r == -1)
+		return (-1);
+	r = tp_write(tp, str, r);
+	free(str);
+	return (r);
+}
+
+int
 tp_urlescape(struct template *tp, const char *str)
 {
 	int	 r;
@@ -36,10 +86,10 @@ tp_urlescape(struct template *tp, const char *str)
 			r = snprintf(tmp, sizeof(tmp), "%%%2X", *str);
 			if (r < 0  || (size_t)r >= sizeof(tmp))
 				return (0);
-			if (tp->tp_puts(tp, tmp) == -1)
+			if (tp_write(tp, tmp, r) == -1)
 				return (-1);
 		} else {
-			if (tp->tp_putc(tp, *str) == -1)
+			if (tp_write(tp, str, 1) == -1)
 				return (-1);
 		}
 	}
@@ -58,22 +108,22 @@ tp_htmlescape(struct template *tp, const char *str)
 	for (; *str; ++str) {
 		switch (*str) {
 		case '<':
-			r = tp->tp_puts(tp, "&lt;");
+			r = tp_write(tp, "&lt;", 4);
 			break;
 		case '>':
-			r = tp->tp_puts(tp, "&gt;");
+			r = tp_write(tp, "&gt;", 4);
 			break;
 		case '&':
-			r = tp->tp_puts(tp, "&amp;");
+			r = tp_write(tp, "&amp;", 5);
 			break;
 		case '"':
-			r = tp->tp_puts(tp, "&quot;");
+			r = tp_write(tp, "&quot;", 6);
 			break;
 		case '\'':
-			r = tp->tp_puts(tp, "&apos;");
+			r = tp_write(tp, "&apos;", 6);
 			break;
 		default:
-			r = tp->tp_putc(tp, *str);
+			r = tp_write(tp, str, 1);
 			break;
 		}
 
@@ -85,7 +135,7 @@ tp_htmlescape(struct template *tp, const char *str)
 }
 
 struct template *
-template(void *arg, tmpl_puts putsfn, tmpl_putc putcfn)
+template(void *arg, tmpl_write writefn, char *buf, size_t siz)
 {
 	struct template *tp;
 
@@ -93,13 +143,25 @@ template(void *arg, tmpl_puts putsfn, tmpl_putc putcfn
 		return (NULL);
 
 	tp->tp_arg = arg;
-	tp->tp_escape = tp_htmlescape;
-	tp->tp_puts = putsfn;
-	tp->tp_putc = putcfn;
+	tp->tp_write = writefn;
+	tp->tp_buf = buf;
+	tp->tp_cap = siz;
 
 	return (tp);
 }
 
+int
+template_flush(struct template *tp)
+{
+	if (tp->tp_len == 0)
+		return (0);
+
+	if (tp->tp_write(tp->tp_arg, tp->tp_buf, tp->tp_len) == -1)
+		return (-1);
+	tp->tp_len = 0;
+	return (0);
+}
+
 void
 template_free(struct template *tp)
 {
blob - 4c8de903c9b7957de3c6bc1ed2058fa9a5db553b
blob + df3b74c2696e16f2a591fb656220ac7957c59a56
--- template/tmpl.h
+++ template/tmpl.h
@@ -19,21 +19,26 @@
 
 struct template;
 
-typedef int (*tmpl_puts)(struct template *, const char *);
-typedef int (*tmpl_putc)(struct template *, int);
+typedef int (*tmpl_write)(void *, const void *, size_t);
 
 struct template {
 	void		*tp_arg;
 	char		*tp_tmp;
-	tmpl_puts	 tp_escape;
-	tmpl_puts	 tp_puts;
-	tmpl_putc	 tp_putc;
+	tmpl_write	 tp_write;
+	char		*tp_buf;
+	size_t		 tp_len;
+	size_t		 tp_cap;
 };
 
-int		 tp_urlescape(struct template *, const char *);
-int		 tp_htmlescape(struct template *, const char *);
+int	 tp_write(struct template *, const char *, size_t);
+int	 tp_writes(struct template *, const char *);
+int	 tp_writef(struct template *, const char *, ...)
+	    __attribute__((__format__ (printf, 2, 3)));
+int	 tp_urlescape(struct template *, const char *);
+int	 tp_htmlescape(struct template *, const char *);
 
-struct template	*template(void *, tmpl_puts, tmpl_putc);
+struct template	*template(void *, tmpl_write, char *, size_t);
+int		 template_flush(struct template *);
 void		 template_free(struct template *);
 
 #endif