defmodule Lua do @external_resource "README.md" @moduledoc @external_resource |> File.read!() |> String.split("") |> Enum.fetch!(1) defstruct [:state, functions: %{}] alias Luerl.New, as: Luerl alias Lua.Util @default_sandbox [ [:io], [:file], [:os, :execute], [:os, :exit], [:os, :getenv], [:os, :remove], [:os, :rename], [:os, :tmpname], [:package], [:load], [:loadfile], [:require], [:dofile], [:load], [:loadfile], [:loadstring] ] defimpl Inspect do alias Lua.Util import Inspect.Algebra def inspect(lua, _opts) do concat(["#Lua"]) end end @doc """ Initializes a Lua VM sandbox. All library functions are stubbed out, so no access to the filesystem or the execution environment is exposed. ## Options * `:sandboxed` - list of paths to be sandboxed, e.g. `sandboxed: [[:require], [:os, :exit]]` """ def new(opts \\ []) do sandboxed = Keyword.get(opts, :sandboxed, @default_sandbox) Enum.reduce(sandboxed, %__MODULE__{state: :luerl.init()}, &sandbox(&2, &1)) end @doc """ Sandboxes the given path, swapping out the implementation with a function that raises when called iex> lua = Lua.new(sandboxed: []) iex> Lua.sandbox(lua, [:os, :exit]) """ def sandbox(lua, path) do set!(lua, [:_G | path], fn args -> raise Lua.RuntimeException, "#{Lua.Util.format_function(path, Enum.count(args))} is sandboxed" end) end @doc """ Sets the path patterns that Lua will look in when requiring Lua scripts. """ def set_lua_paths(%__MODULE__{} = lua, paths) when is_binary(paths) do set!(lua, ["package", "path"], paths) end @doc """ Sets a table value in Lua. Nested keys will create intermediate tables iex> Lua.set!(Lua.new(), [:hello], "World") It can also set nested values iex> Lua.set!(Lua.new(), [:a, :b, :c], []) These table values are availble in lua scripts iex> lua = Lua.set!(Lua.new(), [:a, :b, :c], "nested!") iex> {result, _} = Lua.eval!(lua, "return a.b.c") iex> result ["nested!"] """ def set!(%__MODULE__{state: state} = lua, keys, value) do new_state = state |> ensure_keys(keys) |> set_keys(keys, value) global? = case keys do [:_G | _] -> true _ -> false end functions = if is_function(value) and not global? do info = Function.info(value) Map.put(lua.functions, keys, info[:arity]) else lua.functions end %__MODULE__{state: new_state, functions: functions} end @doc """ Gets a table value in Lua iex> state = Lua.set!(Lua.new(), [:hello], "world") iex> Lua.get!(state, [:hello]) "world" When a value doesn't exist, it returns nil iex> Lua.get!(Lua.new(), [:nope]) nil It can also get nested values iex> state = Lua.set!(Lua.new(), [:a, :b, :c], "nested") iex> Lua.get!(state, [:a, :b, :c]) "nested" """ def get!(%__MODULE__{state: state}, keys), do: get!(state, keys) def get!(state, keys) when is_tuple(state) do {:ok, value, _state} = Luerl.get_table_keys_dec(state, keys) value end @doc """ Evalutes the script or chunk, returning the result and discarding side effects in the state """ def eval!(state \\ new(), script) def eval!(%__MODULE__{state: state} = lua, script) when is_binary(script) do case Luerl.do_dec(state, script) do {:ok, result, new_state} -> {result, %__MODULE__{lua | state: new_state}} {:lua_error, _e, _state} = error -> raise Lua.RuntimeException, error {:error, [error | _], _} -> raise Lua.CompilerException, error end rescue e in [UndefinedFunctionError] -> reraise Lua.RuntimeException, Util.format_function([e.module, e.function], e.arity), __STACKTRACE__ e in [Lua.RuntimeException, Lua.CompilerException] -> reraise e, __STACKTRACE__ e -> reraise Lua.RuntimeException, e, __STACKTRACE__ end # Deep-set a value within a nested Lua table structure, # ensuring the entire path of keys exists. defp ensure_keys(state, [first | rest]) do ensure_keys(state, [first], rest) end defp ensure_keys(state, keys, rest) do state = case Luerl.get_table_keys_dec(state, keys) do {:ok, nil, state} -> set_keys(state, keys) {:ok, _result, state} -> state {:lua_error, _error, _state} = error -> raise Lua.RuntimeException, error end case rest do [] -> state [next | rest] -> ensure_keys(state, keys ++ [next], rest) end end defp set_keys(state, keys, value \\ []) do case Luerl.set_table_keys_dec(state, keys, value) do {:ok, [], state} -> state {:lua_error, error, _state} -> raise Lua.RuntimeException, reason: error end end def load_lua_file!(%__MODULE__{state: state} = lua, path) when is_binary(path) do case Luerl.dofile(state, String.to_charlist(path), [:return]) do {:ok, [], state} -> %__MODULE__{lua | state: state} {:lua_error, _error, _lua} = error -> raise Lua.CompilerException, error {:error, [{:none, :file, :enoent} | _], _} -> raise "Cannot load lua file, #{inspect(path <> ".lua")} does not exist" {:error, [error | _], _} -> # We just take the first error raise Lua.CompilerException, error end end @doc """ Inject functions written with the `deflua` macro into the Lua runtime """ def load_api(lua, module, scope \\ nil) do funcs = :functions |> module.__info__() |> Enum.group_by(&elem(&1, 0), &elem(&1, 1)) scope = scope || module.scope() module.__lua_functions__() |> Enum.reduce(lua, fn {name, with_state?, variadic?}, lua -> arities = Map.get(funcs, name) Lua.set!( lua, List.wrap(scope) ++ [name], if variadic? do wrap_variadic_function(module, name, with_state?) else wrap_function(module, name, arities, with_state?) end ) end) end defp wrap_variadic_function(module, function_name, with_state?) do # formatted = Util.format_function(module.scope() ++ [function_name], 0) if with_state? do fn args, state -> apply(module, function_name, [args] ++ [state]) end else fn args -> execute_function(module, function_name, [args]) end end end # credo:disable-for-lines:25 defp wrap_function(module, function_name, arities, with_state?) do # formatted = Util.format_function(module.scope() ++ [function_name], 0) if with_state? do fn args, state -> if (length(args) + 1) in arities do apply(module, function_name, args ++ [state]) else arities = Enum.map(arities, &(&1 - 1)) raise Lua.RuntimeException, function: function_name, scope: module.scope(), message: "expected #{Enum.join(arities, " or ")} arguments, got #{length(args)}" end end else fn args -> if length(args) in arities do execute_function(module, function_name, args) else raise Lua.RuntimeException, function: function_name, scope: module.scope(), message: "expected #{Enum.join(arities, " or ")} arguments, got #{length(args)}" end end end end defp execute_function(module, function_name, args) do # Luerl mandates lists as return values; this function ensures all results conform. List.wrap(apply(module, function_name, args)) catch thrown_value -> {:error, "Value thrown during function '#{function_name}' execution: #{inspect(thrown_value)}"} end end