github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/lua/rockspec_parser.go (about)

     1  package lua
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  
     8  	"github.com/anchore/syft/syft/internal/parsing"
     9  )
    10  
    11  type rockspec struct {
    12  	value []rockspecNode
    13  }
    14  
    15  type rockspecNode struct {
    16  	key   string
    17  	value interface{}
    18  }
    19  
    20  func (r rockspecNode) Slice() []rockspecNode {
    21  	out, ok := r.value.([]rockspecNode)
    22  	if ok {
    23  		return out
    24  	}
    25  	return nil
    26  }
    27  
    28  func (r rockspecNode) String() string {
    29  	out, ok := r.value.(string)
    30  	if ok {
    31  		return out
    32  	}
    33  	return ""
    34  }
    35  
    36  var noReturn = rockspec{
    37  	value: nil,
    38  }
    39  
    40  // parseRockspec basic parser for rockspec
    41  func parseRockspecData(reader io.Reader) (rockspec, error) {
    42  	data, err := io.ReadAll(reader)
    43  	if err != nil {
    44  		return noReturn, err
    45  	}
    46  
    47  	i := 0
    48  	locals := make(map[string]string)
    49  	blocks, err := parseRockspecBlock(data, &i, locals)
    50  
    51  	if err != nil {
    52  		return noReturn, err
    53  	}
    54  
    55  	return rockspec{
    56  		value: blocks,
    57  	}, nil
    58  }
    59  
    60  func parseRockspecBlock(data []byte, i *int, locals map[string]string) ([]rockspecNode, error) {
    61  	var out []rockspecNode
    62  	var iterator func(data []byte, i *int, locals map[string]string) (*rockspecNode, error)
    63  
    64  	parsing.SkipWhitespace(data, i)
    65  
    66  	if *i >= len(data) && len(out) > 0 {
    67  		return nil, fmt.Errorf("unexpected end of block at %d", *i)
    68  	}
    69  
    70  	c := data[*i]
    71  
    72  	// Block starting with a comment
    73  	if c == '-' {
    74  		parseComment(data, i)
    75  		parsing.SkipWhitespace(data, i)
    76  		c = data[*i]
    77  	}
    78  
    79  	switch {
    80  	case c == '"' || c == '\'':
    81  		iterator = parseRockspecListItem
    82  	case isLiteral(c):
    83  		iterator = parseRockspecNode
    84  	default:
    85  		return nil, fmt.Errorf("unexpected character: %s", string(c))
    86  	}
    87  
    88  	for *i < len(data) {
    89  		item, err := iterator(data, i, locals)
    90  		if err != nil {
    91  			return nil, fmt.Errorf("%w\n%s", err, parsing.PrintError(data, *i))
    92  		}
    93  
    94  		parsing.SkipWhitespace(data, i)
    95  
    96  		if (item.key == "," || item.key == "-") && item.value == nil {
    97  			continue
    98  		}
    99  
   100  		if item.key == "}" && item.value == nil {
   101  			break
   102  		}
   103  
   104  		out = append(out, *item)
   105  	}
   106  
   107  	return out, nil
   108  }
   109  
   110  //nolint:funlen, gocognit
   111  func parseRockspecNode(data []byte, i *int, locals map[string]string) (*rockspecNode, error) {
   112  	parsing.SkipWhitespace(data, i)
   113  
   114  	if *i >= len(data) {
   115  		return nil, fmt.Errorf("unexpected end of node at %d", *i)
   116  	}
   117  
   118  	c := data[*i]
   119  
   120  	if c == ',' || c == ';' || c == '}' {
   121  		*i++
   122  		return &rockspecNode{
   123  			key: string(c),
   124  		}, nil
   125  	}
   126  
   127  	if c == '-' {
   128  		offset := *i + 1
   129  		if offset >= len(data) {
   130  			return nil, fmt.Errorf("unexpected character: %s", string(c))
   131  		}
   132  		c2 := data[offset]
   133  
   134  		if c2 != '-' {
   135  			return nil, fmt.Errorf("unexpected character: %s", string(c2))
   136  		}
   137  
   138  		parseComment(data, i)
   139  		return &rockspecNode{
   140  			key: string(c),
   141  		}, nil
   142  	}
   143  
   144  	if !isLiteral(c) {
   145  		return nil, fmt.Errorf("invalid literal character: %s", string(c))
   146  	}
   147  
   148  	key, err := parseRockspecLiteral(data, i, locals)
   149  	if err != nil {
   150  		return nil, err
   151  	}
   152  
   153  	parsing.SkipWhitespace(data, i)
   154  
   155  	if *i >= len(data) {
   156  		return nil, fmt.Errorf("unexpected end of node at %d", *i)
   157  	}
   158  
   159  	if key == "local" {
   160  		err := parseLocal(data, i, locals)
   161  		if err != nil {
   162  			return nil, err
   163  		}
   164  		return &rockspecNode{
   165  			key: ",",
   166  		}, nil
   167  	}
   168  
   169  	c = data[*i]
   170  	if c != '=' {
   171  		return nil, fmt.Errorf("unexpected character: %s", string(c))
   172  	}
   173  
   174  	*i++
   175  	parsing.SkipWhitespace(data, i)
   176  
   177  	if *i >= len(data) {
   178  		return nil, fmt.Errorf("unexpected end of node at %d", *i)
   179  	}
   180  
   181  	if key == "build" {
   182  		skipBuildNode(data, i)
   183  
   184  		return &rockspecNode{
   185  			key: ",",
   186  		}, nil
   187  	}
   188  
   189  	c = data[*i]
   190  
   191  	switch c {
   192  	case '{':
   193  		offset := *i + 1
   194  		parsing.SkipWhitespace(data, &offset)
   195  		if offset >= len(data) {
   196  			return nil, fmt.Errorf("unterminated block at %d", *i)
   197  		}
   198  		c2 := data[offset]
   199  
   200  		// Add support for empty lists
   201  		if c == '{' && c2 == '}' {
   202  			*i = offset + 1
   203  			return &rockspecNode{}, nil
   204  		}
   205  
   206  		*i = offset
   207  		parsing.SkipWhitespace(data, i)
   208  
   209  		obj, err := parseRockspecBlock(data, i, locals)
   210  
   211  		if err != nil {
   212  			return nil, err
   213  		}
   214  		value := obj
   215  
   216  		return &rockspecNode{
   217  			key, value,
   218  		}, nil
   219  	case '(':
   220  		skipExpression(data, i)
   221  		return &rockspecNode{
   222  			key: ",",
   223  		}, nil
   224  	case '[':
   225  		offset := *i + 1
   226  		if offset >= len(data) {
   227  			return nil, fmt.Errorf("unterminated block at %d", *i)
   228  		}
   229  		c2 := data[offset]
   230  
   231  		if c2 != '[' {
   232  			return nil, fmt.Errorf("unexpected character: %s", string(c))
   233  		}
   234  
   235  		*i++
   236  
   237  		str, err := parseRockspecString(data, i, locals)
   238  
   239  		if err != nil {
   240  			return nil, err
   241  		}
   242  		value := str.String()
   243  
   244  		c = data[*i]
   245  
   246  		if c != ']' {
   247  			return nil, fmt.Errorf("unexpected character: %s", string(c))
   248  		}
   249  
   250  		*i++
   251  
   252  		return &rockspecNode{
   253  			key, value,
   254  		}, nil
   255  	}
   256  
   257  	value, err := parseRockspecValue(data, i, locals, "")
   258  
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  
   263  	return &rockspecNode{
   264  		key, value,
   265  	}, nil
   266  }
   267  
   268  func parseRockspecListItem(data []byte, i *int, locals map[string]string) (*rockspecNode, error) {
   269  	parsing.SkipWhitespace(data, i)
   270  
   271  	if *i >= len(data) {
   272  		return nil, fmt.Errorf("unexpected end of block at %d", *i)
   273  	}
   274  
   275  	c := data[*i]
   276  	if c == ',' || c == ';' || c == '}' {
   277  		*i++
   278  		return &rockspecNode{
   279  			key: string(c),
   280  		}, nil
   281  	}
   282  
   283  	if c == '-' {
   284  		offset := *i + 1
   285  		if offset >= len(data) {
   286  			return nil, fmt.Errorf("unexpected character: %s", string(c))
   287  		}
   288  		c2 := data[offset]
   289  
   290  		if c2 != '-' {
   291  			return nil, fmt.Errorf("unexpected character: %s", string(c2))
   292  		}
   293  
   294  		parseComment(data, i)
   295  		return &rockspecNode{
   296  			key: string(c),
   297  		}, nil
   298  	}
   299  
   300  	str, err := parseRockspecString(data, i, locals)
   301  	if err != nil {
   302  		return nil, err
   303  	}
   304  	return str, nil
   305  }
   306  
   307  func parseRockspecValue(data []byte, i *int, locals map[string]string, initialValue string) (string, error) {
   308  	c := data[*i]
   309  
   310  	var value string
   311  
   312  	switch c {
   313  	case '"', '\'':
   314  		str, err := parseRockspecString(data, i, locals)
   315  
   316  		if err != nil {
   317  			return "", err
   318  		}
   319  		value = str.value.(string)
   320  	default:
   321  		local, err := parseRockspecLiteral(data, i, locals)
   322  
   323  		if err != nil {
   324  			return "", err
   325  		}
   326  
   327  		l, ok := locals[local]
   328  
   329  		if !ok {
   330  			return "", fmt.Errorf("unknown local: %s", local)
   331  		}
   332  
   333  		value = l
   334  	}
   335  
   336  	value = fmt.Sprintf("%s%s", initialValue, value)
   337  
   338  	skipWhitespaceNoNewLine(data, i)
   339  
   340  	if len(data) > *i+2 {
   341  		if data[*i] == '.' && data[*i+1] == '.' {
   342  			*i += 2
   343  
   344  			skipWhitespaceNoNewLine(data, i)
   345  
   346  			if *i >= len(data) {
   347  				return "", fmt.Errorf("unexpected end of expression at %d", *i)
   348  			}
   349  
   350  			v, err := parseRockspecValue(data, i, locals, value)
   351  
   352  			if err != nil {
   353  				return "", err
   354  			}
   355  
   356  			value = v
   357  		}
   358  	}
   359  
   360  	return value, nil
   361  }
   362  
   363  func parseRockspecLiteral(data []byte, i *int, locals map[string]string) (string, error) {
   364  	var buf bytes.Buffer
   365  out:
   366  	for *i < len(data) {
   367  		c := data[*i]
   368  		switch {
   369  		case c == '[':
   370  			*i++
   371  			nested, err := parseRockspecString(data, i, locals)
   372  			if err != nil {
   373  				return "", err
   374  			}
   375  			c = data[*i]
   376  			if c != ']' {
   377  				return "", fmt.Errorf("unterminated literal at %d", *i)
   378  			}
   379  			buf.WriteString(fmt.Sprintf("[\"%s\"]", nested.String()))
   380  		case isLiteral(c):
   381  			buf.WriteByte(c)
   382  		default:
   383  			break out
   384  		}
   385  		*i++
   386  	}
   387  	return buf.String(), nil
   388  }
   389  
   390  func parseRockspecString(data []byte, i *int, _ map[string]string) (*rockspecNode, error) {
   391  	delim := data[*i]
   392  	var endDelim byte
   393  	switch delim {
   394  	case '"', '\'':
   395  		endDelim = delim
   396  	case '[':
   397  		endDelim = ']'
   398  	}
   399  
   400  	*i++
   401  	var buf bytes.Buffer
   402  	for *i < len(data) {
   403  		c := data[*i]
   404  		if c == endDelim {
   405  			*i++
   406  			str := rockspecNode{value: buf.String()}
   407  			return &str, nil
   408  		}
   409  		buf.WriteByte(c)
   410  		*i++
   411  	}
   412  	return nil, fmt.Errorf("unterminated string at %d", *i)
   413  }
   414  
   415  func parseComment(data []byte, i *int) {
   416  	for *i < len(data) {
   417  		c := data[*i]
   418  
   419  		*i++
   420  
   421  		// Rest of a line is a comment. Deals with CR, LF and CR/LF
   422  		if c == '\n' {
   423  			break
   424  		} else if c == '\r' && data[*i] == '\n' {
   425  			*i++
   426  			break
   427  		}
   428  	}
   429  }
   430  
   431  //nolint:funlen
   432  func parseLocal(data []byte, i *int, locals map[string]string) error {
   433  	keys := []string{}
   434  	values := []string{}
   435  
   436  keys:
   437  	for {
   438  		parsing.SkipWhitespace(data, i)
   439  
   440  		key, err := parseRockspecLiteral(data, i, locals)
   441  		if err != nil {
   442  			return err
   443  		}
   444  
   445  		if key == "function" {
   446  			err := skipFunction(data, i)
   447  			if err != nil {
   448  				return err
   449  			}
   450  			return nil
   451  		}
   452  
   453  		keys = append(keys, key)
   454  
   455  		parsing.SkipWhitespace(data, i)
   456  
   457  		c := data[*i]
   458  
   459  		switch c {
   460  		case ',':
   461  			*i++
   462  			continue
   463  		case '=':
   464  			*i++
   465  			break keys
   466  		default:
   467  			return fmt.Errorf("unexpected character: %s", string(c))
   468  		}
   469  	}
   470  
   471  values:
   472  	for {
   473  		skipWhitespaceNoNewLine(data, i)
   474  
   475  		c := data[*i]
   476  
   477  		switch c {
   478  		case '"', '\'':
   479  			value, err := parseRockspecString(data, i, locals)
   480  
   481  			if err != nil {
   482  				return err
   483  			}
   484  			values = append(values, value.value.(string))
   485  		default:
   486  			ref, err := parseRockspecLiteral(data, i, locals)
   487  			if err != nil {
   488  				return err
   489  			}
   490  
   491  			// Skip if it's an expression
   492  			skipWhitespaceNoNewLine(data, i)
   493  			c := data[*i]
   494  
   495  			var value string
   496  
   497  			if c != '\n' && c != '\r' {
   498  				skipExpression(data, i)
   499  				value = ""
   500  			} else {
   501  				value = locals[ref]
   502  			}
   503  
   504  			values = append(values, value)
   505  		}
   506  
   507  		skipWhitespaceNoNewLine(data, i)
   508  
   509  		c = data[*i]
   510  
   511  		switch c {
   512  		case ',':
   513  			*i++
   514  			continue
   515  		case '\n', '\r':
   516  			parsing.SkipWhitespace(data, i)
   517  			break values
   518  		}
   519  	}
   520  
   521  	if len(keys) != len(values) {
   522  		return fmt.Errorf("expected %d values got %d", len(keys), len(values))
   523  	}
   524  
   525  	for i := 0; i < len(keys); i++ {
   526  		locals[keys[i]] = values[i]
   527  	}
   528  
   529  	return nil
   530  }
   531  
   532  func skipBuildNode(data []byte, i *int) {
   533  	bracesCount := 0
   534  
   535  	for *i < len(data) {
   536  		c := data[*i]
   537  
   538  		switch c {
   539  		case '{':
   540  			bracesCount++
   541  		case '}':
   542  			bracesCount--
   543  		}
   544  
   545  		if bracesCount == 0 {
   546  			return
   547  		}
   548  
   549  		*i++
   550  	}
   551  }
   552  
   553  func skipFunction(data []byte, i *int) error {
   554  	blocks := 1
   555  
   556  	for *i < len(data)-5 {
   557  		if parsing.IsWhitespace(data[*i]) {
   558  			switch {
   559  			case string(data[*i+1:*i+3]) == "if" && parsing.IsWhitespace(data[*i+3]):
   560  				blocks++
   561  				*i += 3
   562  			case string(data[*i+1:*i+4]) == "end" && parsing.IsWhitespace(data[*i+4]):
   563  				blocks--
   564  				*i += 4
   565  
   566  				if blocks == 0 {
   567  					return nil
   568  				}
   569  			default:
   570  				*i++
   571  			}
   572  		} else {
   573  			*i++
   574  		}
   575  	}
   576  
   577  	return fmt.Errorf("unterminated function at %d", *i)
   578  }
   579  
   580  func skipExpression(data []byte, i *int) {
   581  	parseComment(data, i)
   582  }
   583  
   584  func skipWhitespaceNoNewLine(data []byte, i *int) {
   585  	for *i < len(data) && (data[*i] == ' ' || data[*i] == '\t') {
   586  		*i++
   587  	}
   588  }
   589  
   590  func isLiteral(c byte) bool {
   591  	if c == '[' || c == ']' {
   592  		return true
   593  	}
   594  	if c == '.' {
   595  		return false
   596  	}
   597  	return parsing.IsLiteral(c)
   598  }