Repository: tv-labs/lua
Pull request #2 (2024-03-05 03:05:16)

Add CI


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..cc6993b
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,42 @@
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+name: Test
+
+on:
+  push:
+    branches: [ "main" ]
+  pull_request:
+
+permissions:
+  contents: read
+
+jobs:
+  build:
+
+    name: Build and test
+    runs-on: ubuntu-latest
+
+    steps:
+    - name: Checkout
+      uses: actions/checkout@v3
+
+    - name: Set up Elixir
+      uses: erlef/setup-beam@v1
+      with:
+        elixir-version: '1.16.1'
+        otp-version: '26.0'
+    - name: Restore dependencies cache
+      uses: actions/cache@v3
+      with:
+        path: deps
+        key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
+        restore-keys: ${{ runner.os }}-mix-
+    - name: Install dependencies
+      run: mix deps.get
+    - name: Run tests
+      run: mix test
+    - name: Format
+      run: mix format --check-formatted
diff --git a/lib/lua.ex b/lib/lua.ex
index ec1aaac..f90744a 100644
--- a/lib/lua.ex
+++ b/lib/lua.ex
@@ -9,54 +9,67 @@ defmodule Lua do
 
   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<functions:", "[", Enum.join(Util.user_functions(lua), ", "), "]>"])
+      concat(["#Lua<functions: ", "[", Enum.join(Util.user_functions(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
-    %__MODULE__{state: :luerl.new()}
+  def new(opts \\ []) do
+    sandboxed = Keyword.get(opts, :sandboxed, @default_sandbox)
+
+    Enum.reduce(sandboxed, %__MODULE__{state: :luerl.init()}, &sandbox(&2, &1))
   end
 
-  def sandbox(_opts \\ []) do
-    # TODO create an options API for this
-    sandboxed = [
-      [:_G, :io],
-      [:_G, :file],
-      [:_G, :os, :execute],
-      [:_G, :os, :exit],
-      [:_G, :os, :getenv],
-      [:_G, :os, :remove],
-      [:_G, :os, :rename],
-      [:_G, :os, :tmpname],
-      #       [:_G, :package],
-      [:_G, :load],
-      [:_G, :loadfile],
-      #       [:_G, :require],
-      [:_G, :dofile],
-      [:_G, :load],
-      [:_G, :loadfile],
-      [:_G, :loadstring]
-    ]
-
-    # TODO let's implement sandbox ourselves
-    %__MODULE__{state: :luerl_sandbox.new(sandboxed)}
+  @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__{state: state} = lua, paths) when is_binary(paths) do
-    %{lua | state: state}
+  def set_lua_paths(%__MODULE__{} = lua, paths) when is_binary(paths) do
     set!(lua, ["package", "path"], paths)
   end
 
@@ -84,8 +97,14 @@ defmodule Lua do
       |> ensure_keys(keys)
       |> set_keys(keys, value)
 
+    global? =
+      case keys do
+        [:_G | _] -> true
+        _ -> false
+      end
+
     functions =
-      if is_function(value) do
+      if is_function(value) and not global? do
         info = Function.info(value)
         Map.put(lua.functions, keys, info[:arity])
       else
@@ -167,7 +186,9 @@ defmodule Lua do
 
         {:ok, _result, state} ->
           state
-          # TODO error?
+
+        {:lua_error, _error, _state} = error ->
+          raise Lua.RuntimeException, error
       end
 
     case rest do
diff --git a/lib/lua/runtime_exception.ex b/lib/lua/runtime_exception.ex
index 029566d..bb40251 100644
--- a/lib/lua/runtime_exception.ex
+++ b/lib/lua/runtime_exception.ex
@@ -22,7 +22,7 @@ defmodule Lua.RuntimeException do
         {:undefined_method, nil, name} ->
           "undefined method #{inspect(name)}"
 
-        {:illegal_index, nil, name} ->
+        {:illegal_index, _, name} ->
           "invalid index #{inspect(name)}"
       end
 
diff --git a/lib/lua/util.ex b/lib/lua/util.ex
index 2ffe65c..448959e 100644
--- a/lib/lua/util.ex
+++ b/lib/lua/util.ex
@@ -40,7 +40,10 @@ defmodule Lua.Util do
   @doc """
   Pretty prints a stack trace
   """
-  def format_stacktrace([_ | rest] = stacktrace, state, opts \\ []) do
+  def format_stacktrace(_stack, _state, _opts \\ [])
+  def format_stacktrace([], _state, _opts), do: ""
+
+  def format_stacktrace([_ | rest] = stacktrace, state, opts) do
     script_name = Keyword.get(opts, :script_name, "script")
 
     stacktrace
@@ -87,6 +90,11 @@ defmodule Lua.Util do
       iex> format_function(["foo"], [1, 2, 3])
       "foo(1, 2, 3)"
 
+  Internal Lua functions are formatted nicely
+
+      iex> format_function([:_G, :os, :exit], 1)
+      "os.exit(_)"
+
   References to tables are displayed as <ref>
 
       iex> format_function([tref: 1234], [1, 2, 3])
@@ -97,6 +105,8 @@ defmodule Lua.Util do
       iex> format_function(["namespace", "function"], 1)
       "namespace.function(_)"
   """
+  def format_function([:_G | list], args), do: format_function(list, args)
+
   def format_function(list, args) do
     list = List.wrap(list)
 


Test patch:
diff --git a/test/lua_test.exs b/test/lua_test.exs
index bc46203..2d6d001 100644
--- a/test/lua_test.exs
+++ b/test/lua_test.exs
@@ -7,45 +7,6 @@ defmodule LuaTest do
 
   require Lua.Util
 
-  alias Lua.Util
-
-  describe "messing with Luerl functions" do
-    test "it can access useful data" do
-      lua = Lua.new()
-
-      lua =
-        Lua.set!(lua, [:foo], fn [value] ->
-          [1 + value]
-        end)
-
-      global_table_data =
-        lua.state
-        |> Util.luerl(:g)
-        |> :luerl_heap.get_table(lua.state)
-        |> Util.table(:d)
-
-      user_functions =
-        :ttdict.filter(
-          fn
-            _key, {:erl_func, _} -> true
-            _, _ -> false
-          end,
-          global_table_data
-        )
-        |> :ttdict.fetch_keys()
-
-      assert user_functions == ["foo"]
-
-      assert {:erl_func, func} = :ttdict.fetch("foo", global_table_data)
-
-      assert is_function(func)
-
-      # These are the datastructures Luerl uses for managing tables
-      # :ttdict.from_list([{:a, 1}, {:b, 2}, {:c, 3}, {:d, 4}, {:e, 5}, {:f, 6}, {:g, 7}])
-      # :ttsets.from_list([{:a, 1}, {:b, 2}, {:c, 3}, {:d, 4}, {:e, 5}, {:f, 6}, {:g, 7}])
-    end
-  end
-
   describe "inspect" do
     test "shows all functions in the lua state" do
       lua = Lua.new()
@@ -57,7 +18,7 @@ defmodule LuaTest do
 
       lua = Lua.set!(lua, [:bar, :my_func], fn _, _ -> [] end)
 
-      assert inspect(lua) == "#Lua<functions:[bar.my_func(_, _), foo(_)]>"
+      assert inspect(lua) == "#Lua<functions: [bar.my_func(_, _), foo(_)]>"
     end
   end
 
@@ -123,11 +84,7 @@ defmodule LuaTest do
     test "sandboxed functions show a nice error message" do
       lua = Lua.new()
 
-      message = """
-      Lua runtime error: sandboxed function
-
-      script line 1: sandboxed(1)
-      """
+      message = "Lua runtime error: os.exit(_) is sandboxed"
 
       assert_raise Lua.RuntimeException, message, fn ->
         Lua.eval!(lua, """
@@ -444,7 +401,7 @@ defmodule LuaTest do
 
   describe "require" do
     test "it can find lua code when modifying package.path" do
-      lua = Lua.new()
+      lua = Lua.new(sandboxed: [])
 
       assert {["required file successfully"], _} =
                Lua.eval!(lua, """
@@ -455,7 +412,7 @@ defmodule LuaTest do
     end
 
     test "we can use set_lua_paths/2 to add the paths" do
-      lua = Lua.new()
+      lua = Lua.new(sandboxed: [])
 
       lua = Lua.set_lua_paths(lua, "./test/fixtures/?.lua")
 
@@ -464,5 +421,19 @@ defmodule LuaTest do
                return require("test_require")
                """)
     end
+
+    test "set_lua_paths/2 raises if package is sandboxed" do
+      lua = Lua.new()
+
+      message = """
+      Lua runtime error: invalid index "path"
+
+
+      """
+
+      assert_raise Lua.RuntimeException, message, fn ->
+        Lua.set_lua_paths(lua, "./test/fixtures/?.lua")
+      end
+    end
   end
 end