changeset 1:836a698946f8

pasterd: add basic routes
author David Demelier <markand@malikania.fr>
date Tue, 04 Feb 2020 16:44:43 +0100
parents 15a06aa20298
children 65607ae124b1
files .hgignore Makefile config.c config.h database.c http.c http.h pasterd.c themes/minimal/footer.html themes/minimal/header.html themes/minimal/index-paste.html themes/minimal/index.html themes/minimal/paste.html util.c util.h
diffstat 15 files changed, 545 insertions(+), 137 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Tue Feb 04 13:35:52 2020 +0100
+++ b/.hgignore	Tue Feb 04 16:44:43 2020 +0100
@@ -15,5 +15,9 @@
 \.DS_Store$
 
 # Temporary files.
+extern/libsqlite3.a
 \.o$
 \.d$
+
+# Executables.
+pasterd
--- a/Makefile	Tue Feb 04 13:35:52 2020 +0100
+++ b/Makefile	Tue Feb 04 16:44:43 2020 +0100
@@ -19,12 +19,12 @@
 .POSIX:
 
 CC=             cc
-CFLAGS=         -std=c18 -Wall -Wextra -pedantic -D_XOPEN_SOURCE=700 -g
+CFLAGS=         -std=c18 -pedantic -D_XOPEN_SOURCE=700 -g
 # Release
 # CFLAGS=         -std=c18 -Wall -Wextra -pedantic -O3 -DNDEBUG -D_XOPEN_SOURCE=700
 LDFLAGS=        -static -lkcgi -lz
 
-SRCS=           database.c log.c pasterd.c paste.c util.c
+SRCS=           config.c database.c http.c log.c pasterd.c paste.c util.c
 OBJS=           ${SRCS:.c=.o}
 DEPS=           ${SRCS:.c=.d}
 
@@ -33,17 +33,22 @@
                 -DSQLITE_OMIT_DEPRECATED \
                 -DSQLITE_DEFAULT_FOREIGN_KEYS=1
 
-UID=            www
+PREFIX=         /usr/local
+BINDIR=         ${PREFIX}/bin
+SHAREDIR=       ${PREFIX}/share
+VARDIR=         ${PREFIX}/var
+
+DEFINES=        -DSHAREDIR=\"${SHAREDIR}\" -DVARDIR=\"${VARDIR}\"
 
 .SUFFIXES:
 .SUFFIXES: .c .o
 
-all: paster
+all: pasterd
 
 -include ${DEPS}
 
 .c.o:
-	${CC} ${CFLAGS} -MMD -Iextern -c $<
+	${CC} ${CFLAGS} ${DEFINES} -MMD -Iextern -c $<
 
 extern/sqlite3.o: extern/sqlite3.c extern/sqlite3.h
 	${CC} ${CFLAGS} ${SQLITE_FLAGS} -MMD -c $< -o $@
@@ -58,7 +63,10 @@
 	rm -f extern/sqlite3.o extern/libsqlite3.a
 	rm -f pasterd ${OBJS} ${DEPS}
 
-run: pasterd
-	kfcgi -dv -s ./paster.sock -u ${UID} -U ${UID} -p . -- pasterd -f
+install:
+	mkdir -p ${DESTDIR}${BINDIR}
+	cp pasterd ${DESTDIR}${BINDIR}
+	mkdir -p ${DESTDIR}${SHAREDIR}/paster
+	cp -R themes ${DESTDIR}${SHAREDIR}/paster
 
 .PHONY: all clean run
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/config.c	Tue Feb 04 16:44:43 2020 +0100
@@ -0,0 +1,23 @@
+/*
+ * config.c -- pasterd options
+ *
+ * Copyright (c) 2020 David Demelier <markand@malikania.fr>
+ * 
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "config.h"
+
+struct config config = {
+	.themedir = SHAREDIR "/paster/themes/minimal"
+};
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/config.h	Tue Feb 04 16:44:43 2020 +0100
@@ -0,0 +1,29 @@
+/*
+ * config.h -- pasterd options
+ *
+ * Copyright (c) 2020 David Demelier <markand@malikania.fr>
+ * 
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef PASTER_CONFIG_H
+#define PASTER_CONFIG_H
+
+#include <limits.h>
+
+extern struct config {
+	char themedir[PATH_MAX];
+	int verbosity;
+} config;
+
+#endif /* !PASTER_CONFIG_H */
--- a/database.c	Tue Feb 04 13:35:52 2020 +0100
+++ b/database.c	Tue Feb 04 16:44:43 2020 +0100
@@ -10,9 +10,6 @@
 #include "paste.h"
 #include "util.h"
 
-/* sqlite3 use const unsigned char *. */
-#define DUP(s) estrdup((const char *)(s))
-
 static sqlite3 *db;
 
 static const char *sql_init =
@@ -41,7 +38,7 @@
 	"     , visible\n"
 	"     , duration\n"
 	"  FROM paste\n"
-	" WHERE id = ?";
+	" WHERE uuid = ?";
 
 static const char *sql_insert =
 	"INSERT INTO paste(\n"
@@ -76,6 +73,13 @@
 	"\n"
 	"END TRANSACTION";
 
+/* sqlite3 use const unsigned char *. */
+static char *
+dup(const unsigned char *s)
+{
+	return estrdup(s ? (const char *)(s) : "");
+}
+
 static const char *
 create_id(void)
 {
@@ -100,11 +104,11 @@
 static void
 convert(sqlite3_stmt *stmt, struct paste *paste)
 {
-	paste->uuid = DUP(sqlite3_column_text(stmt, 0));
-	paste->title = DUP(sqlite3_column_text(stmt, 1));
-	paste->author = DUP(sqlite3_column_text(stmt, 2));
-	paste->language = DUP(sqlite3_column_text(stmt, 3));
-	paste->code = DUP(sqlite3_column_text(stmt, 4));
+	paste->uuid = dup(sqlite3_column_text(stmt, 0));
+	paste->title = dup(sqlite3_column_text(stmt, 1));
+	paste->author = dup(sqlite3_column_text(stmt, 2));
+	paste->language = dup(sqlite3_column_text(stmt, 3));
+	paste->code = dup(sqlite3_column_text(stmt, 4));
 	paste->timestamp = sqlite3_column_int64(stmt, 5);
 	paste->visible = sqlite3_column_int(stmt, 6);
 	paste->duration = sqlite3_column_int64(stmt, 7);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/http.c	Tue Feb 04 16:44:43 2020 +0100
@@ -0,0 +1,330 @@
+/*
+ * http.c -- HTTP parsing and rendering
+ *
+ * Copyright (c) 2020 David Demelier <markand@malikania.fr>
+ * 
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <assert.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <kcgi.h>
+
+#include "config.h"
+#include "database.h"
+#include "http.h"
+#include "log.h"
+#include "paste.h"
+#include "util.h"
+
+static void page_index(struct kreq *);
+static void page_new(struct kreq *);
+static void page_fork(struct kreq *);
+static void page_paste(struct kreq *);
+static void page_about(struct kreq *);
+static void page_download(struct kreq *);
+
+enum page {
+	PAGE_INDEX,
+	PAGE_NEW,
+	PAGE_FORK,
+	PAGE_PASTE,
+	PAGE_ABOUT,
+	PAGE_DOWNLOAD,
+	PAGE_LAST       /* Not used. */
+};
+
+static const char *pages[] = {
+	[PAGE_INDEX]    = "",
+	[PAGE_NEW]      = "new",
+	[PAGE_FORK]     = "fork",
+	[PAGE_PASTE]    = "paste",
+	[PAGE_ABOUT]    = "about",
+	[PAGE_DOWNLOAD] = "download",
+};
+
+static void (*handlers[])(struct kreq *req) = {
+	[PAGE_INDEX]    = page_index,
+	[PAGE_NEW]      = page_new,
+	[PAGE_FORK]     = page_fork,
+	[PAGE_PASTE]    = page_paste,
+	[PAGE_ABOUT]    = page_about,
+	[PAGE_DOWNLOAD] = page_download
+};
+
+struct tmpl_index {
+	struct kreq *req;
+	struct paste pastes[10];
+	size_t count;
+	size_t current;
+};
+
+struct tmpl_paste {
+	struct kreq *req;
+	struct paste paste;
+};
+
+static const char *
+template(const char *filename)
+{
+	/* Build path to the template file. */
+	static char path[PATH_MAX];
+
+	snprintf(path, sizeof (path), "%s/%s", config.themedir, filename);
+
+	return path;
+}
+
+static int
+tmpl_paste(size_t index, void *arg)
+{
+	struct tmpl_paste *data = arg;
+	struct paste *paste = &data->paste;
+
+	switch (index) {
+	case 0:
+		khttp_puts(data->req, paste->uuid);
+		break;
+	case 1:
+		khttp_puts(data->req, paste->title);
+		break;
+	case 2:
+		khttp_puts(data->req, paste->author);
+		break;
+	case 3:
+		khttp_puts(data->req, paste->language);
+		break;
+	case 4:
+		khttp_puts(data->req, paste->code);
+		break;
+	case 5:
+		/* TODO: timestamp here. */
+		khttp_puts(data->req, "TODO");
+		break;
+	case 6:
+		khttp_puts(data->req, bprintf("%s", paste->visible ? "Yes" : "No"));
+		break;
+	case 7:
+		/* TODO: convert time left. */
+		khttp_puts(data->req, "TODO");
+		break;
+	default:
+		break;
+	}
+
+	return true;
+}
+
+static int
+tmpl_index_pastes(size_t index, void *arg)
+{
+	struct tmpl_index *data = arg;
+	struct paste *paste = &data->pastes[data->current];
+
+	switch (index) {
+	case 0:
+		khttp_puts(data->req, paste->uuid);
+		break;
+	case 1:
+		khttp_puts(data->req, paste->title);
+		break;
+	case 2:
+		khttp_puts(data->req, paste->author);
+		break;
+	case 3:
+		khttp_puts(data->req, paste->language);
+		break;
+	case 4:
+		khttp_puts(data->req, bprintf("%d", paste->duration));
+		break;
+	default:
+		break;
+	}
+
+	return true;
+}
+
+static int
+tmpl_index(size_t index, void *arg)
+{
+	/* No check, only one index. */
+	struct tmpl_index *data = arg;
+	const char *keywords[] = {
+		"uuid",
+		"name",
+		"author",
+		"language",
+		"expiration"
+	};
+	struct ktemplate kt = {
+		.key    = keywords,
+		.keysz  = 5,
+		.arg    = data,
+		.cb     = tmpl_index_pastes
+	};
+
+	for (size_t i = 0; i < data->count; ++i) {
+		khttp_template(data->req, &kt, template("index-paste.html"));
+		data->current++;
+	}
+
+	return true;
+}
+
+static void
+header(struct kreq *req)
+{
+	khttp_template(req, NULL, template("header.html"));
+}
+
+static void
+footer(struct kreq *req)
+{
+	khttp_template(req, NULL, template("footer.html"));
+}
+
+static void
+page_index(struct kreq *req)
+{
+	struct tmpl_index data = {
+		.req    = req,
+		.count  = 10
+	};
+
+	khttp_head(req, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_TEXT_HTML]);
+
+	if (!database_recents(data.pastes, &data.count)) {
+		khttp_head(req, kresps[KRESP_STATUS], "%s", khttps[KHTTP_500]);
+		khttp_body(req);
+		khttp_template(req, NULL, template("500.html"));
+	} else {
+		const char *keywords[] = { "pastes" };
+		struct ktemplate kt = {
+			.key    = keywords,
+			.keysz  = 1,
+			.arg    = &data,
+			.cb     = tmpl_index
+		};
+
+		khttp_head(req, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
+		khttp_body(req);
+		header(req);
+		khttp_template(req, &kt, template("index.html"));
+		footer(req);
+	}
+
+	khttp_free(req);
+}
+
+static void
+page_new(struct kreq *req)
+{
+	(void)req;
+}
+
+static void
+page_fork(struct kreq *req)
+{
+	(void)req;
+}
+
+static void
+page_paste(struct kreq *req)
+{
+	struct tmpl_paste data = {
+		.req = req
+	};
+
+	khttp_head(req, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_TEXT_HTML]);
+
+	if (!database_get(&data.paste, req->path)) {
+		khttp_head(req, kresps[KRESP_STATUS], "%s", khttps[KHTTP_404]);
+		khttp_body(req);
+		khttp_template(req, NULL, template("404.html"));
+	} else {
+		const char *keywords[] = {
+			"uuid",
+			"title",
+			"author",
+			"language",
+			"code",
+			"timestamp",
+			"visible",
+			"duration"
+		};
+		const struct ktemplate kt = {
+			.key    = keywords,
+			.keysz  = 8,
+			.cb     = tmpl_paste,
+			.arg    = &data
+		};
+
+		khttp_head(req, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
+		khttp_body(req);
+		header(req);
+		khttp_template(req, &kt, template("paste.html"));
+		footer(req);
+		khttp_free(req);
+	}
+}
+
+static void
+page_about(struct kreq *req)
+{
+	(void)req;
+}
+
+static void
+page_download(struct kreq *req)
+{
+	(void)req;
+}
+
+static void
+process(struct kreq *req)
+{
+	assert(req);
+
+	handlers[req->page](req);
+}
+
+void
+http_fcgi_run(void)
+{
+	struct kreq req;
+	struct kfcgi *fcgi;
+
+	if (khttp_fcgi_init(&fcgi, NULL, 0, pages, PAGE_LAST, 0) != KCGI_OK)
+		return;
+ 
+	while (khttp_fcgi_parse(fcgi, &req) == KCGI_OK)
+		process(&req);
+ 
+	khttp_fcgi_free(fcgi);
+}
+
+void
+http_cgi_run(void)
+{
+	struct kreq req;
+
+	if (khttp_parse(&req, NULL, 0, pages, PAGE_LAST, 0) == KCGI_OK)
+		process(&req);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/http.h	Tue Feb 04 16:44:43 2020 +0100
@@ -0,0 +1,28 @@
+/*
+ * http.h -- HTTP parsing and rendering
+ *
+ * Copyright (c) 2020 David Demelier <markand@malikania.fr>
+ * 
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ * 
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef PASTER_HTTP_H
+#define PASTER_HTTP_H
+
+void
+http_fcgi_run(void);
+
+void
+http_cgi_run(void);
+
+#endif /* !PASTER_HTTP_H */
--- a/pasterd.c	Tue Feb 04 13:35:52 2020 +0100
+++ b/pasterd.c	Tue Feb 04 16:44:43 2020 +0100
@@ -16,146 +16,47 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include <sys/types.h>
-#include <assert.h>
-#include <stdarg.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <unistd.h>
-#include <kcgi.h>
-
-
 #include <stdlib.h>
 #include <time.h>
+#include <unistd.h>
 
 #include "database.h"
-
-#if 0
-
-static const char *pages[] = {
-	"index",
-	"new",
-	"fork",
-	"get"
-};
-
-
-static int
-process(struct kreq *req)
-{
-	assert(req);
+#include "http.h"
+#include "log.h"
 
-	khttp_head(req, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]);
-	khttp_head(req, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_TEXT_PLAIN]);
-	khttp_body(req);
-	khttp_puts(req, "== begin ==\n");
-
-	for (size_t i = 0; i < req->fieldsz; ++i) {
-		khttp_puts(req, req->fields[i].key);
-		khttp_putc(req, '=');
-		khttp_puts(req, req->fields[i].val);
-		khttp_putc(req, '\n');
-	}
-
-	khttp_puts(req, "== end ==\n");
-	khttp_free(req);
-
-	return 0;
+static void
+init(void)
+{
+	srand(time(NULL));
+	log_open();
+	database_open("test.db");
 }
 
-static int
-kcgi_run(void)
+static void
+quit(void)
 {
-	struct kreq req;
-	struct kfcgi *fcgi;
-
-	if (khttp_fcgi_init(&fcgi, NULL, 0, pages, 4, 0) != KCGI_OK)
-		return 1;
- 
-	while (khttp_fcgi_parse(fcgi, &req) == KCGI_OK)
-		process(&req);
- 
-	khttp_fcgi_free(fcgi);
-
-	return 0;
+	database_finish();
+	log_finish();
 }
-
-static int
-cgi_run(void)
-{
-	struct kreq req;
-
-	if (khttp_parse(&req, NULL, 0, pages, 4, 0) != KCGI_OK)
-		return 1;
-
-	return process(&req);
-}
-
-#endif
-
-#include "util.h"
-#include "paste.h"
-#include "log.h"
  
 int
 main(int argc, char **argv)
 {
-	srand(time(NULL));
-
-	(void)argc;
-	(void)argv;
-
-	struct paste paste = {
-		.title = estrdup("Test de C++"),
-		.author = estrdup("David Demelier"),
-		.language = estrdup("C"),
-		.code = estrdup("int main(void) { }"),
-		.duration = 60
-	};
-
-	log_open();
-	database_open("test.db");
-
-
+	init();
 
-	struct paste pastes[10];
-	size_t n = 10;
-
-
-	database_clear();
-	database_recents(pastes, &n);
-	for (size_t i = 0; i < n; ++i) {
-		printf("%s, %s, %lld\n", pastes[i].uuid, pastes[i].author, (long long int)pastes[i].timestamp);
-		paste_finish(&pastes[i]);
-	}
-
-
-
-	paste_finish(&paste);
-	database_finish();
-	log_finish();
-
-#if 0
 	int opt;
-	int (*run)(void) = &(cgi_run);
+	void (*run)(void) = &(http_cgi_run);
 
 	while ((opt = getopt(argc, argv, "f")) != -1) {
 		switch (opt) {
 		case 'f':
-			run = &(kcgi_run);
+			run = &(http_fcgi_run);
 			break;
 		default:
 			break;
 		}
 	}
 
-	return run();
-#endif
-	
-#if 0
-	(void)argc;
-	(void)argv;
-
-#endif
+	run();
+	quit();
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/themes/minimal/footer.html	Tue Feb 04 16:44:43 2020 +0100
@@ -0,0 +1,2 @@
+	<body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/themes/minimal/header.html	Tue Feb 04 16:44:43 2020 +0100
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta charset="UTF-8">
+	</head>
+	<body>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/themes/minimal/index-paste.html	Tue Feb 04 16:44:43 2020 +0100
@@ -0,0 +1,6 @@
+<tr>
+	<td><a href="/paste/@@uuid@@">@@name@@</a></td>
+	<td>@@author@@</td>
+	<td>@@language@@</td>
+	<td>@@expiration@@</td>
+</tr>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/themes/minimal/index.html	Tue Feb 04 16:44:43 2020 +0100
@@ -0,0 +1,16 @@
+	<h1>Recent pastes</h1>
+
+	<table>
+		<thead>
+			<tr>
+				<th>Name</th>
+				<th>Author</th>
+				<th>Language</th>
+				<th>Expires in</th>
+			<tr>
+		</thead>
+	<tbody>
+
+	@@pastes@@
+
+	</tbody>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/themes/minimal/paste.html	Tue Feb 04 16:44:43 2020 +0100
@@ -0,0 +1,35 @@
+	<table>
+		<tbody>
+			<tr>
+				<td>Identifier</td>
+				<td>@@uuid@@</td>
+			</tr>
+			<tr>
+				<td>Title</td>
+				<td>@@title@@</td>
+			</tr>
+			<tr>
+				<td>Author</td>
+				<td>@@author@@</td>
+			</tr>
+			<tr>
+				<td>Language</td>
+				<td>@@language@@</td>
+			</tr>
+			<tr>
+				<td>Date</td>
+				<td>@@timestamp@@</td>
+			</tr>
+			<tr>
+				<td>Is visible?</td>
+				<td>@@visible@@</td>
+			</tr>
+			<tr>
+				<td>Lifetime</td>
+				<td>@@duration@@</td>
+			</tr>
+		</tbody>
+	</table>
+	<pre>
+@@code@@
+	</pre>
--- a/util.c	Tue Feb 04 13:35:52 2020 +0100
+++ b/util.c	Tue Feb 04 16:44:43 2020 +0100
@@ -19,10 +19,10 @@
 #include <assert.h>
 #include <errno.h>
 #include <stdarg.h>
+#include <stdio.h>
 #include <stdlib.h>
 #include <stdnoreturn.h>
 #include <string.h>
-#include <stdio.h>
 
 #include "util.h"
 
@@ -51,3 +51,16 @@
 
 	return strcpy(ret, str);
 }
+
+const char *
+bprintf(const char *fmt, ...)
+{
+	static char buf[BUFSIZ];
+	va_list ap;
+
+	va_start(ap, fmt);
+	vsnprintf(buf, sizeof (buf), fmt, ap);
+	va_end(ap);
+
+	return buf;
+}
--- a/util.h	Tue Feb 04 13:35:52 2020 +0100
+++ b/util.h	Tue Feb 04 16:44:43 2020 +0100
@@ -19,12 +19,15 @@
 #ifndef PASTER_UTIL_H
 #define PASTER_UTIL_H
 
-#include <noreturn.h>
+#include <stdnoreturn.h>
 
 noreturn void
-die(const char *fmt, ...);
+die(const char *, ...);
 
 char *
-estrdup(const char *str);
+estrdup(const char *);
+
+const char *
+bprintf(const char *, ...);
 
 #endif /* !PASTER_UTIL_H */