syft/syft/pkg/cataloger/lua/rockspec_parser.go
Weston Steimel dce99543b2
feat: update to go 1.24.x (#3660)
* feat: update to go 1.24.x

Update to building with go 1.24.x so that the main module version gets
set during `go build`

Signed-off-by: Weston Steimel <author@code.w.steimel.me.uk>

* chore: bump golangci-lint for go 1.24.x support

Signed-off-by: Weston Steimel <author@code.w.steimel.me.uk>

* chore: appease the updated linter

Signed-off-by: Weston Steimel <author@code.w.steimel.me.uk>

* chore: fix test logging for go 1.24

Signed-off-by: Weston Steimel <author@code.w.steimel.me.uk>

---------

Signed-off-by: Weston Steimel <author@code.w.steimel.me.uk>
2025-02-13 10:08:10 -05:00

599 lines
10 KiB
Go

package lua
import (
"bytes"
"fmt"
"io"
"github.com/anchore/syft/syft/internal/parsing"
)
type rockspec struct {
value []rockspecNode
}
type rockspecNode struct {
key string
value interface{}
}
func (r rockspecNode) Slice() []rockspecNode {
out, ok := r.value.([]rockspecNode)
if ok {
return out
}
return nil
}
func (r rockspecNode) String() string {
out, ok := r.value.(string)
if ok {
return out
}
return ""
}
var noReturn = rockspec{
value: nil,
}
// parseRockspec basic parser for rockspec
func parseRockspecData(reader io.Reader) (rockspec, error) {
data, err := io.ReadAll(reader)
if err != nil {
return noReturn, err
}
i := 0
locals := make(map[string]string)
blocks, err := parseRockspecBlock(data, &i, locals)
if err != nil {
return noReturn, err
}
return rockspec{
value: blocks,
}, nil
}
func parseRockspecBlock(data []byte, i *int, locals map[string]string) ([]rockspecNode, error) {
var out []rockspecNode
var iterator func(data []byte, i *int, locals map[string]string) (*rockspecNode, error)
parsing.SkipWhitespace(data, i)
if *i >= len(data) && len(out) > 0 {
return nil, fmt.Errorf("unexpected end of block at %d", *i)
}
c := data[*i]
// Block starting with a comment
if c == '-' {
parseComment(data, i)
parsing.SkipWhitespace(data, i)
c = data[*i]
}
switch {
case c == '"' || c == '\'':
iterator = parseRockspecListItem
case isLiteral(c):
iterator = parseRockspecNode
default:
return nil, fmt.Errorf("unexpected character: %s", string(c))
}
for *i < len(data) {
item, err := iterator(data, i, locals)
if err != nil {
return nil, fmt.Errorf("%w\n%s", err, parsing.PrintError(data, *i))
}
parsing.SkipWhitespace(data, i)
if (item.key == "," || item.key == "-") && item.value == nil {
continue
}
if item.key == "}" && item.value == nil {
break
}
out = append(out, *item)
}
return out, nil
}
//nolint:funlen, gocognit
func parseRockspecNode(data []byte, i *int, locals map[string]string) (*rockspecNode, error) {
parsing.SkipWhitespace(data, i)
if *i >= len(data) {
return nil, fmt.Errorf("unexpected end of node at %d", *i)
}
c := data[*i]
if c == ',' || c == ';' || c == '}' {
*i++
return &rockspecNode{
key: string(c),
}, nil
}
if c == '-' {
offset := *i + 1
if offset >= len(data) {
return nil, fmt.Errorf("unexpected character: %s", string(c))
}
c2 := data[offset]
if c2 != '-' {
return nil, fmt.Errorf("unexpected character: %s", string(c2))
}
parseComment(data, i)
return &rockspecNode{
key: string(c),
}, nil
}
if !isLiteral(c) {
return nil, fmt.Errorf("invalid literal character: %s", string(c))
}
key, err := parseRockspecLiteral(data, i, locals)
if err != nil {
return nil, err
}
parsing.SkipWhitespace(data, i)
if *i >= len(data) {
return nil, fmt.Errorf("unexpected end of node at %d", *i)
}
if key == "local" {
err := parseLocal(data, i, locals)
if err != nil {
return nil, err
}
return &rockspecNode{
key: ",",
}, nil
}
c = data[*i]
if c != '=' {
return nil, fmt.Errorf("unexpected character: %s", string(c))
}
*i++
parsing.SkipWhitespace(data, i)
if *i >= len(data) {
return nil, fmt.Errorf("unexpected end of node at %d", *i)
}
if key == "build" {
skipBuildNode(data, i)
return &rockspecNode{
key: ",",
}, nil
}
c = data[*i]
switch c {
case '{':
offset := *i + 1
parsing.SkipWhitespace(data, &offset)
if offset >= len(data) {
return nil, fmt.Errorf("unterminated block at %d", *i)
}
c2 := data[offset]
// Add support for empty lists
if c == '{' && c2 == '}' {
*i = offset + 1
return &rockspecNode{}, nil
}
*i = offset
parsing.SkipWhitespace(data, i)
obj, err := parseRockspecBlock(data, i, locals)
if err != nil {
return nil, err
}
value := obj
return &rockspecNode{
key, value,
}, nil
case '(':
skipExpression(data, i)
return &rockspecNode{
key: ",",
}, nil
case '[':
offset := *i + 1
if offset >= len(data) {
return nil, fmt.Errorf("unterminated block at %d", *i)
}
c2 := data[offset]
if c2 != '[' {
return nil, fmt.Errorf("unexpected character: %s", string(c))
}
*i++
str, err := parseRockspecString(data, i, locals)
if err != nil {
return nil, err
}
value := str.String()
c = data[*i]
if c != ']' {
return nil, fmt.Errorf("unexpected character: %s", string(c))
}
*i++
return &rockspecNode{
key, value,
}, nil
}
value, err := parseRockspecValue(data, i, locals, "")
if err != nil {
return nil, err
}
return &rockspecNode{
key, value,
}, nil
}
func parseRockspecListItem(data []byte, i *int, locals map[string]string) (*rockspecNode, error) {
parsing.SkipWhitespace(data, i)
if *i >= len(data) {
return nil, fmt.Errorf("unexpected end of block at %d", *i)
}
c := data[*i]
if c == ',' || c == ';' || c == '}' {
*i++
return &rockspecNode{
key: string(c),
}, nil
}
if c == '-' {
offset := *i + 1
if offset >= len(data) {
return nil, fmt.Errorf("unexpected character: %s", string(c))
}
c2 := data[offset]
if c2 != '-' {
return nil, fmt.Errorf("unexpected character: %s", string(c2))
}
parseComment(data, i)
return &rockspecNode{
key: string(c),
}, nil
}
str, err := parseRockspecString(data, i, locals)
if err != nil {
return nil, err
}
return str, nil
}
func parseRockspecValue(data []byte, i *int, locals map[string]string, initialValue string) (string, error) {
c := data[*i]
var value string
switch c {
case '"', '\'':
str, err := parseRockspecString(data, i, locals)
if err != nil {
return "", err
}
value = str.value.(string)
default:
local, err := parseRockspecLiteral(data, i, locals)
if err != nil {
return "", err
}
l, ok := locals[local]
if !ok {
return "", fmt.Errorf("unknown local: %s", local)
}
value = l
}
value = fmt.Sprintf("%s%s", initialValue, value)
skipWhitespaceNoNewLine(data, i)
if len(data) > *i+2 {
if data[*i] == '.' && data[*i+1] == '.' {
*i += 2
skipWhitespaceNoNewLine(data, i)
if *i >= len(data) {
return "", fmt.Errorf("unexpected end of expression at %d", *i)
}
v, err := parseRockspecValue(data, i, locals, value)
if err != nil {
return "", err
}
value = v
}
}
return value, nil
}
func parseRockspecLiteral(data []byte, i *int, locals map[string]string) (string, error) {
var buf bytes.Buffer
out:
for *i < len(data) {
c := data[*i]
switch {
case c == '[':
*i++
nested, err := parseRockspecString(data, i, locals)
if err != nil {
return "", err
}
c = data[*i]
if c != ']' {
return "", fmt.Errorf("unterminated literal at %d", *i)
}
buf.WriteString(fmt.Sprintf("[\"%s\"]", nested.String()))
case isLiteral(c):
buf.WriteByte(c)
default:
break out
}
*i++
}
return buf.String(), nil
}
func parseRockspecString(data []byte, i *int, _ map[string]string) (*rockspecNode, error) {
delim := data[*i]
var endDelim byte
switch delim {
case '"', '\'':
endDelim = delim
case '[':
endDelim = ']'
}
*i++
var buf bytes.Buffer
for *i < len(data) {
c := data[*i]
if c == endDelim {
*i++
str := rockspecNode{value: buf.String()}
return &str, nil
}
buf.WriteByte(c)
*i++
}
return nil, fmt.Errorf("unterminated string at %d", *i)
}
func parseComment(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
}
}
}
//nolint:funlen
func parseLocal(data []byte, i *int, locals map[string]string) error {
keys := []string{}
values := []string{}
keys:
for {
parsing.SkipWhitespace(data, i)
key, err := parseRockspecLiteral(data, i, locals)
if err != nil {
return err
}
if key == "function" {
err := skipFunction(data, i)
if err != nil {
return err
}
return nil
}
keys = append(keys, key)
parsing.SkipWhitespace(data, i)
c := data[*i]
switch c {
case ',':
*i++
continue
case '=':
*i++
break keys
default:
return fmt.Errorf("unexpected character: %s", string(c))
}
}
values:
for {
skipWhitespaceNoNewLine(data, i)
c := data[*i]
switch c {
case '"', '\'':
value, err := parseRockspecString(data, i, locals)
if err != nil {
return err
}
values = append(values, value.value.(string))
default:
ref, err := parseRockspecLiteral(data, i, locals)
if err != nil {
return err
}
// Skip if it's an expression
skipWhitespaceNoNewLine(data, i)
c := data[*i]
var value string
if c != '\n' && c != '\r' {
skipExpression(data, i)
value = ""
} else {
value = locals[ref]
}
values = append(values, value)
}
skipWhitespaceNoNewLine(data, i)
c = data[*i]
switch c {
case ',':
*i++
continue
case '\n', '\r':
parsing.SkipWhitespace(data, i)
break values
}
}
if len(keys) != len(values) {
return fmt.Errorf("expected %d values got %d", len(keys), len(values))
}
for i := 0; i < len(keys); i++ {
locals[keys[i]] = values[i]
}
return nil
}
func skipBuildNode(data []byte, i *int) {
bracesCount := 0
for *i < len(data) {
c := data[*i]
switch c {
case '{':
bracesCount++
case '}':
bracesCount--
}
if bracesCount == 0 {
return
}
*i++
}
}
func skipFunction(data []byte, i *int) error {
blocks := 1
for *i < len(data)-5 {
if parsing.IsWhitespace(data[*i]) {
switch {
case string(data[*i+1:*i+3]) == "if" && parsing.IsWhitespace(data[*i+3]):
blocks++
*i += 3
case string(data[*i+1:*i+4]) == "end" && parsing.IsWhitespace(data[*i+4]):
blocks--
*i += 4
if blocks == 0 {
return nil
}
default:
*i++
}
} else {
*i++
}
}
return fmt.Errorf("unterminated function at %d", *i)
}
func skipExpression(data []byte, i *int) {
parseComment(data, i)
}
func skipWhitespaceNoNewLine(data []byte, i *int) {
for *i < len(data) && (data[*i] == ' ' || data[*i] == '\t') {
*i++
}
}
func isLiteral(c byte) bool {
if c == '[' || c == ']' {
return true
}
if c == '.' {
return false
}
return parsing.IsLiteral(c)
}