202 lines
6.6 KiB
Lua
202 lines
6.6 KiB
Lua
--
|
|
-- formspec_ast: An abstract system tree for formspecs.
|
|
--
|
|
-- This verifies that formspecs from untrusted sources are safe(-ish) to
|
|
-- display, provided they are passed through formspec_ast.interpret.
|
|
--
|
|
-- The MIT License (MIT)
|
|
--
|
|
-- Copyright © 2019 by luk3yx.
|
|
--
|
|
-- Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
-- of this software and associated documentation files (the "Software"), to
|
|
-- deal in the Software without restriction, including without limitation the
|
|
-- rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
-- sell copies of the Software, and to permit persons to whom the Software is
|
|
-- furnished to do so, subject to the following conditions:
|
|
--
|
|
-- The above copyright notice and this permission notice shall be included in
|
|
-- all copies or substantial portions of the Software.
|
|
--
|
|
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
-- IN THE SOFTWARE.
|
|
--
|
|
|
|
-- Similar to ast.walk(), however returns {} and then exits if walk() would
|
|
-- crash. Use this for untrusted formspecs, otherwise use walk() for speed.
|
|
local function safe_walk(tree)
|
|
local walk = formspec_ast.walk(tree)
|
|
local seen = {}
|
|
return function()
|
|
if not walk or not seen then return end
|
|
|
|
local good, msg = pcall(walk)
|
|
if good and (type(msg) == 'table' or msg == nil) and not seen[msg] then
|
|
if msg then
|
|
seen[msg] = true
|
|
end
|
|
return msg
|
|
else
|
|
return {}
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Similar to ast.flatten(), however removes unsafe elements.
|
|
local function safe_flatten(tree)
|
|
local res = {formspec_version = 1}
|
|
if tree.formspec_version == 2 then
|
|
res.formspec_version = 2
|
|
end
|
|
for elem in safe_walk(table.copy(tree)) do
|
|
if elem.type == 'container' then
|
|
if type(elem.x) == 'number' and type(elem.y) == 'number' then
|
|
formspec_ast.apply_offset(elem, elem.x, elem.y)
|
|
end
|
|
elseif elem.type then
|
|
table.insert(res, elem)
|
|
end
|
|
end
|
|
return res
|
|
end
|
|
|
|
local ensure = {}
|
|
|
|
function ensure.string(obj)
|
|
if obj == nil then
|
|
return ''
|
|
end
|
|
return tostring(obj)
|
|
end
|
|
|
|
function ensure.number(obj, max, min)
|
|
local res = tonumber(obj)
|
|
assert(res ~= nil and res == res)
|
|
assert(res <= (max or 100) and res >= (min or 0))
|
|
return res
|
|
end
|
|
|
|
function ensure.integer(obj)
|
|
return math.floor(ensure.number(obj))
|
|
end
|
|
|
|
local validate
|
|
local function validate_elem(obj)
|
|
local template = validate[obj.type]
|
|
assert(type(template) == 'table')
|
|
for k, v in pairs(obj) do
|
|
local func
|
|
if k == 'type' then
|
|
func = ensure.string
|
|
else
|
|
local type_ = template[k]
|
|
if type(type_) == 'string' then
|
|
if type_:sub(#type_) == '?' then
|
|
type_ = type_:sub(1, #type_ - 1)
|
|
end
|
|
func = ensure[type_]
|
|
elseif type(type_) == 'function' then
|
|
func = type_
|
|
end
|
|
end
|
|
|
|
if func then
|
|
obj[k] = func(v)
|
|
else
|
|
obj[k] = nil
|
|
end
|
|
end
|
|
|
|
for k, v in pairs(template) do
|
|
if type(v) ~= 'string' or v:sub(#v) ~= '?' then
|
|
assert(obj[k] ~= nil, k .. ' does not exist!')
|
|
end
|
|
end
|
|
end
|
|
|
|
validate = {
|
|
size = {w = 'number', h = 'number'},
|
|
label = {x = 'number', y = 'number', label = 'string'},
|
|
image = {x = 'number', y = 'number', w = 'number', h = 'number',
|
|
texture_name = 'string'},
|
|
button = {x = 'number', y = 'number', w = 'number', h = 'number',
|
|
name = 'string', label = 'string'},
|
|
image_button = {x = 'number', y = 'number', w = 'number', h = 'number',
|
|
name = 'string', label = 'string', texture_name = 'string',
|
|
noclip = 'string', drawborder = 'string',
|
|
pressed_texture_name = 'string'},
|
|
item_image_button = {x = 'number', y = 'number', w = 'number',
|
|
h = 'number', name = 'string', label = 'string',
|
|
texture_name = 'string'},
|
|
field = {x = 'number', y = 'number', w = 'number', h = 'number',
|
|
name = 'string', label = 'string', default = 'string'},
|
|
pwdfield = {x = 'number', y = 'number', w = 'number', h = 'number',
|
|
name = 'string', label = 'string'},
|
|
field_close_on_enter = {name = 'string', close_on_enter = 'string'},
|
|
textarea = {x = 'number', y = 'number', w = 'number', h = 'number',
|
|
name = 'string', label = 'string', default = 'string'},
|
|
dropdown = {
|
|
x = 'number', y = 'number', w = 'number', name = 'string',
|
|
items = function(items)
|
|
assert(type(items) == 'list')
|
|
for k, v in pairs(items) do
|
|
assert(type(k) == 'number' and type(v) == 'string')
|
|
end
|
|
end,
|
|
selected_idx = 'integer',
|
|
},
|
|
checkbox = {x = 'number', y = 'number', name = 'string', label = 'string',
|
|
selected = 'string'},
|
|
box = {x = 'number', y = 'number', w = 'number', h = 'number',
|
|
color = 'string'},
|
|
|
|
list = {
|
|
inventory_location = function(location)
|
|
assert(location == 'current_node' or location == 'current_player')
|
|
return location
|
|
end,
|
|
list_name = 'string', x = 'number', y = 'number', w = 'number',
|
|
h = 'number', starting_item_index = 'number?',
|
|
},
|
|
listring = {},
|
|
}
|
|
|
|
validate.vertlabel = validate.label
|
|
validate.button_exit = validate.button
|
|
validate.image_button_exit = validate.item_image_button
|
|
|
|
-- Ensure that an AST tree is safe to display. The resulting tree will be
|
|
-- flattened for simplicity.
|
|
function formspec_ast.safe_parse(tree, custom_handlers)
|
|
if type(tree) == 'string' then
|
|
tree = formspec_ast.parse(tree, custom_handlers)
|
|
end
|
|
|
|
if type(tree) ~= 'table' then
|
|
return {}
|
|
end
|
|
|
|
-- Flatten the tree and remove objects that can't possibly be elements.
|
|
tree = safe_flatten(tree)
|
|
|
|
-- Iterate over the tree and add valid elements to a new table.
|
|
local res = {formspec_version = tree.formspec_version}
|
|
for _, elem in ipairs(tree) do
|
|
local good, _ = pcall(validate_elem, elem)
|
|
if good then
|
|
res[#res + 1] = elem
|
|
end
|
|
end
|
|
|
|
return res
|
|
end
|
|
|
|
function formspec_ast.safe_interpret(tree)
|
|
return formspec_ast.unparse(formspec_ast.safe_parse(tree))
|
|
end
|