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