/*
* The output layer every cgit page is built with, holding the escaping rules
* for page text, attribute values, URL paths and query arguments along with
* the small formatting helpers the rest of the code prints through. What is
* written here is gathered into one buffer and handed to stdout in whole
* blocks, since a page is made of a great many small fragments. cgit shares
* stdout with the filters it runs and with git itself, so that buffer has
* to be emptied wherever another writer takes over.
*/
#include "cgit.h"
#include "html.h"
#define HTML_WRITE_BUFSIZE (64 * 1024)
// The percent encoding for each byte, with NULL marking the bytes a URL may
// carry as themselves. Those are the letters, the digits, and !$()*,-./:;@[]_~
static const char *url_escape_table[256] = {
"%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07",
"%08", "%09", "%0A", "%0B", "%0C", "%0D", "%0E", "%0F",
"%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17",
"%18", "%19", "%1A", "%1B", "%1C", "%1D", "%1E", "%1F",
"%20", NULL, "%22", "%23", NULL, "%25", "%26", "%27",
NULL, NULL, NULL, "%2B", NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, "%3C", "%3D", "%3E", "%3F",
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, "%5C", NULL, "%5E", NULL,
"%60", NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
NULL, NULL, NULL, "%7B", "%7C", "%7D", NULL, "%7F",
"%80", "%81", "%82", "%83", "%84", "%85", "%86", "%87",
"%88", "%89", "%8A", "%8B", "%8C", "%8D", "%8E", "%8F",
"%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97",
"%98", "%99", "%9A", "%9B", "%9C", "%9D", "%9E", "%9F",
"%A0", "%A1", "%A2", "%A3", "%A4", "%A5", "%A6", "%A7",
"%A8", "%A9", "%AA", "%AB", "%AC", "%AD", "%AE", "%AF",
"%B0", "%B1", "%B2", "%B3", "%B4", "%B5", "%B6", "%B7",
"%B8", "%B9", "%BA", "%BB", "%BC", "%BD", "%BE", "%BF",
"%C0", "%C1", "%C2", "%C3", "%C4", "%C5", "%C6", "%C7",
"%C8", "%C9", "%CA", "%CB", "%CC", "%CD", "%CE", "%CF",
"%D0", "%D1", "%D2", "%D3", "%D4", "%D5", "%D6", "%D7",
"%D8", "%D9", "%DA", "%DB", "%DC", "%DD", "%DE", "%DF",
"%E0", "%E1", "%E2", "%E3", "%E4", "%E5", "%E6", "%E7",
"%E8", "%E9", "%EA", "%EB", "%EC", "%ED", "%EE", "%EF",
"%F0", "%F1", "%F2", "%F3", "%F4", "%F5", "%F6", "%F7",
"%F8", "%F9", "%FA", "%FB", "%FC", "%FD", "%FE", "%FF"
};
static char out_buf[HTML_WRITE_BUFSIZE];
static size_t out_len;
static struct strbuf *capture;
static void write_out(const char *data, size_t size)
{
// A blob or snapshot is well past what one write can move onto a
// pipe, so short writes are resumed rather than reported.
if (write_in_full(STDOUT_FILENO, data, size) < 0)
die_errno("write error on html output");
}
/*
* Format into one of a rotating set of static buffers, so that a few results
* can be alive at once, for example as several arguments to one call. The slot
* count has to stay a power of two for the wrap below.
*/
char *cgit_fmt(const char *format, ...)
{
static char buf[8][1024];
static int slot;
int len;
va_list args;
slot++;
slot &= ARRAY_SIZE(buf) - 1;
va_start(args, format);
len = vsnprintf(buf[slot], sizeof(buf[slot]), format, args);
va_end(args);
if (len < 0 || (size_t)len >= sizeof(buf[slot])) {
fprintf(stderr, "[cgit] String truncated: %s\n", format);
exit(1);
}
return buf[slot];
}
char *cgit_fmtalloc(const char *format, ...)
{
struct strbuf sb = STRBUF_INIT;
va_list args;
va_start(args, format);
strbuf_vaddf(&sb, format, args);
va_end(args);
return strbuf_detach(&sb, NULL);
}
void html_flush(void)
{
size_t len = out_len;
if (!len)
return;
// Clear the length first, because write_out can die and the error page
// it produces would otherwise try to flush the same bytes again.
out_len = 0;
write_out(out_buf, len);
}
void html_capture_begin(struct strbuf *sb)
{
html_flush();
capture = sb;
}
void html_capture_end(void)
{
capture = NULL;
}
void html_raw(const char *data, size_t size)
{
if (capture) {
strbuf_add(capture, data, size);
return;
}
if (size >= HTML_WRITE_BUFSIZE) {
html_flush();
write_out(data, size);
return;
}
if (out_len + size > HTML_WRITE_BUFSIZE)
html_flush();
memcpy(out_buf + out_len, data, size);
out_len += size;
}
void html(const char *txt)
{
html_raw(txt, strlen(txt));
}
void htmlf(const char *format, ...)
{
va_list args;
struct strbuf sb = STRBUF_INIT;
va_start(args, format);
strbuf_vaddf(&sb, format, args);
va_end(args);
html(sb.buf);
strbuf_release(&sb);
}
void html_txtf(const char *format, ...)
{
va_list args;
va_start(args, format);
html_vtxtf(format, args);
va_end(args);
}
void html_vtxtf(const char *format, va_list ap)
{
va_list copy;
struct strbuf sb = STRBUF_INIT;
va_copy(copy, ap);
strbuf_vaddf(&sb, format, copy);
va_end(copy);
html_txt(sb.buf);
strbuf_release(&sb);
}
void html_txt(const char *txt)
{
if (txt)
html_ntxt(txt, strlen(txt));
}
ssize_t html_ntxt(const char *txt, size_t len)
{
const char *p = txt;
ssize_t left;
if (len > SSIZE_MAX)
return -1;
left = (ssize_t) len;
while (p && *p && left--) {
int c = *p;
if (c == '<' || c == '>' || c == '&') {
html_raw(txt, p - txt);
if (c == '>')
html(">");
else if (c == '<')
html("<");
else if (c == '&')
html("&");
txt = p + 1;
}
p++;
}
if (p != txt)
html_raw(txt, p - txt);
return left;
}
void html_attrf(const char *format, ...)
{
va_list args;
struct strbuf sb = STRBUF_INIT;
va_start(args, format);
strbuf_vaddf(&sb, format, args);
va_end(args);
html_attr(sb.buf);
strbuf_release(&sb);
}
void html_attr(const char *txt)
{
const char *p = txt;
while (p && *p) {
int c = *p;
if (c == '<' || c == '>' || c == '\'' || c == '\"' || c == '&') {
html_raw(txt, p - txt);
if (c == '>')
html(">");
else if (c == '<')
html("<");
else if (c == '\'')
html("'");
else if (c == '"')
html(""");
else if (c == '&')
html("&");
txt = p + 1;
}
p++;
}
if (p != txt)
html(txt);
}
void html_url_path(const char *txt)
{
const char *p = txt;
while (p && *p) {
unsigned char c = *p;
// A raw ampersand or plus is legal in a URL path, but this
// table is shared with html_url_arg, where a bare plus decodes
// back as a space, and the paths written here land in attribute
// values, where an ampersand can start a character reference
// and quietly turn "a©.txt" into a different filename.
const char *esc = url_escape_table[c];
if (esc) {
html_raw(txt, p - txt);
html(esc);
txt = p + 1;
}
p++;
}
if (p != txt)
html(txt);
}
void html_url_arg(const char *txt)
{
const char *p = txt;
while (p && *p) {
unsigned char c = *p;
const char *esc = url_escape_table[c];
if (c == ' ')
esc = "+";
if (esc) {
html_raw(txt, p - txt);
html(esc);
txt = p + 1;
}
p++;
}
if (p != txt)
html(txt);
}
void html_header_arg_in_quotes(const char *txt)
{
const char *p = txt;
while (p && *p) {
unsigned char c = *p;
const char *esc = NULL;
if (c == '\\')
esc = "\\\\";
else if (c == '\r')
esc = "\\r";
else if (c == '\n')
esc = "\\n";
else if (c == '"')
esc = "\\\"";
if (esc) {
html_raw(txt, p - txt);
html(esc);
txt = p + 1;
}
p++;
}
if (p != txt)
html(txt);
}
void html_hidden(const char *name, const char *value)
{
html("");
}
void html_option(const char *value, const char *text,
const char *selected_value)
{
html("\n");
}
void html_intoption(int value, const char *text, int selected_value)
{
htmlf("\n");
}
void html_link_open(const char *url, const char *title, const char *class)
{
html("");
}
void html_link_close(void)
{
html("");
}
/*
* Render one permission triplet, so a caller prints a whole mode by passing it
* shifted right by six, then by three, then unshifted.
*/
void html_fileperm(unsigned short mode)
{
htmlf("%c%c%c", (mode & 4 ? 'r' : '-'),
(mode & 2 ? 'w' : '-'), (mode & 1 ? 'x' : '-'));
}
int html_include(const char *filename)
{
FILE *f;
char buf[4096];
size_t len;
if (!(f = fopen(filename, "r"))) {
fprintf(stderr, "[cgit] Error including file %s: %s (%d)\n",
filename, strerror(errno), errno);
return -1;
}
while ((len = fread(buf, 1, sizeof(buf), f)) > 0)
html_raw(buf, len);
fclose(f);
return 0;
}
void http_parse_querystring(const char *txt,
void (*fn)(const char *name, const char *value))
{
const char *p = txt;
while (p && *p) {
char *name = url_decode_parameter_name(&p);
if (*name) {
char *value = url_decode_parameter_value(&p);
fn(name, value);
free(value);
}
free(name);
}
}