Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Draw a vole portrait for every name

753cd703960a1f024b3e5819ae6330bb2ab1cd08 · Keenan Nemetz · 7d ago · parent a6b42433

Draw a vole portrait for every name

The wordmark is a vole's face, so a name gets one too: the profile, the
directory, comment cards, inline review notes, approvals and the
contributor list all render a small vole drawn from the account name.

The portrait is derived with SHA-256 rather than stored. The same name
draws the same ears, eyes, whiskers, fur shade and ground colour on every
page and between visits, and the instance keeps no image, accepts no
upload, and calls no avatar service. The seven grounds are the
Everforest accents pulled toward the page, the head and features use the
palette's cream and dark ground, and the picture is inline SVG: the CSP,
the single binary, and an instance with no outbound requests are all
unchanged.

The name never reaches the markup. The builder takes only integer
traits, which is what makes it safe to hand the template as HTML, and a
test holds that a hostile name cannot appear in the output.

Two shapes were tried and dropped. Separating the ears with a
ground-coloured gap, the way the wordmark does, collapses into noise
below 24px, so the ears and head are one silhouette. Cheek patches and
brow strokes read as smudges at the same size, so a blaze down the
forehead is the only marking left; the rest of the variation is colour.

server/web/avatar_test.go +132

@@ -0,0 +1,132 @@
1 package web
2
3 // The portrait is a deterministic function of the name, and the markup is built
4 // from geometry rather than from the name. These tests hold both: the same name
5 // draws the same face, different names draw different faces, and a name cannot
6 // put anything into the SVG.
7
8 import (
9 "encoding/xml"
10 "io"
11 "strings"
12 "testing"
13 )
14
15 func wellFormed(t *testing.T, markup string) {
16 t.Helper()
17 decoder := xml.NewDecoder(strings.NewReader(markup))
18 for {
19 _, err := decoder.Token()
20 if err == io.EOF {
21 return
22 }
23 if err != nil {
24 t.Fatalf("avatar is not well-formed XML: %v\n%s", err, markup)
25 }
26 }
27 }
28
29 func TestAvatarIsStableForAName(t *testing.T) {
30 t.Parallel()
31 first := portrait("alice")
32 for i := 0; i < 5; i++ {
33 again := portrait("alice")
34 if again.Tone != first.Tone || again.SVG != first.SVG {
35 t.Fatalf("portrait of alice changed between calls:\n%s\n%s", first.SVG, again.SVG)
36 }
37 }
38 if portrait("alice").Tone != portrait("alice").Tone {
39 t.Fatal("tone is not deterministic")
40 }
41 // A name is not case-folded into another account's face by accident: the
42 // canonical account name is what the caller passes, and that is what varies.
43 if portrait("alice").SVG == portrait("bob").SVG && portrait("alice").Tone == portrait("bob").Tone {
44 t.Fatal("two names drew the same portrait")
45 }
46 }
47
48 func TestAvatarVariesAcrossNames(t *testing.T) {
49 t.Parallel()
50 tones := map[int]bool{}
51 faces := map[string]bool{}
52 for _, name := range []string{
53 "alice", "bob", "adam", "keenan", "wilbur", "coding-agent", "reviewer",
54 "a", "z", "aa", "ab", "ba", "someone-with-a-long-name", "x1", "x2",
55 "moss", "bark", "clay", "river", "heather", "straw", "stone",
56 } {
57 v := portrait(name)
58 tones[v.Tone] = true
59 faces[string(v.SVG)] = true
60 wellFormed(t, string(v.SVG))
61 }
62 if len(tones) < 5 {
63 t.Fatalf("only %d of 7 tones appeared: %v", len(tones), tones)
64 }
65 if len(faces) < 15 {
66 t.Fatalf("only %d distinct faces across 22 names", len(faces))
67 }
68 }
69
70 func TestAvatarToneIsInRange(t *testing.T) {
71 t.Parallel()
72 for i := 0; i < 200; i++ {
73 name := "account-" + string(rune('a'+i%26)) + string(rune('a'+i/26))
74 if tone := portrait(name).Tone; tone < 1 || tone > 7 {
75 t.Fatalf("portrait(%q).Tone = %d, want 1..7", name, tone)
76 }
77 }
78 }
79
80 // The name is never interpolated into the markup, so the one place a page hands
81 // the template raw HTML cannot carry user text. This is the test that keeps it
82 // that way: if a future change prints the name into a title or a data attribute,
83 // it fails here.
84 func TestAvatarMarkupCannotCarryTheName(t *testing.T) {
85 t.Parallel()
86 hostile := `</svg><script>alert(1)</script>`
87 v := portrait(hostile)
88 markup := string(v.SVG)
89 if strings.Contains(markup, "<script") || strings.Contains(markup, "alert") {
90 t.Fatalf("the name reached the markup:\n%s", markup)
91 }
92 if strings.Contains(markup, "svg><svg") {
93 t.Fatalf("the name closed the element:\n%s", markup)
94 }
95 wellFormed(t, markup)
96 }
97
98 func TestAvatarLinkedOnlyWhenTheNameIsAnAccount(t *testing.T) {
99 t.Parallel()
100 s := newSite(t)
101
102 plain := s.web.avatar("alice")
103 if plain.Href != "" {
104 t.Fatalf("a bare portrait has an href: %+v", plain)
105 }
106 linked := s.web.avatarUser(s.web.userRef("alice"))
107 if linked.Href != "/u/alice" {
108 t.Fatalf("linked portrait href = %q", linked.Href)
109 }
110 // A name that is not an account still gets a face, but no link to a 404.
111 stranger := s.web.avatarUser(s.web.userRef("nobody"))
112 if stranger.Href != "" || stranger.SVG == "" {
113 t.Fatalf("unknown account portrait = %+v", stranger)
114 }
115 }
116
117 func TestAvatarRendersInTheChrome(t *testing.T) {
118 t.Parallel()
119 s := newSite(t)
120 body := s.serve(t, nil, "/u/alice").Body.String()
121 if !strings.Contains(body, `<svg class="vole-avatar`) {
122 t.Fatalf("the profile does not render a portrait:\n%s", body)
123 }
124 // The portrait is decoration: the account name is beside it, so it is hidden
125 // from assistive technology and skipped by Tab.
126 if !strings.Contains(body, `aria-hidden="true"`) {
127 t.Fatalf("the profile portrait is not hidden from assistive technology:\n%s", body)
128 }
129 if strings.Contains(body, `class="avatar c7"`) && !strings.Contains(body, "<svg") {
130 t.Fatalf("an avatar rendered without its portrait:\n%s", body)
131 }
132 }