Files
falco/userspace/engine/lua/modules/compiler.lua
2022-02-24 18:24:19 +01:00

236 lines
6.4 KiB
Lua

-- Copyright (C) 2019 The Falco Authors.
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
local parser = require("parser")
local compiler = {}
compiler.trim = parser.trim
function map(f, arr)
local res = {}
for i,v in ipairs(arr) do
res[i] = f(v)
end
return res
end
function foldr(f, acc, arr)
for i,v in pairs(arr) do
acc = f(acc, v)
end
return acc
end
--[[
Given a map of macro definitions, traverse AST and replace macro references
with their definitions.
The AST is changed in-place.
The return value is a boolean which is true if any macro was
substitued. This allows a caller to re-traverse until no more macros are
found, a simple strategy for recursive resoltuions (e.g. when a macro
definition uses another macro).
--]]
function copy_ast_obj(obj)
if type(obj) ~= 'table' then return obj end
local res = {}
for k, v in pairs(obj) do res[copy_ast_obj(k)] = copy_ast_obj(v) end
return res
end
function expand_macros(ast, defs, changed)
if (ast.type == "Rule") then
return expand_macros(ast.filter, defs, changed)
elseif ast.type == "Filter" then
if (ast.value.type == "Macro") then
if (defs[ast.value.value] == nil) then
return false, "Undefined macro '".. ast.value.value .. "' used in filter."
end
defs[ast.value.value].used = true
ast.value = copy_ast_obj(defs[ast.value.value].ast)
changed = true
return true, changed
end
return expand_macros(ast.value, defs, changed)
elseif ast.type == "BinaryBoolOp" then
if (ast.left.type == "Macro") then
if (defs[ast.left.value] == nil) then
return false, "Undefined macro '".. ast.left.value .. "' used in filter."
end
defs[ast.left.value].used = true
ast.left = copy_ast_obj(defs[ast.left.value].ast)
changed = true
end
if (ast.right.type == "Macro") then
if (defs[ast.right.value] == nil) then
return false, "Undefined macro ".. ast.right.value .. " used in filter."
end
defs[ast.right.value].used = true
ast.right = copy_ast_obj(defs[ast.right.value].ast)
changed = true
end
local status, changed_left = expand_macros(ast.left, defs, false)
if status == false then
return false, changed_left
end
local status, changed_right = expand_macros(ast.right, defs, false)
if status == false then
return false, changed_right
end
return true, changed or changed_left or changed_right
elseif ast.type == "UnaryBoolOp" then
if (ast.argument.type == "Macro") then
if (defs[ast.argument.value] == nil) then
return false, "Undefined macro ".. ast.argument.value .. " used in filter."
end
defs[ast.argument.value].used = true
ast.argument = copy_ast_obj(defs[ast.argument.value].ast)
changed = true
end
return expand_macros(ast.argument, defs, changed)
end
return true, changed
end
function get_filters(ast)
local filters = {}
function cb(node)
if node.type == "FieldName" then
filters[node.value] = 1
end
end
parser.traverse_ast(ast.filter.value, {FieldName=1} , cb)
return filters
end
function compiler.expand_lists_in(source, list_defs)
for name, def in pairs(list_defs) do
local bpos = string.find(source, name, 1, true)
while bpos ~= nil do
def.used = true
local epos = bpos + string.len(name)
-- The characters surrounding the name must be delimiters of beginning/end of string
if (bpos == 1 or string.match(string.sub(source, bpos-1, bpos-1), "[%s(),=]")) and (epos > string.len(source) or string.match(string.sub(source, epos, epos), "[%s(),=]")) then
new_source = ""
if bpos > 1 then
new_source = new_source..string.sub(source, 1, bpos-1)
end
sub = table.concat(def.items, ", ")
new_source = new_source..sub
if epos <= string.len(source) then
new_source = new_source..string.sub(source, epos, string.len(source))
end
source = new_source
bpos = bpos + (string.len(sub)-string.len(name))
end
bpos = string.find(source, name, bpos+1, true)
end
end
return source
end
function compiler.compile_macro(line, macro_defs, list_defs)
line = compiler.expand_lists_in(line, list_defs)
local ast, error_msg = parser.parse_filter(line)
if (error_msg) then
msg = "Compilation error when compiling \""..line.."\": ".. error_msg
return false, msg
end
-- Simply as a validation step, try to expand all macros in this
-- macro's condition. This changes the ast, so we make a copy
-- first.
local ast_copy = copy_ast_obj(ast)
if (ast.type == "Rule") then
-- Line is a filter, so expand macro references
repeat
status, expanded = expand_macros(ast_copy, macro_defs, false)
if status == false then
msg = "Compilation error when compiling \""..line.."\": ".. expanded
return false, msg
end
until expanded == false
else
return false, "Unexpected top-level AST type: "..ast.type
end
return true, ast
end
--[[
Parses a single filter, then expands macros using passed-in table of definitions. Returns resulting AST.
--]]
function compiler.compile_filter(name, source, macro_defs, list_defs)
source = compiler.expand_lists_in(source, list_defs)
local ast, error_msg = parser.parse_filter(source)
if (error_msg) then
msg = "Compilation error when compiling \""..source.."\": "..error_msg
return false, msg
end
if (ast.type == "Rule") then
-- Line is a filter, so expand macro references
repeat
status, expanded = expand_macros(ast, macro_defs, false)
if status == false then
return false, expanded
end
until expanded == false
else
return false, "Unexpected top-level AST type: "..ast.type
end
filters = get_filters(ast)
return true, ast, filters
end
return compiler