Download
85 lines 2.7 KiBGoModified 2026-10-20 raw
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
package render_test

import (
	"net/url"
	"path"
	"path/filepath"
	"strings"
	"testing"

	"code.bruce-hill.com/repo-renderer/internal/render"
)

// Run one with `make fuzz`; plain `go test` runs only the seeds.

var fuzzLay = render.Layout{GitBase: "/var/git", Htdocs: "/htdocs/h", QueueDir: "/spool", State: "/db"}

// under reports whether p lies strictly inside dir.
func under(p, dir string) bool {
	rel, err := filepath.Rel(dir, p)
	return err == nil && filepath.IsLocal(rel)
}

// Tree paths come from the pusher. Every one ValidPath admits must land
// inside its repository's output directories and come back out of its URL
// unchanged, with nothing taken for a query or fragment.
func FuzzValidPath(f *testing.F) {
	for _, s := range []string{
		"a", "src/main.c", "..", "a/../b", "./a", "a//b", "/etc/passwd", "a/",
		"a?b#c", "%2e%2e", "a b/ü.txt", "x\x00y", "a\\..\\b", "index.html", "...",
	} {
		f.Add(s)
	}
	f.Fuzz(func(t *testing.T, p string) {
		if !render.ValidPath(p) {
			return
		}
		if path.Clean(p) != p || !filepath.IsLocal(p) {
			t.Fatalf("ValidPath(%q) but it is not clean and local", p)
		}
		for name, pair := range map[string][2]string{
			"page": {fuzzLay.Page("r", p), fuzzLay.RepoDir("r")},
			"raw":  {fuzzLay.Raw("r", p), fuzzLay.RawDir("r")},
			"svg":  {fuzzLay.SVG("r", p), fuzzLay.SVGDir("r")},
		} {
			if !under(pair[0], pair[1]) {
				t.Fatalf("%s of %q is %s, outside %s", name, p, pair[0], pair[1])
			}
		}
		for prefix, u := range map[string]string{
			"/code/r/": render.CodeURL("r", p),
			"/raw/r/":  render.RawURL("r", p),
			"/svg/r/":  render.SVGURL("r", p),
		} {
			pu, err := url.Parse(u)
			if err != nil || pu.Path != prefix+p || pu.RawQuery != "" || pu.Fragment != "" || pu.Host != "" {
				t.Fatalf("URL %q of %q parses as %+v (%v)", u, p, pu, err)
			}
		}
	})
}

// Tag names come from the pusher too. A sanitized name must be a single
// harmless file name in the repository's archive directory.
func FuzzSanitizeTag(f *testing.F) {
	for _, s := range []string{"v1.0", "release/2", "..", ".hidden", "a/../b", "-x", "v1?x", "", "/", "a\x00"} {
		f.Add(s)
	}
	f.Fuzz(func(t *testing.T, tag string) {
		s, ok := render.SanitizeTag(tag)
		if !ok {
			return
		}
		if s == "" || strings.ContainsAny(s, "/\\\x00") || s == "." || s == ".." {
			t.Fatalf("SanitizeTag(%q) = %q", tag, s)
		}
		a := fuzzLay.Archive("r", s)
		if filepath.Dir(a) != fuzzLay.ArchiveDir("r") || filepath.Base(a) != "r-"+s+".tar.gz" {
			t.Fatalf("archive of %q is %s", tag, a)
		}
		pu, err := url.Parse(render.ArchiveURL("r", s))
		if err != nil || pu.Path != "/archive/r/r-"+s+".tar.gz" || pu.RawQuery != "" || pu.Fragment != "" {
			t.Fatalf("archive URL of %q parses as %+v (%v)", tag, pu, err)
		}
	})
}