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(is) - w.WriteString(``) + _, _ = w.WriteString(``) + _, _ = w.WriteString(is) + _, _ = 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()