mirror of
https://github.com/anchore/syft.git
synced 2026-11-02 15:36:41 +01:00
parseErlangString advances past a backslash escape and then checks len(data) >= *i before reading the escaped byte. That condition is almost always true (it only turns false once *i runs off the end), so the intended out-of-range guard fires on the very first escape character it sees instead of only at EOF. Any rebar.lock or OTP resource file containing a backslash in a quoted string (a Windows git path, an escaped quote, anything) fails to parse and the whole file, and every package in it, gets dropped. Flip the comparison to *i >= len(data) so the guard only trips when the escape is genuinely truncated, and add a regression test for a string with an escaped quote. Signed-off-by: Arpit Jain <arpitjain099@gmail.com>
219 lines
4.0 KiB
Go
219 lines
4.0 KiB
Go
package erlang
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/anchore/syft/syft/internal/parsing"
|
|
)
|
|
|
|
type erlangNode struct {
|
|
value any
|
|
}
|
|
|
|
var errSkipComments = errors.New("")
|
|
|
|
func (e erlangNode) Slice() []erlangNode {
|
|
out, ok := e.value.([]erlangNode)
|
|
if ok {
|
|
return out
|
|
}
|
|
return []erlangNode{}
|
|
}
|
|
|
|
func (e erlangNode) String() string {
|
|
out, ok := e.value.(string)
|
|
if ok {
|
|
return out
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (e erlangNode) Get(index int) erlangNode {
|
|
s := e.Slice()
|
|
if len(s) > index {
|
|
return s[index]
|
|
}
|
|
return erlangNode{}
|
|
}
|
|
|
|
func node(value any) erlangNode {
|
|
return erlangNode{
|
|
value: value,
|
|
}
|
|
}
|
|
|
|
// parseErlang basic parser for erlang, used by rebar.lock
|
|
func parseErlang(reader io.Reader) (erlangNode, error) {
|
|
data, err := io.ReadAll(reader) //nolint:gocritic // custom parser requires []byte
|
|
if err != nil {
|
|
return node(nil), err
|
|
}
|
|
|
|
out := erlangNode{
|
|
value: []erlangNode{},
|
|
}
|
|
|
|
i := 0
|
|
for i < len(data) {
|
|
item, err := parseErlangBlock(data, &i)
|
|
if err == errSkipComments {
|
|
parsing.SkipWhitespace(data, &i)
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return node(nil), fmt.Errorf("%w\n%s", err, parsing.PrintError(data, i))
|
|
}
|
|
|
|
parsing.SkipWhitespace(data, &i)
|
|
|
|
if i, ok := item.value.(string); ok && i == "." {
|
|
continue
|
|
}
|
|
|
|
out.value = append(out.value.([]erlangNode), item)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func parseErlangBlock(data []byte, i *int) (erlangNode, error) {
|
|
block, err := parseErlangNode(data, i)
|
|
if err != nil {
|
|
return node(nil), err
|
|
}
|
|
|
|
parsing.SkipWhitespace(data, i)
|
|
*i++ // skip the trailing .
|
|
return block, nil
|
|
}
|
|
|
|
func parseErlangNode(data []byte, i *int) (erlangNode, error) {
|
|
parsing.SkipWhitespace(data, i)
|
|
c := data[*i]
|
|
switch c {
|
|
case '[', '{':
|
|
offset := *i + 1
|
|
parsing.SkipWhitespace(data, &offset)
|
|
c2 := data[offset]
|
|
|
|
// Add support for empty lists
|
|
if (c == '[' && c2 == ']') || (c == '{' && c2 == '}') {
|
|
*i = offset + 1
|
|
return node(nil), nil
|
|
}
|
|
|
|
return parseErlangList(data, i)
|
|
case '"':
|
|
fallthrough
|
|
case '\'':
|
|
return parseErlangString(data, i)
|
|
case '<':
|
|
return parseErlangAngleString(data, i)
|
|
case '%':
|
|
parseErlangComment(data, i)
|
|
return node(nil), errSkipComments
|
|
}
|
|
|
|
if parsing.IsLiteral(c) {
|
|
return parseErlangLiteral(data, i)
|
|
}
|
|
|
|
return erlangNode{}, fmt.Errorf("invalid literal character: %s", string(c))
|
|
}
|
|
|
|
func parseErlangLiteral(data []byte, i *int) (erlangNode, error) {
|
|
var buf bytes.Buffer
|
|
for *i < len(data) {
|
|
c := data[*i]
|
|
if parsing.IsLiteral(c) {
|
|
buf.WriteByte(c)
|
|
} else {
|
|
break
|
|
}
|
|
*i++
|
|
}
|
|
return node(buf.String()), nil
|
|
}
|
|
|
|
func parseErlangAngleString(data []byte, i *int) (erlangNode, error) {
|
|
*i += 2
|
|
out, err := parseErlangString(data, i)
|
|
*i += 2
|
|
return out, err
|
|
}
|
|
|
|
func parseErlangString(data []byte, i *int) (erlangNode, error) {
|
|
delim := data[*i]
|
|
*i++
|
|
var buf bytes.Buffer
|
|
for *i < len(data) {
|
|
c := data[*i]
|
|
if c == delim {
|
|
*i++
|
|
return node(buf.String()), nil
|
|
}
|
|
if c == '\\' {
|
|
*i++
|
|
if *i >= len(data) {
|
|
return node(nil), fmt.Errorf("invalid escape without closed string at %d", *i)
|
|
}
|
|
c = data[*i]
|
|
}
|
|
buf.WriteByte(c)
|
|
*i++
|
|
}
|
|
return node(nil), fmt.Errorf("unterminated string at %d", *i)
|
|
}
|
|
|
|
func parseErlangList(data []byte, i *int) (erlangNode, error) {
|
|
*i++
|
|
out := erlangNode{
|
|
value: []erlangNode{},
|
|
}
|
|
for *i < len(data) {
|
|
item, err := parseErlangNode(data, i)
|
|
if err != nil {
|
|
if err == errSkipComments {
|
|
parsing.SkipWhitespace(data, i)
|
|
continue
|
|
}
|
|
return node(nil), err
|
|
}
|
|
out.value = append(out.value.([]erlangNode), item)
|
|
parsing.SkipWhitespace(data, i)
|
|
c := data[*i]
|
|
switch c {
|
|
case ',':
|
|
*i++
|
|
continue
|
|
case '%':
|
|
// Starts a new comment node
|
|
continue
|
|
case ']', '}':
|
|
*i++
|
|
return out, nil
|
|
default:
|
|
return node(nil), fmt.Errorf("unexpected character: %s", string(c))
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func parseErlangComment(data []byte, i *int) {
|
|
for *i < len(data) {
|
|
c := data[*i]
|
|
|
|
*i++
|
|
|
|
// Rest of a line is a comment. Deals with CR, LF and CR/LF
|
|
if c == '\n' {
|
|
break
|
|
} else if c == '\r' && data[*i] == '\n' {
|
|
*i++
|
|
break
|
|
}
|
|
}
|
|
}
|