diff --git a/.gitignore b/.gitignore
index 6e4db92..06c135f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,9 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
+
+.DS_Store
+fuzz/corpus
+fuzz/crashers
+fuzz/suppressions
+fuzz/fuzz-fuzz.zip
diff --git a/Makefile b/Makefile
index 2ac9f47..cfeb21b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,16 @@
-.PHONY: test
+.PHONY: test fuzz
test:
go test -coverprofile=profile.out -coverpkg=github.com/yuin/goldmark,github.com/yuin/goldmark/ast,github.com/yuin/goldmark/extension,github.com/yuin/goldmark/extension/ast,github.com/yuin/goldmark/parser,github.com/yuin/goldmark/renderer,github.com/yuin/goldmark/renderer/html,github.com/yuin/goldmark/text,github.com/yuin/goldmark/util ./...
cov: test
go tool cover -html=profile.out
+
+fuzz:
+ which go-fuzz 2>&1 > /dev/null || (GO111MODULE=off go get -u github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build; GO111MODULE=off go get -d github.com/dvyukov/go-fuzz-corpus; true)
+ cd ./fuzz && go-fuzz-build
+ rm -rf ./fuzz/corpus
+ rm -rf ./fuzz/crashers
+ rm -rf ./fuzz/suppressions
+ rm -f ./fuzz/fuzz-fuzz.zip
+ cd ./fuzz && go-fuzz
diff --git a/ast/ast.go b/ast/ast.go
index 81b0da1..06d1883 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -348,7 +348,6 @@ func (n *BaseNode) SetAttribute(name, value []byte) {
}
}
n.attributes = append(n.attributes, Attribute{name, value})
- return
}
// Attribute implements Node.Attribute.
@@ -396,10 +395,8 @@ func DumpHelper(v Node, source []byte, level int, kv map[string]string, cb func(
fmt.Printf("\"\n")
fmt.Printf("%sHasBlankPreviousLines: %v\n", indent2, v.HasBlankPreviousLines())
}
- if kv != nil {
- for name, value := range kv {
- fmt.Printf("%s%s: %s\n", indent2, name, value)
- }
+ for name, value := range kv {
+ fmt.Printf("%s%s: %s\n", indent2, name, value)
}
if cb != nil {
cb(level + 1)
diff --git a/extension/definition_list.go b/extension/definition_list.go
index 1d17e38..8622319 100644
--- a/extension/definition_list.go
+++ b/extension/definition_list.go
@@ -28,7 +28,7 @@ func (b *definitionListParser) Open(parent gast.Node, reader text.Reader, pc par
}
line, _ := reader.PeekLine()
pos := pc.BlockOffset()
- if line[pos] != ':' {
+ if pos < 0 || line[pos] != ':' {
return nil, parser.NoChildren
}
@@ -105,10 +105,13 @@ func NewDefinitionDescriptionParser() parser.BlockParser {
func (b *definitionDescriptionParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
line, _ := reader.PeekLine()
pos := pc.BlockOffset()
- if line[pos] != ':' {
+ if pos < 0 || line[pos] != ':' {
return nil, parser.NoChildren
}
list, _ := parent.(*ast.DefinitionList)
+ if list == nil {
+ return nil, parser.NoChildren
+ }
para := list.TemporaryParagraph
list.TemporaryParagraph = nil
if para != nil {
@@ -183,18 +186,18 @@ func (r *DefinitionListHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFunc
func (r *DefinitionListHTMLRenderer) renderDefinitionList(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
if entering {
- w.WriteString("
\n")
+ _, _ = w.WriteString("\n")
} else {
- w.WriteString("
\n")
+ _, _ = w.WriteString("
\n")
}
return gast.WalkContinue, nil
}
func (r *DefinitionListHTMLRenderer) renderDefinitionTerm(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
if entering {
- w.WriteString("")
+ _, _ = w.WriteString("")
} else {
- w.WriteString("\n")
+ _, _ = w.WriteString("\n")
}
return gast.WalkContinue, nil
}
@@ -203,12 +206,12 @@ func (r *DefinitionListHTMLRenderer) renderDefinitionDescription(w util.BufWrite
if entering {
n := node.(*ast.DefinitionDescription)
if n.IsTight {
- w.WriteString("")
+ _, _ = w.WriteString("")
} else {
- w.WriteString("\n")
+ _, _ = w.WriteString("\n")
}
} else {
- w.WriteString("\n")
+ _, _ = w.WriteString("\n")
}
return gast.WalkContinue, nil
}
diff --git a/extension/footnote.go b/extension/footnote.go
index fc13ff3..0e78fe7 100644
--- a/extension/footnote.go
+++ b/extension/footnote.go
@@ -29,7 +29,7 @@ func NewFootnoteBlockParser() parser.BlockParser {
func (b *footnoteBlockParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
line, segment := reader.PeekLine()
pos := pc.BlockOffset()
- if line[pos] != '[' {
+ if pos < 0 || line[pos] != '[' {
return nil, parser.NoChildren
}
pos++
@@ -37,7 +37,7 @@ func (b *footnoteBlockParser) Open(parent gast.Node, reader text.Reader, pc pars
return nil, parser.NoChildren
}
open := pos + 1
- closes := -1
+ closes := 0
closure := util.FindClosure(line[pos+1:], '[', ']', false, false)
if closure > -1 {
closes = pos + 1 + closure
@@ -52,10 +52,15 @@ func (b *footnoteBlockParser) Open(parent gast.Node, reader text.Reader, pc pars
if util.IsBlank(label) {
return nil, parser.NoChildren
}
+ item := ast.NewFootnote(label)
+
pos = pos + 2 + closes - open + 2
+ if pos >= len(line) {
+ reader.Advance(pos)
+ return item, parser.NoChildren
+ }
childpos, padding := util.IndentPosition(line[pos:], pos, 1)
reader.AdvanceAndSetPadding(pos+childpos, padding)
- item := ast.NewFootnote(label)
return item, parser.HasChildren
}
@@ -207,13 +212,13 @@ func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byt
if entering {
n := node.(*ast.FootnoteLink)
is := strconv.Itoa(n.Index)
- w.WriteString(``)
+ _, _ = w.WriteString(``)
}
return gast.WalkContinue, nil
}
@@ -222,12 +227,12 @@ func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, n
n := node.(*ast.Footnote)
is := strconv.Itoa(n.Index)
if entering {
- w.WriteString(``)
- w.WriteString("\n")
+ _, _ = w.WriteString(``)
+ _, _ = w.WriteString("\n")
} else {
- w.WriteString("\n")
+ _, _ = w.WriteString("\n")
}
return gast.WalkContinue, nil
}
@@ -238,20 +243,20 @@ func (r *FootnoteHTMLRenderer) renderFootnoteList(w util.BufWriter, source []byt
tag = "div"
}
if entering {
- w.WriteString("<")
- w.WriteString(tag)
- w.WriteString(` class="footnotes" role="doc-endnotes">`)
+ _, _ = w.WriteString("<")
+ _, _ = w.WriteString(tag)
+ _, _ = w.WriteString(` class="footnotes" role="doc-endnotes">`)
if r.Config.XHTML {
- w.WriteString("\n
\n")
+ _, _ = w.WriteString("\n
\n")
} else {
- w.WriteString("\n
\n")
+ _, _ = w.WriteString("\n
\n")
}
- w.WriteString("\n")
+ _, _ = w.WriteString("\n")
} else {
- w.WriteString("
\n")
- w.WriteString("<")
- w.WriteString(tag)
- w.WriteString(">\n")
+ _, _ = w.WriteString("
\n")
+ _, _ = w.WriteString("<")
+ _, _ = w.WriteString(tag)
+ _, _ = w.WriteString(">\n")
}
return gast.WalkContinue, nil
}
diff --git a/extension/table.go b/extension/table.go
index 4ff7b73..a5bfce6 100644
--- a/extension/table.go
+++ b/extension/table.go
@@ -169,22 +169,22 @@ func (r *TableHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegistere
func (r *TableHTMLRenderer) renderTable(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
if entering {
- w.WriteString("\n")
+ _, _ = w.WriteString("\n")
} else {
- w.WriteString("
\n")
+ _, _ = w.WriteString("
\n")
}
return gast.WalkContinue, nil
}
func (r *TableHTMLRenderer) renderTableHeader(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
if entering {
- w.WriteString("\n")
- w.WriteString("\n")
+ _, _ = w.WriteString("\n")
+ _, _ = w.WriteString("\n")
} else {
- w.WriteString("
\n")
- w.WriteString("\n")
+ _, _ = w.WriteString("
\n")
+ _, _ = w.WriteString("\n")
if n.NextSibling() != nil {
- w.WriteString("\n")
+ _, _ = w.WriteString("\n")
}
}
return gast.WalkContinue, nil
@@ -192,11 +192,11 @@ func (r *TableHTMLRenderer) renderTableHeader(w util.BufWriter, source []byte, n
func (r *TableHTMLRenderer) renderTableRow(w util.BufWriter, source []byte, n gast.Node, entering bool) (gast.WalkStatus, error) {
if entering {
- w.WriteString("\n")
+ _, _ = w.WriteString("
\n")
} else {
- w.WriteString("
\n")
+ _, _ = w.WriteString("\n")
if n.Parent().LastChild() == n {
- w.WriteString("\n")
+ _, _ = w.WriteString("\n")
}
}
return gast.WalkContinue, nil
diff --git a/fuzz/fuzz.go b/fuzz/fuzz.go
new file mode 100644
index 0000000..f56dec6
--- /dev/null
+++ b/fuzz/fuzz.go
@@ -0,0 +1,28 @@
+package fuzz
+
+import (
+ "bytes"
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/extension"
+ "github.com/yuin/goldmark/renderer/html"
+)
+
+func Fuzz(data []byte) int {
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ extension.DefinitionList,
+ extension.Footnote,
+ extension.GFM,
+ extension.Typographer,
+ ),
+ )
+ var b bytes.Buffer
+ if err := markdown.Convert(data, &b); err != nil {
+ return 0
+ }
+
+ return 1
+}
diff --git a/fuzz/fuzz_test.go b/fuzz/fuzz_test.go
new file mode 100644
index 0000000..7a9b899
--- /dev/null
+++ b/fuzz/fuzz_test.go
@@ -0,0 +1,41 @@
+package fuzz
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "testing"
+
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/extension"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/util"
+)
+
+var _ = fmt.Printf
+
+func TestFuzz(t *testing.T) {
+ crasher := "6dff3d03167cb144d4e2891edac76ee740a77bc7"
+ data, err := ioutil.ReadFile("crashers/" + crasher)
+ if err != nil {
+ return
+ }
+ fmt.Printf("%s\n", util.VisualizeSpaces(data))
+ fmt.Println("||||||||||||||||||||||")
+ markdown := goldmark.New(
+ goldmark.WithRendererOptions(
+ html.WithUnsafe(),
+ ),
+ goldmark.WithExtensions(
+ extension.DefinitionList,
+ extension.Footnote,
+ extension.GFM,
+ extension.Typographer,
+ ),
+ )
+ var b bytes.Buffer
+ if err := markdown.Convert(data, &b); err != nil {
+ panic(err)
+ }
+ fmt.Println(b.String())
+}
diff --git a/parser/atx_heading.go b/parser/atx_heading.go
index fbcfc5c..669fe6e 100644
--- a/parser/atx_heading.go
+++ b/parser/atx_heading.go
@@ -77,6 +77,9 @@ func NewATXHeadingParser(opts ...HeadingOption) BlockParser {
func (b *atxHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
line, segment := reader.PeekLine()
pos := pc.BlockOffset()
+ if pos < 0 {
+ return nil, NoChildren
+ }
i := pos
for ; i < len(line) && line[i] == '#'; i++ {
}
diff --git a/parser/code_block.go b/parser/code_block.go
index 4125587..6b149a7 100644
--- a/parser/code_block.go
+++ b/parser/code_block.go
@@ -55,7 +55,7 @@ func (b *codeBlockParser) Close(node ast.Node, reader text.Reader, pc Context) {
lines := node.Lines()
length := lines.Len() - 1
source := reader.Source()
- for {
+ for length >= 0 {
line := lines.At(length)
if util.IsBlank(line.Value(source)) {
length--
diff --git a/parser/fcode_block.go b/parser/fcode_block.go
index a837d2b..3710d3f 100644
--- a/parser/fcode_block.go
+++ b/parser/fcode_block.go
@@ -30,7 +30,7 @@ var fencedCodeBlockInfoKey = NewContextKey()
func (b *fencedCodeBlockParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
line, segment := reader.PeekLine()
pos := pc.BlockOffset()
- if line[pos] != '`' && line[pos] != '~' {
+ if pos < 0 || (line[pos] != '`' && line[pos] != '~') {
return nil, NoChildren
}
findent := pos
diff --git a/parser/html_block.go b/parser/html_block.go
index eb822ef..a8112ae 100644
--- a/parser/html_block.go
+++ b/parser/html_block.go
@@ -109,7 +109,7 @@ func (b *htmlBlockParser) Open(parent ast.Node, reader text.Reader, pc Context)
var node *ast.HTMLBlock
line, segment := reader.PeekLine()
last := pc.LastOpenedBlock().Node
- if pos := pc.BlockOffset(); line[pos] != '<' {
+ if pos := pc.BlockOffset(); pos < 0 || line[pos] != '<' {
return nil, NoChildren
}
diff --git a/parser/list.go b/parser/list.go
index a180400..88bcc2d 100644
--- a/parser/list.go
+++ b/parser/list.go
@@ -35,7 +35,7 @@ func parseListItem(line []byte) ([6]int, listItemType) {
ret[1] = i
ret[2] = i
var typ listItemType
- if i < l && line[i] == '-' || line[i] == '*' || line[i] == '+' {
+ if i < l && (line[i] == '-' || line[i] == '*' || line[i] == '+') {
i++
ret[3] = i
typ = bulletList
@@ -46,7 +46,7 @@ func parseListItem(line []byte) ([6]int, listItemType) {
if ret[3] == ret[2] || ret[3]-ret[2] > 9 {
return ret, notList
}
- if i < l && line[i] == '.' || line[i] == ')' {
+ if i < l && (line[i] == '.' || line[i] == ')') {
i++
ret[3] = i
} else {
@@ -56,12 +56,17 @@ func parseListItem(line []byte) ([6]int, listItemType) {
} else {
return ret, notList
}
- if line[i] != '\n' {
+ if i < l && line[i] != '\n' {
w, _ := util.IndentWidth(line[i:], 0)
if w == 0 {
return ret, notList
}
}
+ if i >= l {
+ ret[4] = -1
+ ret[5] = -1
+ return ret, typ
+ }
ret[4] = i
ret[5] = len(line)
if line[ret[5]-1] == '\n' && line[i] != '\n' {
@@ -80,7 +85,7 @@ func matchesListItem(source []byte, strict bool) ([6]int, listItemType) {
func calcListOffset(source []byte, match [6]int) int {
offset := 0
- if util.IsBlank(source[match[4]:]) { // list item starts with a blank line
+ if match[4] < 0 || util.IsBlank(source[match[4]:]) { // list item starts with a blank line
offset = 1
} else {
offset, _ = util.IndentWidth(source[match[4]:], match[4])
diff --git a/parser/list_item.go b/parser/list_item.go
index c0ae496..2ced38d 100644
--- a/parser/list_item.go
+++ b/parser/list_item.go
@@ -36,7 +36,7 @@ func (b *listItemParser) Open(parent ast.Node, reader text.Reader, pc Context) (
}
itemOffset := calcListOffset(line, match)
node := ast.NewListItem(match[3] + itemOffset)
- if match[5]-match[4] == 1 {
+ if match[4] < 0 || match[5]-match[4] == 1 {
return node, NoChildren
}
diff --git a/parser/parser.go b/parser/parser.go
index 6352353..21bd035 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -156,6 +156,7 @@ type Context interface {
// BlockOffset returns a first non-space character position on current line.
// This value is valid only for BlockParser.Open.
+ // BlockOffset returns -1 if current line is blank.
BlockOffset() int
// BlockOffset sets a first non-space character position on current line.
@@ -833,7 +834,11 @@ retry:
//currentLineNum, _ = reader.Position()
line, _ = reader.PeekLine()
w, pos = util.IndentWidth(line, 0)
- pc.SetBlockOffset(pos)
+ if w >= len(line) {
+ pc.SetBlockOffset(-1)
+ } else {
+ pc.SetBlockOffset(pos)
+ }
shouldPeek = false
if line == nil || line[0] == '\n' {
break
diff --git a/parser/setext_headings.go b/parser/setext_headings.go
index da3a079..1012c5d 100644
--- a/parser/setext_headings.go
+++ b/parser/setext_headings.go
@@ -65,7 +65,7 @@ func (b *setextHeadingParser) Open(parent ast.Node, reader text.Reader, pc Conte
}
node := ast.NewHeading(level)
node.Lines().Append(segment)
- pc.Set(temporaryParagraphKey, paragraph)
+ pc.Set(temporaryParagraphKey, last)
return node, NoChildren
}
diff --git a/renderer/html/html.go b/renderer/html/html.go
index bfd9761..ca7c7e3 100644
--- a/renderer/html/html.go
+++ b/renderer/html/html.go
@@ -577,28 +577,30 @@ func (d *defaultWriter) Write(writer util.BufWriter, source []byte) {
next := i + 1
if next < limit && source[next] == '#' {
nnext := next + 1
- nc := source[nnext]
- // code point like #x22;
- if nnext < limit && nc == 'x' || nc == 'X' {
- start := nnext + 1
- i, ok = util.ReadWhile(source, [2]int{start, limit}, util.IsHexDecimal)
- if ok && i < limit && source[i] == ';' {
- v, _ := strconv.ParseUint(util.BytesToReadOnlyString(source[start:i]), 16, 32)
- d.RawWrite(writer, source[n:pos])
- n = i + 1
- escapeRune(writer, rune(v))
- continue
- }
- // code point like #1234;
- } else if nc >= '0' && nc <= '9' {
- start := nnext
- i, ok = util.ReadWhile(source, [2]int{start, limit}, util.IsNumeric)
- if ok && i < limit && i-start < 8 && source[i] == ';' {
- v, _ := strconv.ParseUint(util.BytesToReadOnlyString(source[start:i]), 0, 32)
- d.RawWrite(writer, source[n:pos])
- n = i + 1
- escapeRune(writer, rune(v))
- continue
+ if nnext < limit {
+ nc := source[nnext]
+ // code point like #x22;
+ if nnext < limit && nc == 'x' || nc == 'X' {
+ start := nnext + 1
+ i, ok = util.ReadWhile(source, [2]int{start, limit}, util.IsHexDecimal)
+ if ok && i < limit && source[i] == ';' {
+ v, _ := strconv.ParseUint(util.BytesToReadOnlyString(source[start:i]), 16, 32)
+ d.RawWrite(writer, source[n:pos])
+ n = i + 1
+ escapeRune(writer, rune(v))
+ continue
+ }
+ // code point like #1234;
+ } else if nc >= '0' && nc <= '9' {
+ start := nnext
+ i, ok = util.ReadWhile(source, [2]int{start, limit}, util.IsNumeric)
+ if ok && i < limit && i-start < 8 && source[i] == ';' {
+ v, _ := strconv.ParseUint(util.BytesToReadOnlyString(source[start:i]), 0, 32)
+ d.RawWrite(writer, source[n:pos])
+ n = i + 1
+ escapeRune(writer, rune(v))
+ continue
+ }
}
}
} else {
diff --git a/text/reader.go b/text/reader.go
index d1c435f..b2a4af1 100644
--- a/text/reader.go
+++ b/text/reader.go
@@ -326,6 +326,9 @@ func (r *blockReader) PrecendingCharacter() rune {
break
}
}
+ if i < 0 {
+ return rune('\n')
+ }
rn, _ := utf8.DecodeRune(r.source[i:])
return rn
}
diff --git a/util/util.go b/util/util.go
index c2bac4a..a303cbb 100644
--- a/util/util.go
+++ b/util/util.go
@@ -567,7 +567,7 @@ func URLEscape(v []byte, resolveReference bool) []byte {
i += int(u8len)
n = i
}
- if cob.IsCopied() {
+ if cob.IsCopied() && n < limit {
cob.Write(v[n:])
}
return cob.Bytes()