package markup
import (
"strings"
"testing"
)
// The crash: Outline lowercased the whole document into a second string
// and indexed it with offsets computed on the first. strings.ToLower
// does not preserve length — "Ⱥ" is two bytes and lowercases to three —
// so after one such character every offset addressed a different byte
// in each string, and the slice ran off the end. Every reader of that
// page got a 500. Found on pkgsrc's own documentation.
func TestADocumentThatChangesLengthWhenLoweredDoesNotPanic(t *testing.T) {
for _, tc := range []struct{ name, char string }{
{"a character that grows when lowered", "Ⱥ"},
{"a character that shrinks when lowered", "İ"},
{"the kelvin sign", "K"},
} {
// Enough of them to push a later heading well past the drift.
doc := "<p>" + strings.Repeat(tc.char, 400) + "</p>\n<h2>After</h2>\n<p>body</p>"
func() {
defer func() {
if p := recover(); p != nil {
t.Errorf("FAIL: %s: %v", tc.name, p)
}
}()
html, heads := Outline([]byte(doc))
if len(heads) != 1 || heads[0].Text != "After" {
t.Errorf("FAIL: %s: headings = %+v", tc.name, heads)
return
}
if !strings.Contains(string(html), tc.char) {
t.Errorf("FAIL: %s: the document's own characters did not survive", tc.name)
return
}
t.Logf("ok: %s", tc.name)
}()
}
}
// The lowering used for searching must never change the length, which
// is the property the offsets depend on.
func TestAsciiLowerPreservesLength(t *testing.T) {
for _, s := range []string{"", "abc", "ABC", "<H2>Ⱥ</H2>", "İstanbul", "K", "\xff\xfe"} {
if got := asciiLower(s); len(got) != len(s) {
t.Errorf("FAIL: asciiLower(%q) is %d bytes, want %d", s, len(got), len(s))
continue
}
t.Logf("ok: %q keeps its length", s)
}
if asciiLower("<H2 ID=\"x\">") != "<h2 id=\"x\">" {
t.Fatal("FAIL: ASCII was not lowered")
}
if asciiLower("Ⱥ") != "Ⱥ" {
t.Fatal("FAIL: a non-ASCII rune was altered")
}
}