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 | } | ||