diff --git a/README.md b/README.md index af758d9..988a537 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,17 @@ A Minetest mod library to make modifying formspecs easier. - `formspec_ast.apply_offset(tree, x, y)`: Shifts all elements in `tree`. Similar to `container`. - `formspec_ast.flatten(tree)`: Removes all containers and offsets elements - that were in containers accordingly. **The use of this function is - discouraged as `scroll_container[]` elements are not flattened.** + that were in containers accordingly. Note that `scroll_container[]` + elements are not flattened. - `formspec_ast.show_formspec(player_or_name, formname, formspec)`: Similar to `minetest.show_formspec`, however also accepts player objects and will pass `formspec` through `formspec_ast.interpret` first. - `formspec_ast.safe_parse(string_or_tree)`: Similar to `formspec_ast.parse`, however will delete any elements that may crash the client (or any I - haven't added to the safe element list). + haven't added to the safe element list). The safe element list that this + function uses is very limited, it may break complex formspecs. + - `formspec_ast.safe_interpret(string_or_tree)`: Equivalent to + `formspec_ast.unparse(formspec_ast.safe_parse(string_or_tree))`. - `formspec_ast.formspec_escape(text)`: The same as `minetest.formspec_escape`, should only be used when formspec_ast is being embedded outside of Minetest. diff --git a/core.lua b/core.lua index 6c544a6..cf22504 100644 --- a/core.lua +++ b/core.lua @@ -1,12 +1,12 @@ -- --- formspec_ast: An abstract system tree for formspecs. +-- formspec_ast: An abstract syntax tree for formspecs. -- -- This does not actually depend on Minetest and could probably run in -- standalone Lua. -- -- The MIT License (MIT) -- --- Copyright © 2019 by luk3yx. +-- Copyright © 2019-2022 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 @@ -541,8 +541,8 @@ end -- Allow other mods to access raw_parse and raw_unparse. Note that these may -- change or be removed at any time. -formspec_ast._raw_parse = raw_parse -formspec_ast._raw_unparse = raw_unparse +-- formspec_ast._raw_parse = raw_parse +-- formspec_ast._raw_unparse = raw_unparse -- Register custom elements parse_mt = {} diff --git a/helpers.lua b/helpers.lua index 5e0a601..df071fd 100644 --- a/helpers.lua +++ b/helpers.lua @@ -1,12 +1,12 @@ -- --- formspec_ast: An abstract system tree for formspecs. +-- formspec_ast: An abstract syntax tree for formspecs. -- -- This does not actually depend on Minetest and could probably run in -- standalone Lua. -- -- The MIT License (MIT) -- --- Copyright © 2019 by luk3yx. +-- Copyright © 2019-2022 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 @@ -29,6 +29,9 @@ local formspec_ast, minetest = formspec_ast, formspec_ast.minetest +-- Expose minetest.formspec_escape for use outside of Minetest +formspec_ast.formspec_escape = minetest.formspec_escape + -- Parses and unparses plain formspecs and just unparses AST trees. function formspec_ast.interpret(spec, custom_handlers) local ast = spec diff --git a/init.lua b/init.lua index 11f1b7d..c7df241 100644 --- a/init.lua +++ b/init.lua @@ -1,12 +1,12 @@ -- --- formspec_ast: An abstract system tree for formspecs. +-- formspec_ast: An abstract syntax tree for formspecs. -- -- This does not actually depend on Minetest and could probably run in -- standalone Lua. -- -- The MIT License (MIT) -- --- Copyright © 2019 by luk3yx. +-- Copyright © 2019-2022 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 @@ -37,7 +37,6 @@ if minetest then modpath = minetest.get_modpath('formspec_ast') assert(minetest.get_current_modname() == 'formspec_ast', 'This mod must be called formspec_ast!') - formspec_ast.formspec_escape = minetest.formspec_escape else -- Probably running outside Minetest. modpath = rawget(_G, 'FORMSPEC_AST_PATH') or '.' @@ -69,13 +68,21 @@ else return res end formspec_ast.minetest = minetest - formspec_ast.formspec_escape = minetest.formspec_escape end formspec_ast.modpath = modpath dofile(modpath .. '/core.lua') dofile(modpath .. '/helpers.lua') -dofile(modpath .. '/safety.lua') + +-- Lazy load safety.lua because I don't think anything actually uses it +function formspec_ast.safe_parse(...) + dofile(modpath .. '/safety.lua') + return formspec_ast.safe_parse(...) +end + +function formspec_ast.safe_interpret(tree) + return formspec_ast.unparse(formspec_ast.safe_parse(tree)) +end formspec_ast.modpath, formspec_ast.minetest = nil, nil diff --git a/safety.lua b/safety.lua index 98e423b..0097de0 100644 --- a/safety.lua +++ b/safety.lua @@ -1,12 +1,12 @@ -- --- formspec_ast: An abstract system tree for formspecs. +-- formspec_ast: An abstract syntax 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. +-- Copyright © 2019-2022 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 @@ -27,8 +27,8 @@ -- 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. +-- Similar to ast.walk(), however returns nil 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 = {} @@ -36,13 +36,9 @@ local function safe_walk(tree) 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 + if good and type(msg) == 'table' and not seen[msg] then + seen[msg] = true return msg - else - return {} end end end @@ -52,7 +48,7 @@ local function safe_flatten(tree) local res = {formspec_version = 1} if type(tree.formspec_version) == 'number' and tree.formspec_version > 1 then - res.formspec_version = 2 + res.formspec_version = math.min(math.floor(tree.formspec_version), 6) end for elem in safe_walk(table.copy(tree)) do if elem.type == 'container' then @@ -76,16 +72,40 @@ function ensure.string(obj) end function ensure.number(obj, max, min) + if obj == nil then return end + 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.boolean(bool) + assert(type(bool) == "boolean" or bool == nil) + return bool +end + function ensure.integer(obj) return math.floor(ensure.number(obj)) end +function ensure.texture(obj) + return ensure.string(obj):match("^[^%[]+") +end + +function ensure.list(items) + assert(type(items) == 'table') + for k, v in pairs(items) do + assert(type(k) == 'number' and type(v) == 'string') + end + return items +end + +function ensure.inventory_location(location) + assert(location == 'current_node' or location == 'current_player') + return location +end + local validate local function validate_elem(obj) local template = validate[obj.type] @@ -95,15 +115,11 @@ local function validate_elem(obj) 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_ + local value_type = template[k] + if value_type and value_type:sub(-1) == '?' then + value_type = value_type:sub(1, -2) end + func = ensure[value_type] end if func then @@ -114,7 +130,7 @@ local function validate_elem(obj) end for k, v in pairs(template) do - if type(v) ~= 'string' or v:sub(#v) ~= '?' then + if v:sub(-1) ~= '?' then assert(obj[k] ~= nil, k .. ' does not exist!') end end @@ -124,16 +140,16 @@ 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'}, + texture_name = 'texture'}, 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', + name = 'string', label = 'string', texture_name = 'texture', noclip = 'string', drawborder = 'string', - pressed_texture_name = 'string'}, + pressed_texture_name = 'texture'}, item_image_button = {x = 'number', y = 'number', w = 'number', h = 'number', name = 'string', label = 'string', - texture_name = 'string'}, + texture_name = 'texture'}, field = {x = 'number', y = 'number', w = 'number', h = 'number', name = 'string', label = 'string', default = 'string'}, pwdfield = {x = 'number', y = 'number', w = 'number', h = 'number', @@ -142,14 +158,13 @@ validate = { 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', + x = 'number', y = 'number', w = 'number', h = 'number?', + name = 'string', items = 'list', selected_idx = 'integer', + }, + textlist = { + x = 'number', y = 'number', w = 'number', h = 'number?', + name = 'string', listelems = 'list', selected_idx = 'integer?', + transparent = 'boolean?', }, checkbox = {x = 'number', y = 'number', name = 'string', label = 'string', selected = 'string'}, @@ -157,12 +172,9 @@ validate = { 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?', + inventory_location = 'inventory_location', list_name = 'string', + x = 'number', y = 'number', w = 'number', h = 'number', + starting_item_index = 'number?', }, listring = {}, } @@ -196,7 +208,3 @@ function formspec_ast.safe_parse(tree, custom_handlers) return res end - -function formspec_ast.safe_interpret(tree) - return formspec_ast.unparse(formspec_ast.safe_parse(tree)) -end diff --git a/tests.lua b/tests.lua index 236bca0..356bbc1 100644 --- a/tests.lua +++ b/tests.lua @@ -562,4 +562,20 @@ assert_equal(assert(formspec_ast.interpret('label[1,2;abc\\')), assert_equal(assert(formspec_ast.interpret('label[1,2;abc\\\\')), 'label[1,2;abc\\\\]') +assert_equal(formspec_ast.formspec_escape('label[1,2;abc\\def]'), + 'label\\[1\\,2\\;abc\\\\def\\]') + +assert_equal( + formspec_ast.safe_interpret([[ + formspec_version[5.1] + size[3,3] + label[0,0;Hi] + image[1,2;3,4;a^b\[c] + formspec_ast:crash[] + textlist[1,2;3,4;test;a,b,c] + ]]), + 'formspec_version[5]size[3,3]label[0,0;Hi]image[1,2;3,4;a^b]' .. + 'textlist[1,2;3,4;test;a,b,c]' +) + print('Tests pass')