Download
81 lines 2.9 KiBGoModified 2026-10-27 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
package highlight

import (
	"html"
	"regexp"
	"strings"
	"testing"
	"time"
	"unicode/utf8"

	"github.com/alecthomas/chroma/v2"
	"github.com/alecthomas/chroma/v2/lexers"
)

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

// fuzzLexers is every lexer a pushed file can select: chroma's builtins,
// this repository's custom lexers and the fence-aware markdown lexer.
func fuzzLexers(f *testing.F) (*Registry, []chroma.Lexer) {
	r, err := New(nil, []Custom{{Name: "bp"}, {Name: "nomsu"}, {Name: "tomo"}}, "github")
	if err != nil {
		f.Fatal(err)
	}
	lxs := append([]chroma.Lexer{}, lexers.GlobalLexerRegistry.Lexers...)
	for _, name := range []string{"bp", "nomsu", "tomo"} {
		lxs = append(lxs, r.ByName(name))
	}
	return r, append(lxs, r.ByPath("README.md"))
}

// garbles names chroma lexers known to show some text other than it is,
// which the fuzzer still runs for hangs.
var garbles = map[string]bool{
	"Sed":       true, // "\\  " emits a space twice
	"Caddyfile": true, // a line's leading space before "{" is lost
	"Raku":      true, // "q!0" gains a closing "!"
}

var newlines = strings.NewReplacer("\r\n", "\n", "\r", "\n")

var tagRE = regexp.MustCompile(`</?code[^>]*>|<span class="[^"]*">|</span>`)

// A pushed file is highlighted by whichever lexer its name picks, while the
// render lock is held. Whatever its contents, the page must show exactly
// that text, escaped, and no file may hold up the render for long.
func FuzzHTML(f *testing.F) {
	r, lxs := fuzzLexers(f)
	for _, lx := range lxs {
		_ = r.HTML([]byte("x"), lx) // compile the lexer outside the timing
	}
	for i, s := range []string{
		"", "int main() { return 0; }\n", "<script>alert(1)</script>", `"unterminated`,
		"```go\nfunc f() {}\n```\n", "a & b < c > d \"e\" 'f'", "\t\r\n\x00", "/* /* */",
		strings.Repeat("(", 200), strings.Repeat("a", 500) + "!",
	} {
		f.Add(uint16(i*37), s)
	}
	f.Fuzz(func(t *testing.T, which uint16, src string) {
		if len(src) > 4096 || !utf8.ValidString(src) {
			return
		}
		lx := lxs[int(which)%len(lxs)]
		start := time.Now()
		out := string(r.HTML([]byte(src), lx))
		// Some lexers are slow (Scilab) or quadratic (reg: 8KiB of "@" in
		// over two seconds), so what must hold is that HTML gives up in
		// time, with slack for the fuzzer's workers crowding the CPU.
		if d := time.Since(start); d > highlightTime+2*time.Second {
			t.Fatalf("%s took %v on %d bytes", Name(lx), d, len(src))
		}
		// Compared as a browser reads them, where "\r\n" and "\r" are
		// both "\n": some lexers rewrite one as another.
		got := newlines.Replace(html.UnescapeString(tagRE.ReplaceAllString(out, "")))
		if want := newlines.Replace(src); got != want && got != want+"\n" && !garbles[Name(lx)] {
			t.Fatalf("%s shows %q as %q", Name(lx), src, got)
		}
		if strings.Count(tagRE.ReplaceAllString(out, ""), "<") != 0 {
			t.Fatalf("%s leaves markup unescaped: %q", Name(lx), out)
		}
	})
}