"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Stefan Sperling <stsp@stsp.name>
Subject:
allow UTF-8 in gotsys.conf site owner and repository description
To:
gameoftrees@openbsd.org
Date:
Fri, 10 Apr 2026 12:36:54 +0200

Download raw body.

Thread
  • Stefan Sperling:

    allow UTF-8 in gotsys.conf site owner and repository description

The gotsys.conf parser and related string validation routines currently
reject non-ASCII bytes. This limitation was put in place during early
development of gotsysd, with an intention to lift this limitation eventually.

I would like to allow UTF-8 in quoted strings, such that repository owner
names will not have to be mangled and repository descriptions can be
written in arbitrary languages.

gotwebd.conf already accepts non-ASCII bytes without doing any validation,
which is fine since this configuration file is considered trusted input
provided by the root user.

The diff below combines a sequence of commits which is as follows,
starting from the current tip of main:

2026-03-21 main    typo fix
2026-04-10 80e8ce5 move utf8d.h to lib/
2026-04-10 bbfbab7 rename decode() to utf8d_decode() for namespacing reasons
2026-04-10 0846d39 validate UTF-8 in quoted strings in gotsys.conf
2026-04-10 18af3a0 allow UTF-8 in gotsys string values
2026-04-10 52e22e7 add a test case for UTF-8 repository owner names in gotsys.conf
2026-04-10 gotsys-utf8 document that site owner and repository description may use UTF-8


ok?

M  gotd/libexec/got-notify-http/got-notify-http.c  |    2+   2-
D  gotd/libexec/got-notify-http/utf8d.h            |    0+  55-
M  gotsys/gotsys.conf.5                            |   16+   0-
M  gotsys/parse.y                                  |    7+   0-
M  lib/gotsys_conf.c                               |   13+   3-
A  lib/utf8d.h                                     |   55+   0-
M  regress/gotsysd/test_gotwebd.sh                 |  234+   0-

7 files changed, 327 insertions(+), 60 deletions(-)

commit - dd1f33d3b4d0d6303a96755e5a73ea539136e64c
commit + 1667f4f7b4577ba6b72b96b20e2b2ba6694a91dc
blob - d9499c2daaac15867a5911f267ea5a8b669ca96a
blob + ef4d323107bcbd311475045114b4c58e010c5a8b
--- gotd/libexec/got-notify-http/got-notify-http.c
+++ gotd/libexec/got-notify-http/got-notify-http.c
@@ -109,9 +109,9 @@ escape(FILE *fp, const uint8_t *s)
 	uint32_t codepoint, state;
 	const uint8_t *start = s;
 
-	state = 0;
+	state = UTF8_ACCEPT;
 	for (; *s; ++s) {
-		switch (decode(&state, &codepoint, *s)) {
+		switch (utf8_decode(&state, &codepoint, *s)) {
 		case UTF8_ACCEPT:
 			switch (codepoint) {
 			case '"':
blob - 480e9c55ac4a6aa58414ec65e0053dd620b5b01d (mode 644)
blob + /dev/null
--- gotd/libexec/got-notify-http/utf8d.h
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
- *
- * Permission is hereby granted, free of charge, to any person obtaining a
- * copy of this software and associated documentation files (the "Software"),
- * to deal in the Software without restriction, including without limitation
- * the rights to use, copy, modify, merge, publish, distribute, sublicense,
- * and/or sell copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
- * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- * DEALINGS IN THE SOFTWARE.
- */
-
-// See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details.
-
-#define UTF8_ACCEPT 0
-#define UTF8_REJECT 1
-
-static const uint8_t utf8d[] = {
-  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f
-  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f
-  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f
-  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f
-  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f
-  7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf
-  8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df
-  0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef
-  0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff
-  0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0
-  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2
-  1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4
-  1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6
-  1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // s7..s8
-};
-
-static uint32_t inline
-decode(uint32_t* state, uint32_t* codep, uint32_t byte) {
-  uint32_t type = utf8d[byte];
-
-  *codep = (*state != UTF8_ACCEPT) ?
-    (byte & 0x3fu) | (*codep << 6) :
-    (0xff >> type) & (byte);
-
-  *state = utf8d[256 + *state*16 + type];
-  return *state;
-}
blob - b769c8da569ed6be844d2e2ef103e02b1178eedc
blob + 25fce29e9304bd5a74cb81666364f2613ffe5048
--- gotsys/gotsys.conf.5
+++ gotsys/gotsys.conf.5
@@ -280,6 +280,16 @@ Additionally, the name may not contain the two-charact
 .Pp
 The available repository configuration directives are as follows:
 .Bl -tag -width Ds
+.It Ic description Ar string
+Sets the repository description shown on the
+.Xr gotwebd 8
+repository listing page.
+The
+.Ar string
+parameter can only contain one line of text in ASCII encoding,
+or in UTF-8 encoding if
+.Ar string
+is wrapped in quotes.
 .It Ic head Ar branch
 Point the repository's symbolic
 .Pa HEAD
@@ -705,6 +715,12 @@ repository is hidden.
 Set the displayed site owner.
 If not set then no site owner will be displayed by
 .Xr gotwebd 8 .
+The
+.Ar name
+parameter can only contain one line of text in ASCII encoding,
+or in UTF-8 encoding if
+.Ar name
+is wrapped in quotes.
 .It Ic repositories url path Ar url-path
 Sets the URL path under which Git repositories will be displayed by
 .Xr gotwebd 8 .
blob - 7d29858771ec205754bead3726797efbb55eb0ab
blob + 693f694844dc8414c699da48f55a275e458fa780
--- gotsys/parse.y
+++ gotsys/parse.y
@@ -54,6 +54,7 @@
 #include "media.h"
 #include "gotwebd.h"
 #include "gotsys.h"
+#include "utf8d.h"
 
 #ifndef nitems
 #define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
@@ -1392,6 +1393,7 @@ yylex(void)
 	unsigned char *p;
 	int quotec, next, c;
 	int token;
+	uint32_t cp, state = UTF8_ACCEPT;
 
 	p = buf;
 	c = lgetc(0);
@@ -1438,6 +1440,11 @@ yylex(void)
 				yyerror("string too long");
 				return (findeol());
 			}
+			if (utf8_decode(&state, &cp,
+			    (unsigned char)c) == UTF8_REJECT) {
+				yyerror("invalid UTF-8 string");
+				return (findeol());
+			}
 			*p++ = c;
 		}
 		yylval.v.string = strdup(buf);
blob - 55d9518f08f86547a7c0bdec2b855400321a4676
blob + c2cdaefb29324dbdd1917da2f86c01d603014117
--- lib/gotsys_conf.c
+++ lib/gotsys_conf.c
@@ -37,6 +37,7 @@
 #include "media.h"
 #include "gotwebd.h"
 #include "gotsys.h"
+#include "utf8d.h"
 
 #ifndef nitems
 #define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
@@ -1335,16 +1336,24 @@ gotsys_conf_validate_mediatype(const char *s)
 const struct got_error *
 gotsys_conf_validate_string(const char *s)
 {
+	uint32_t cp, state = UTF8_ACCEPT;
 	int i;
 
 	for (i = 0; s[i] != '\0'; ++i) {
 		char x = s[i];
 
+		if (utf8_decode(&state, &cp, (unsigned char)x) == UTF8_REJECT) {
+			return got_error_msg(GOT_ERR_PARSE_CONFIG,
+			    "invalid UTF-8 string");
+		}
+
 		/*
 		 * Similar to gotwebd/parse.y allowed_in_string() while
-		 * allowing for spaces and tabs in quoted strings.
+		 * allowing for UTF-8, spaces, and tabs in quoted strings.
 		 */
-		if (isalnum((unsigned char)x) || x == ' ' || x == '\t' ||
+		if (isalnum((unsigned char)x) ||
+		    ((unsigned char)x & 0x80) == 0x80 ||
+		    x == ' ' || x == '\t' ||
 		    (ispunct((unsigned char)x) && x != '(' && x != ')' &&
 		    x != '{' && x != '}' &&
 		    x != '!' && x != '=' && x != '#' &&
@@ -1352,7 +1361,8 @@ gotsys_conf_validate_string(const char *s)
 			continue;
 
 		return got_error_fmt(GOT_ERR_PARSE_CONFIG,
-		    "character '%c' (0x%.2x) is not allowed in %s", x, x, s);
+		    "character '%c' (0x%x) is not allowed in %s", x,
+		        (unsigned char)x, s);
 	}
 
 	return NULL;
blob - /dev/null
blob + 76457222ed8d2d5366ea06b99224f1d89137ae68 (mode 644)
--- /dev/null
+++ lib/utf8d.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+// See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details.
+
+#define UTF8_ACCEPT 0
+#define UTF8_REJECT 1
+
+static const uint8_t utf8d[] = {
+  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f
+  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f
+  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f
+  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f
+  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f
+  7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf
+  8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df
+  0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef
+  0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff
+  0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0
+  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2
+  1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4
+  1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6
+  1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // s7..s8
+};
+
+static uint32_t inline
+utf8_decode(uint32_t* state, uint32_t* codep, uint32_t byte) {
+  uint32_t type = utf8d[byte];
+
+  *codep = (*state != UTF8_ACCEPT) ?
+    (byte & 0x3fu) | (*codep << 6) :
+    (0xff >> type) & (byte);
+
+  *state = utf8d[256 + *state*16 + type];
+  return *state;
+}
blob - d9b41a3b9a330c9e5ec48b32471c5d0ab8e86361
blob + 74d98394522484f6d2749c8a686246dff5e3f61d
--- regress/gotsysd/test_gotwebd.sh
+++ regress/gotsysd/test_gotwebd.sh
@@ -1104,8 +1104,242 @@ EOF
 	test_done "$testroot" "$ret"
 }
 
+test_utf8_site_owner() {
+	local testroot=`test_init utf8_site_owner 1`
+
+	GOTSYS_ECDSA_HOST_FP=$(ssh -i ${GOTSYSD_SSH_KEY} \
+		${GOTSYSD_TEST_USER}@${VMIP} \
+		ssh-keygen -lf /etc/ssh/ssh_host_ecdsa_key.pub | \
+		cut -d' ' -f2)
+	GOTSYS_ED25519_HOST_FP=$(ssh -i ${GOTSYSD_SSH_KEY} \
+		${GOTSYSD_TEST_USER}@${VMIP} \
+		ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub | \
+		cut -d' ' -f2)
+	GOTSYS_RSA_HOST_FP=$(ssh -i ${GOTSYSD_SSH_KEY} \
+		${GOTSYSD_TEST_USER}@${VMIP} \
+		ssh-keygen -lf /etc/ssh/ssh_host_rsa_key.pub | \
+		cut -d' ' -f2)
+
+	got checkout -q $testroot/${GOTSYS_REPO} $testroot/wt >/dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got checkout failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	crypted_vm_pw=`echo ${GOTSYSD_VM_PASSWORD} | encrypt | tr -d '\n'`
+	crypted_pw=`echo ${GOTSYSD_DEV_PASSWORD} | encrypt | tr -d '\n'`
+	sshkey=`cat ${GOTSYSD_SSH_PUBKEY}`
+	cat > ${testroot}/wt/gotsys.conf <<EOF
+user ${GOTSYSD_TEST_USER} {
+	password "${crypted_vm_pw}" 
+	authorized key ${sshkey}
+}
+user ${GOTSYSD_DEV_USER} {
+	password "${crypted_pw}" 
+	authorized key ${sshkey}
+}
+repository gotsys.git {
+	permit rw ${GOTSYSD_TEST_USER}
+	permit rw ${GOTSYSD_DEV_USER}
+}
+repository public.git {
+	permit rw ${GOTSYSD_TEST_USER}
+	permit rw ${GOTSYSD_DEV_USER}
+}
+repository gottestdev.git {
+	permit rw ${GOTSYSD_DEV_USER}
+}
+repository gottest.git {
+	permit rw ${GOTSYSD_TEST_USER}
+}
+repository hidden.git {
+	permit rw ${GOTSYSD_TEST_USER}
+}
+web server "${VMIP}" {
+	repository public {
+		disable authentication
+	}
+	repository gottestdev.git {
+		permit ${GOTSYSD_DEV_USER}
+		deny ${GOTSYSD_TEST_USER}
+	}
+	repository gottest.git {
+		permit ${GOTSYSD_TEST_USER}
+	}
+	repository hidden {
+		permit ${GOTSYSD_TEST_USER}
+		deny ${GOTSYSD_DEV_USER}
+		hide repository on
+	}
+EOF
+	cp  ${testroot}/wt/gotsys.conf ${testroot}/wt/gotsys.conf.bad
+
+	# "honey bear" in German, in UTF-8 encoding
+	printf '\tsite owner "Honigb\xc3\xa4r"\n' >> ${testroot}/wt/gotsys.conf
+
+	# "honey bear" in German, in Latin1 encoding
+	printf '\tsite owner "Honigb\xe4r"\n' >> ${testroot}/wt/gotsys.conf.bad
+
+	printf '}\n' >> ${testroot}/wt/gotsys.conf.bad
+	printf '}\n' >> ${testroot}/wt/gotsys.conf
+
+	(cd ${testroot}/wt && gotsys check -q \
+		-f ${testroot}/wt/gotsys.conf.bad 2> ${testroot}/stderr)
+	ret=$?
+	if [ $ret -eq 0 ]; then
+		echo "bad gotsys.conf accepted by gotsys check" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+	cat > $testroot/stderr.expected <<EOF
+gotsys: ${testroot}/wt/gotsys.conf.bad: line 42: invalid UTF-8 string
+EOF
+	cmp -s $testroot/stderr.expected $testroot/stderr
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stderr.expected $testroot/stderr
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	(cd ${testroot}/wt && gotsys check -q)
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "bad gotsys.conf written by test" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	(cd ${testroot}/wt && got commit -m "add UTF-8 strings" >/dev/null)
+	local commit_id=`git_show_head $testroot/${GOTSYS_REPO}`
+
+	got send -q -i ${GOTSYSD_SSH_KEY} -r ${testroot}/${GOTSYS_REPO}
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got send failed unexpectedly" >&2
+		test_done "$testroot" 1
+		return 1
+	fi
+
+	# Wait for gotsysd to apply the new configuration.
+	echo "$commit_id" > $testroot/stdout.expected
+	for i in 1 2 3 4 5; do
+		sleep 1
+		ssh -i ${GOTSYSD_SSH_KEY} root@${VMIP} \
+			cat /var/db/gotsysd/commit > $testroot/stdout
+		if cmp -s $testroot/stdout.expected $testroot/stdout; then
+			break;
+		fi
+	done
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "gotsysd failed to apply configuration" >&2
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	got init $testroot/public.git > /dev/null
+	mkdir -p $testroot/public
+
+	cat > $testroot/public/readme.txt <<EOF
+Testing the summary page of gotwebd
+EOF
+	got import -m init -r $testroot/public.git $testroot/public > /dev/null
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got import failed unexpectedly" >&2
+		return 1
+	fi
+	local commit_id=`git_show_head $testroot/public.git`
+	local short_commit_id=`trim_obj_id 10 $commit_id`
+
+	cat > $testroot/public.git/got.conf <<EOF
+remote "origin" {
+	server "${GOTSYSD_DEV_USER}@$VMIP"
+	protocol ssh
+	repository "public"
+}
+EOF
+	got send -q -i ${GOTSYSD_SSH_KEY} -b main -f -r $testroot/public.git
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		echo "got send failed unexpectedly" >&2
+		return 1
+	fi
+
+	# Attempt to access a public repository's summary
+	w3m -O UTF-8 "http://${VMIP}/?action=summary&path=public.git" -dump \
+		> $testroot/stdout
+
+	cat > $testroot/stdout.expected <<EOF
+[got]
+Repositories / public.git / summary
+
+Clone URL:
+
+    ssh://${GOTSYSD_TEST_USER}@${VMIP}/public.git
+
+ECDSA:
+
+    ${GOTSYS_ECDSA_HOST_FP}
+
+ED25519:
+
+    ${GOTSYS_ED25519_HOST_FP}
+
+RSA:
+
+    ${GOTSYS_RSA_HOST_FP}
+
+Commit Briefs
+
+right now $short_commit_id Flan Hacker
+
+init (main)
+
+diff | patch | tree
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+Branches
+
+right now
+main
+summary | commit briefs | commits
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+Tags
+
+This repository contains no tags
+
+Tree
+
+readme.txt commits | blame
+
+readme.txt
+
+Testing the summary page of gotwebd
+
+Honigbär
+
+EOF
+	cmp -s $testroot/stdout.expected $testroot/stdout
+	ret=$?
+	if [ $ret -ne 0 ]; then
+		diff -u $testroot/stdout.expected $testroot/stdout
+		test_done "$testroot" "$ret"
+		return 1
+	fi
+
+	test_done "$testroot" "$ret"
+}
+
 test_parseargs "$@"
 run_test test_login
 run_test test_access_rules_index_page
 run_test test_access_rules_tree_page
 run_test test_summary_page
+run_test test_utf8_site_owner