Skip to content

Commit 75b4975

Browse files
authored
feat: validate function return values (#75)
1 parent 78d63e3 commit 75b4975

File tree

9 files changed

+306
-80
lines changed

9 files changed

+306
-80
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [v0.2.0] - 2025-05-14
9+
10+
### Changed
11+
- Any data returned from a `deflua` function, or a function set by `Lua.set!/3` is now validated. If the data is not an identity value, or an encoded value, it will raise an exception. In the past, `Lua` and Luerl would happily accept bad values, causing downstream problem is the program. This led to unexpected behavior, where depending on if the data passed was decoded or not, the program would succeed or fail
12+
813

914
## [v0.1.1] - 2025-05-13
1015

@@ -26,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2631
- Tables must now be explicitly decoded when receiving as arguments `deflua` and other Elixir callbacks
2732

2833

29-
[unreleased]: https://github.com/tv-labs/lua/compare/v0.1.1...HEAD
34+
[unreleased]: https://github.com/tv-labs/lua/compare/v0.2.0...HEAD
35+
[0.1.1]: https://github.com/tv-labs/lua/compare/v0.1.1...v0.2.0
3036
[0.1.1]: https://github.com/tv-labs/lua/compare/v0.1.0...v0.1.1
3137
[0.1.0]: https://github.com/tv-labs/lua/compare/v0.0.22...v0.1.0

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,46 @@ lua = Lua.new() |> Lua.put_private(:user, user) |> Lua.load_api(UserAPI)
160160

161161
This allows you to have simple, expressive APIs that access context that is unavailable to the Lua code.
162162

163+
## Encoding and Decoding data
164+
165+
When working with `Lua`, you may want inject data of various types into the runtime. Some values, such as integers, have the same representation inside of the runtime as they do in Elixir, they do not require encoding. Other values, such as maps, are represented inside of `Lua` as tables, and must be encoded first. Values not listed are not valid and cannot be encoded by `Lua` and Luerl, however, they can be passed using a `{:userdata, any()}` tuple and encoding them.
166+
167+
Values may be encoded with `Lua.encode!/2`
168+
169+
Elixir type | Luerl type | Requires encoding?
170+
:-------------------------- | :------------------------ | :---------------------
171+
`nil` | `nil` | no
172+
`boolean()` | `boolean()` | no
173+
`number()` | `number()` | no
174+
`binary()` | `binary()` | no
175+
`atom()` | `binary()` | yes
176+
`map()` | `:luerl.tref()` | yes
177+
`{:userdata, any()}` | `:luerl.usdref()` | yes
178+
`(any()) -> any()` | `:luerl.erl_func()` | yes
179+
`(any(), Lua.t()) -> any()` | `:luerl.erl_func()` | yes
180+
`{module(), atom(), list()` | `:luerl.erl_mfa()` | yes
181+
`list(any())` | `list(luerl type)` | maybe (if any of its values require encoding)
182+
183+
184+
## Userdata
185+
186+
There are situations where you want to pass around a reference to some Elixir datastructure, such as a struct. In these situations, you can use a `{:userdata, any()}` tuple.
187+
188+
``` elixir
189+
defmodule Thing do
190+
defstruct [:value]
191+
end
192+
193+
{encoded, lua} = Lua.encode!(Lua.new(), {:userdata, %Thing{value: "1234"}})
194+
195+
lua = Lua.set!(lua, [:foo], encoded)
196+
197+
{[{:userdata, %Thing{value: "1234"}}], _} = Lua.eval!(lua, "return foo")
198+
```
199+
200+
Trying to deference userdata inside a Lua program will result in an exception.
201+
202+
163203
## Credits
164204

165205
`Lua` piggy-backs off of Robert Virding's [Luerl](https://github.com/rvirding/luerl) project, which implements a Lua lexer, parser, and full-blown Lua virtual machine that runs inside the BEAM.

guides/working-with-lua.livemd

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
```elixir
66
Mix.install([
7-
{:lua, "~> 0.1.1"}
7+
{:lua, "~> 0.2.0"}
88
])
99
```
1010

@@ -39,9 +39,7 @@ code = ~LUA"""
3939
## Getting and Setting values
4040

4141
```elixir
42-
lua =
43-
Lua.new()
44-
|> Lua.set!([:dave], "Lucia")
42+
lua = Lua.set!(Lua.new(), [:dave], "Lucia")
4543

4644
{["Grohl", "Lucia"], %Lua{} = lua} =
4745
Lua.eval!(lua, ~LUA"""
@@ -74,6 +72,9 @@ defmodule Global do
7472
@variadic true
7573
deflua my_print(args) do
7674
IO.puts(Enum.join(args, " "))
75+
76+
# Return nothing
77+
[]
7778
end
7879
end
7980

lib/lua.ex

Lines changed: 75 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -175,30 +175,62 @@ defmodule Lua do
175175
iex> {[3], _} = Lua.eval!(lua, "set_count(1, 2, 3); return count")
176176
177177
"""
178-
def set!(%__MODULE__{} = lua, keys, value) do
179-
value =
180-
case value do
178+
def set!(%__MODULE__{}, [], _) do
179+
raise Lua.RuntimeException, "Lua.set!/3 cannot have empty keys"
180+
end
181+
182+
def set!(%__MODULE__{} = lua, keys, func) when is_function(func, 1) or is_function(func, 2) do
183+
{function_name, scope} = List.pop_at(keys, -1)
184+
185+
func =
186+
case func do
181187
func when is_function(func, 1) ->
182-
func
188+
fn data ->
189+
return = func.(data)
190+
191+
if not Lua.Util.encoded?(return) do
192+
raise Lua.RuntimeException,
193+
function: function_name,
194+
scope: scope,
195+
message: "deflua functions must return encoded data, got #{inspect(return)}"
196+
end
197+
198+
return
199+
end
183200

184201
func when is_function(func, 2) ->
185202
fn args, state ->
186203
case func.(args, wrap(state)) do
187-
{value, %__MODULE__{} = lua} ->
188-
{List.wrap(value), lua.state}
189-
190204
{:error, reason, %__MODULE__{} = lua} ->
191205
:luerl_lib.lua_error(reason, lua.state)
192206

207+
{value, %__MODULE__{} = lua} ->
208+
if not Lua.Util.encoded?(value) do
209+
raise Lua.RuntimeException,
210+
function: function_name,
211+
scope: scope,
212+
message: "deflua functions must return encoded data, got #{inspect(value)}"
213+
end
214+
215+
{List.wrap(value), lua.state}
216+
193217
value ->
218+
if not Lua.Util.encoded?(value) do
219+
raise Lua.RuntimeException,
220+
function: function_name,
221+
scope: scope,
222+
message: "deflua functions must return encoded data, got #{inspect(value)}"
223+
end
224+
194225
{List.wrap(value), state}
195226
end
196227
end
197-
198-
value ->
199-
value
200228
end
201229

230+
do_set(lua.state, keys, func)
231+
end
232+
233+
def set!(%__MODULE__{} = lua, keys, value) do
202234
do_set(lua.state, keys, value)
203235
end
204236

@@ -221,7 +253,14 @@ defmodule Lua do
221253
end
222254
end)
223255

224-
case :luerl.set_table_keys_dec(keys, value, state) do
256+
set_keys =
257+
if Lua.Util.encoded?(value) do
258+
&:luerl.set_table_keys/3
259+
else
260+
&:luerl.set_table_keys_dec/3
261+
end
262+
263+
case set_keys.(keys, value, state) do
225264
{:ok, state} ->
226265
wrap(state)
227266

@@ -607,7 +646,7 @@ defmodule Lua do
607646
@doc """
608647
Puts a private value in storage for use in Elixir functions
609648
610-
iex> lua = Lua.new() |> Lua.put_private(:api_key, "1234")
649+
iex> Lua.new() |> Lua.put_private(:api_key, "1234")
611650
"""
612651
def put_private(%__MODULE__{} = lua, key, value) do
613652
update_in(lua.state, fn state -> :luerl.put_private(key, value, state) end)
@@ -657,11 +696,11 @@ defmodule Lua do
657696

658697
if with_state? do
659698
fn args, state ->
660-
execute_function_with_state(module, function_name, [args, wrap(state)], wrap(state))
699+
execute_function(module, function_name, [args, wrap(state)], wrap(state))
661700
end
662701
else
663702
fn args, state ->
664-
execute_function(module, function_name, [args], state)
703+
execute_function(module, function_name, [args], wrap(state))
665704
end
666705
end
667706
end
@@ -673,7 +712,7 @@ defmodule Lua do
673712
if with_state? do
674713
fn args, state ->
675714
if (length(args) + 1) in arities do
676-
execute_function_with_state(module, function_name, args ++ [wrap(state)], wrap(state))
715+
execute_function(module, function_name, args ++ [wrap(state)], wrap(state))
677716
else
678717
arities = Enum.map(arities, &(&1 - 1))
679718

@@ -686,7 +725,7 @@ defmodule Lua do
686725
else
687726
fn args, state ->
688727
if length(args) in arities do
689-
execute_function(module, function_name, args, state)
728+
execute_function(module, function_name, args, wrap(state))
690729
else
691730
raise Lua.RuntimeException,
692731
function: function_name,
@@ -697,7 +736,7 @@ defmodule Lua do
697736
end
698737
end
699738

700-
defp execute_function(module, function_name, args, state) do
739+
defp execute_function(module, function_name, args, lua) do
701740
# Luerl mandates lists as return values; this function ensures all results conform.
702741
case apply(module, function_name, args) do
703742
# Table-like keyword list
@@ -715,42 +754,30 @@ defmodule Lua do
715754
message: "maps must be explicitly encoded to tables using Lua.encode!/2"
716755

717756
{:error, reason} ->
718-
:luerl_lib.lua_error(reason, state)
719-
720-
other ->
721-
{List.wrap(other), state}
722-
end
723-
catch
724-
thrown_value ->
725-
{:error,
726-
"Value thrown during function '#{function_name}' execution: #{inspect(thrown_value)}"}
727-
end
757+
:luerl_lib.lua_error(reason, lua.state)
728758

729-
defp execute_function_with_state(module, function_name, args, lua) do
730-
# Luerl mandates lists as return values; this function ensures all results conform.
731-
case apply(module, function_name, args) do
732-
{ret, %Lua{state: state}} ->
733-
{ret, state}
759+
{:error, reason, %Lua{} = lua} ->
760+
:luerl_lib.lua_error(reason, lua.state)
734761

735-
# Table-like keyword list
736-
[{_, _} | _rest] ->
737-
raise Lua.RuntimeException,
738-
function: function_name,
739-
scope: module.scope(),
740-
message: "keyword lists must be explicitly encoded to tables using Lua.encode!/2"
762+
{data, %Lua{} = lua} ->
763+
if not Lua.Util.encoded?(data) do
764+
raise Lua.RuntimeException,
765+
function: function_name,
766+
scope: module.scope(),
767+
message: "deflua functions must return encoded data, got #{inspect(data)}"
768+
end
741769

742-
# Map
743-
map when is_map(map) ->
744-
raise Lua.RuntimeException,
745-
function: function_name,
746-
scope: module.scope(),
747-
message: "maps must be explicitly encoded to tables using Lua.encode!/2"
770+
{List.wrap(data), lua.state}
748771

749-
{:error, reason, %Lua{} = lua} ->
750-
:luerl_lib.lua_error(reason, lua.state)
772+
data ->
773+
if not Lua.Util.encoded?(data) do
774+
raise Lua.RuntimeException,
775+
function: function_name,
776+
scope: module.scope(),
777+
message: "deflua functions must return encoded data, got #{inspect(data)}"
778+
end
751779

752-
other ->
753-
{List.wrap(other), lua.state}
780+
{List.wrap(data), lua.state}
754781
end
755782
catch
756783
thrown_value ->

lib/lua/util.ex

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,28 @@ defmodule Lua.Util do
1010
Record.defrecord(name, fields)
1111
end
1212

13+
# Returns true for identity values
14+
# or values that hold internal Luerl representations like tref
15+
def encoded?(nil), do: true
16+
def encoded?(false), do: true
17+
def encoded?(true), do: true
18+
def encoded?(binary) when is_binary(binary), do: true
19+
# TODO Remove since this shouldn't be decoded
20+
# take out when https://github.com/rvirding/luerl/pull/213
21+
# is released
22+
def encoded?(number) when is_number(number), do: true
23+
def encoded?(table_ref) when Record.is_record(table_ref, :tref), do: true
24+
def encoded?(record) when Record.is_record(record, :usdref), do: true
25+
def encoded?(record) when Record.is_record(record, :funref), do: true
26+
def encoded?(record) when Record.is_record(record, :erl_func), do: true
27+
def encoded?(record) when Record.is_record(record, :erl_mfa), do: true
28+
29+
def encoded?(list) when is_list(list) do
30+
Enum.all?(list, &encoded?/1)
31+
end
32+
33+
def encoded?(_), do: false
34+
1335
def format_error(error) do
1436
case error do
1537
{line, type, {:illegal, value}} ->

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ defmodule Lua.MixProject do
22
use Mix.Project
33

44
@url "https://github.com/tv-labs/lua"
5-
@version "0.1.1"
5+
@version "0.2.0"
66

77
def project do
88
[

0 commit comments

Comments
 (0)