Lua: Add support for more advanced syntax (#2908)

* Add lua/rocksepc support for variables substitution
* Lua: Skip expressions in rockspec packages
* Lua: Add support for concatenation of string and variables
* Lua: Skip expressions in local
* Lua: Skip build sections in Rockspec files
* Lua: skip function blocks in Rockspec
* Lua: Add support for multi variable per line
---------
Signed-off-by: Laurent Goderre <laurent.goderre@docker.com>
This commit is contained in:
Laurent Goderre 2024-05-30 11:38:45 -04:00 committed by GitHub
parent 5cf8cc9bc3
commit eeb4193d4a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 422 additions and 23 deletions

View File

@ -76,6 +76,27 @@ func TestParseRockspec(t *testing.T) {
}, },
}, },
}, },
{
Fixture: "test-fixtures/rockspec/luasyslog-2.0.1-1.rockspec",
ExpectedPkg: pkg.Package{
Name: "luasyslog",
Version: "2.0.1-1",
PURL: "pkg:luarocks/luasyslog@2.0.1-1",
Type: pkg.LuaRocksPkg,
Language: pkg.Lua,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT/X11", file.NewLocation("test-fixtures/rockspec/luasyslog-2.0.1-1.rockspec")),
),
Metadata: pkg.LuaRocksPackage{
Name: "luasyslog",
Version: "2.0.1-1",
License: "MIT/X11",
Homepage: "https://github.com/lunarmodules/luasyslog",
Description: "Syslog logging for Lua",
URL: "git://github.com/lunarmodules/luasyslog.git",
},
},
},
} }
for _, test := range tests { for _, test := range tests {

View File

@ -45,7 +45,8 @@ func parseRockspecData(reader io.Reader) (rockspec, error) {
} }
i := 0 i := 0
blocks, err := parseRockspecBlock(data, &i) locals := make(map[string]string)
blocks, err := parseRockspecBlock(data, &i, locals)
if err != nil { if err != nil {
return noReturn, err return noReturn, err
@ -56,9 +57,9 @@ func parseRockspecData(reader io.Reader) (rockspec, error) {
}, nil }, nil
} }
func parseRockspecBlock(data []byte, i *int) ([]rockspecNode, error) { func parseRockspecBlock(data []byte, i *int, locals map[string]string) ([]rockspecNode, error) {
var out []rockspecNode var out []rockspecNode
var iterator func(data []byte, i *int) (*rockspecNode, error) var iterator func(data []byte, i *int, locals map[string]string) (*rockspecNode, error)
parsing.SkipWhitespace(data, i) parsing.SkipWhitespace(data, i)
@ -67,6 +68,14 @@ func parseRockspecBlock(data []byte, i *int) ([]rockspecNode, error) {
} }
c := data[*i] c := data[*i]
// Block starting with a comment
if c == '-' {
parseComment(data, i)
parsing.SkipWhitespace(data, i)
c = data[*i]
}
switch { switch {
case c == '"' || c == '\'': case c == '"' || c == '\'':
iterator = parseRockspecListItem iterator = parseRockspecListItem
@ -77,7 +86,7 @@ func parseRockspecBlock(data []byte, i *int) ([]rockspecNode, error) {
} }
for *i < len(data) { for *i < len(data) {
item, err := iterator(data, i) item, err := iterator(data, i, locals)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w\n%s", err, parsing.PrintError(data, *i)) return nil, fmt.Errorf("%w\n%s", err, parsing.PrintError(data, *i))
} }
@ -99,7 +108,7 @@ func parseRockspecBlock(data []byte, i *int) ([]rockspecNode, error) {
} }
//nolint:funlen, gocognit //nolint:funlen, gocognit
func parseRockspecNode(data []byte, i *int) (*rockspecNode, error) { func parseRockspecNode(data []byte, i *int, locals map[string]string) (*rockspecNode, error) {
parsing.SkipWhitespace(data, i) parsing.SkipWhitespace(data, i)
if *i >= len(data) { if *i >= len(data) {
@ -136,7 +145,7 @@ func parseRockspecNode(data []byte, i *int) (*rockspecNode, error) {
return nil, fmt.Errorf("invalid literal character: %s", string(c)) return nil, fmt.Errorf("invalid literal character: %s", string(c))
} }
key, err := parseRockspecLiteral(data, i) key, err := parseRockspecLiteral(data, i, locals)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -147,6 +156,16 @@ func parseRockspecNode(data []byte, i *int) (*rockspecNode, error) {
return nil, fmt.Errorf("unexpected end of node at %d", *i) 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] c = data[*i]
if c != '=' { if c != '=' {
return nil, fmt.Errorf("unexpected character: %s", string(c)) return nil, fmt.Errorf("unexpected character: %s", string(c))
@ -159,6 +178,14 @@ func parseRockspecNode(data []byte, i *int) (*rockspecNode, error) {
return nil, fmt.Errorf("unexpected end of node at %d", *i) return nil, fmt.Errorf("unexpected end of node at %d", *i)
} }
if key == "build" {
skipBuildNode(data, i)
return &rockspecNode{
key: ",",
}, nil
}
c = data[*i] c = data[*i]
switch c { switch c {
@ -180,7 +207,7 @@ func parseRockspecNode(data []byte, i *int) (*rockspecNode, error) {
parsing.SkipWhitespace(data, i) parsing.SkipWhitespace(data, i)
obj, err := parseRockspecBlock(data, i) obj, err := parseRockspecBlock(data, i, locals)
if err != nil { if err != nil {
return nil, err return nil, err
@ -190,16 +217,10 @@ func parseRockspecNode(data []byte, i *int) (*rockspecNode, error) {
return &rockspecNode{ return &rockspecNode{
key, value, key, value,
}, nil }, nil
case '"', '\'': case '(':
str, err := parseRockspecString(data, i) skipExpression(data, i)
if err != nil {
return nil, err
}
value := str.value
return &rockspecNode{ return &rockspecNode{
key, value, key: ",",
}, nil }, nil
case '[': case '[':
offset := *i + 1 offset := *i + 1
@ -214,7 +235,7 @@ func parseRockspecNode(data []byte, i *int) (*rockspecNode, error) {
*i++ *i++
str, err := parseRockspecString(data, i) str, err := parseRockspecString(data, i, locals)
if err != nil { if err != nil {
return nil, err return nil, err
@ -234,10 +255,18 @@ func parseRockspecNode(data []byte, i *int) (*rockspecNode, error) {
}, nil }, nil
} }
return nil, nil value, err := parseRockspecValue(data, i, locals, "")
if err != nil {
return nil, err
}
return &rockspecNode{
key, value,
}, nil
} }
func parseRockspecListItem(data []byte, i *int) (*rockspecNode, error) { func parseRockspecListItem(data []byte, i *int, locals map[string]string) (*rockspecNode, error) {
parsing.SkipWhitespace(data, i) parsing.SkipWhitespace(data, i)
if *i >= len(data) { if *i >= len(data) {
@ -269,14 +298,70 @@ func parseRockspecListItem(data []byte, i *int) (*rockspecNode, error) {
}, nil }, nil
} }
str, err := parseRockspecString(data, i) str, err := parseRockspecString(data, i, locals)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return str, nil return str, nil
} }
func parseRockspecLiteral(data []byte, i *int) (string, error) { 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 var buf bytes.Buffer
out: out:
for *i < len(data) { for *i < len(data) {
@ -284,7 +369,7 @@ out:
switch { switch {
case c == '[': case c == '[':
*i++ *i++
nested, err := parseRockspecString(data, i) nested, err := parseRockspecString(data, i, locals)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -303,7 +388,7 @@ out:
return buf.String(), nil return buf.String(), nil
} }
func parseRockspecString(data []byte, i *int) (*rockspecNode, error) { func parseRockspecString(data []byte, i *int, _ map[string]string) (*rockspecNode, error) {
delim := data[*i] delim := data[*i]
var endDelim byte var endDelim byte
switch delim { switch delim {
@ -344,9 +429,171 @@ func parseComment(data []byte, i *int) {
} }
} }
//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 { func isLiteral(c byte) bool {
if c == '[' || c == ']' { if c == '[' || c == ']' {
return true return true
} }
if c == '.' {
return false
}
return parsing.IsLiteral(c) return parsing.IsLiteral(c)
} }

View File

@ -60,6 +60,49 @@ multiline = [[
a multiline a multiline
string string
]] ]]
`,
},
{
name: "variables",
content: `
local foo = "bar"
local baz = foo
hello = baz
`,
},
{
name: "multiple variables in one line",
content: `
local foo, bar = "hello", "world"
baz = foo
test = bar
`,
},
{
name: "skip expressions",
content: `
test = (hello == "world") and "foo" or "bar"
baz = "123"
`,
},
{
name: "skip expressions in locals",
content: `
local var1 = "foo"
local var2 = var1 == "foo" and "true" or ("false")
foo = "bar"
`,
},
{
name: "concatenation",
content: `
local foo = "bar"
local baz = "123"
hello = "world"..baz
baz = foo.." "..baz
test = foo .. baz
`, `,
}, },
{ {
@ -80,6 +123,17 @@ object = {
hello = "world" hello = "world"
-- this is another comment -- this is another comment
} }
`,
},
{
name: "content start with comment",
content: `
foo = "bar"
-- this is a comment
object = {
-- this is another comment
hello = "world"
}
`, `,
}, },
{ {
@ -91,6 +145,33 @@ list = {
-- "baz" -- "baz"
"hello" "hello"
} }
`,
},
{
name: "skip build section",
content: `
foo = "bar"
build = {
a = {
{
content
}
}
}
bar = "baz"
`,
},
{
name: "skip functions",
content: `
local function test
if foo == bar then
if hello = world then
blah
end
end
end
test = "blah"
`, `,
}, },
{ {
@ -153,6 +234,20 @@ list = {
"bar", "bar",
-`, -`,
}, },
{
name: "undefined local",
wantErr: require.Error,
content: `
test = hello
`,
},
{
name: "unterminated concatenation",
wantErr: require.Error,
content: `
local foo = "123"
hello = foo.. `,
},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {

View File

@ -0,0 +1,36 @@
local package_name = "luasyslog"
local package_version = "2.0.1"
local rockspec_revision = "1"
local github_account_name = "lunarmodules"
local github_repo_name = package_name
package = package_name
version = package_version.."-"..rockspec_revision
source = {
url = "git://github.com/"..github_account_name.."/"..github_repo_name..".git",
branch = (package_version == "dev") and "main" or nil,
tag = (package_version ~= "dev") and package_version or nil,
}
description = {
summary = "Syslog logging for Lua",
detailed = [[
Addon for LuaLogging to log to the system log on unix systems.
Can also be used without LuaLogging to directly write to syslog.
]],
license = "MIT/X11",
homepage = "https://github.com/"..github_account_name.."/"..github_repo_name,
}
dependencies = {
"lua >= 5.1",
"lualogging >= 1.4.0, < 2.0.0",
}
build = {
type = "builtin",
modules = {
lsyslog = {
sources = "lsyslog.c",
},
["logging.syslog"] = "syslog.lua",
}
}