diff --git a/block.go b/block.go index 563cb290..1320b6d0 100644 --- a/block.go +++ b/block.go @@ -1066,10 +1066,10 @@ func (p *parser) dliPrefix(data []byte) int { i := 0 // need a : followed by a spaces - if data[i] != ':' || data[i+1] != ' ' { + if len(data) < 2 || data[i] != ':' || data[i+1] != ' ' { return 0 } - for data[i] == ' ' { + for i < len(data) && data[i] == ' ' { i++ } return i + 2 @@ -1126,13 +1126,13 @@ func (p *parser) listItem(out *bytes.Buffer, data []byte, flags *int) int { } // skip leading whitespace on first line - for data[i] == ' ' { + for i < len(data) && data[i] == ' ' { i++ } // find the end of the line line := i - for i > 0 && data[i-1] != '\n' { + for i < len(data) && (i == 0 || data[i-1] != '\n') { i++ } @@ -1157,9 +1157,13 @@ gatherlines: i++ // find the end of this line - for data[i-1] != '\n' { + for i < len(data) && data[i-1] != '\n' { i++ } + // if we reached end of data without newline, treat as end of line + if i > len(data) { + i = len(data) + } // if it is an empty line, guess that it is part of this item // and move on to the next line if p.isEmpty(data[line:i]) > 0 { @@ -1246,13 +1250,13 @@ gatherlines: if *flags&LIST_TYPE_DEFINITION != 0 && i < len(data)-1 { // is the next item still a part of this list? next := i - for data[next] != '\n' { + for next < len(data) && data[next] != '\n' { next++ } for next < len(data)-1 && data[next] == '\n' { next++ } - if i < len(data)-1 && data[i] != ':' && data[next] != ':' { + if i < len(data)-1 && data[i] != ':' && next < len(data) && data[next] != ':' { *flags |= LIST_ITEM_END_OF_LIST } } else { diff --git a/block_test.go b/block_test.go index f2d998df..e28a4b3c 100644 --- a/block_test.go +++ b/block_test.go @@ -1854,3 +1854,72 @@ func TestSanitizedAnchorName(t *testing.T) { } } } + +// TestListItemBoundsCheck tests that list items without trailing newlines +// don't cause index out of range panics. +// See: https://github.com/russross/blackfriday/issues/xxx +func TestListItemBoundsCheck(t *testing.T) { + // These test cases should not panic + testCases := []struct { + name string + input string + }{ + { + name: "unordered list without trailing newline", + input: "* item", + }, + { + name: "ordered list without trailing newline", + input: "1. item", + }, + { + name: "unordered list with spaces no newline", + input: "* item with spaces", + }, + { + name: "ordered list with spaces no newline", + input: "1. item with spaces", + }, + { + name: "nested list without trailing newline", + input: "* item\n * nested", + }, + { + name: "list with multiple items no trailing newline", + input: "* item1\n* item2", + }, + { + name: "definition list without trailing newline", + input: "Term\n: Definition", + }, + { + name: "empty list marker", + input: "* ", + }, + { + name: "list with only spaces after marker", + input: "* ", + }, + } + + defer func() { + if r := recover(); r != nil { + t.Errorf("listItem caused panic: %v", r) + } + }() + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("Test %q panicked: %v", tc.name, r) + } + }() + // Just ensure it doesn't panic - output correctness is secondary + _ = runMarkdownBlock(tc.input, 0) + _ = runMarkdownBlock(tc.input, EXTENSION_DEFINITION_LISTS) + _ = runMarkdownBlock(tc.input, EXTENSION_FENCED_CODE) + _ = runMarkdownBlock(tc.input, EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK) + }) + } +}