Showing preview only (206K chars total). Download the full file or copy to clipboard to get everything.
Repository: functional-rewire/dune
Branch: main
Commit: 8a6f7ddfb6a3
Files: 57
Total size: 191.6 KB
Directory structure:
gitextract_bo31q11i/
├── .formatter.exs
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── lib/
│ ├── dune/
│ │ ├── allowlist/
│ │ │ ├── default.ex
│ │ │ ├── docs.ex
│ │ │ └── spec.ex
│ │ ├── allowlist.ex
│ │ ├── atom_mapping.ex
│ │ ├── eval/
│ │ │ ├── env.ex
│ │ │ ├── fake_module.ex
│ │ │ ├── function_clause_error.ex
│ │ │ ├── macro_env.ex
│ │ │ └── process.ex
│ │ ├── eval.ex
│ │ ├── failure.ex
│ │ ├── helpers/
│ │ │ ├── diagnostics.ex
│ │ │ └── term_checker.ex
│ │ ├── opts.ex
│ │ ├── parser/
│ │ │ ├── atom_encoder.ex
│ │ │ ├── compile_env.ex
│ │ │ ├── debug.ex
│ │ │ ├── real_module.ex
│ │ │ ├── safe_ast.ex
│ │ │ ├── sanitizer.ex
│ │ │ ├── string_parser.ex
│ │ │ └── unsafe_ast.ex
│ │ ├── parser.ex
│ │ ├── session.ex
│ │ ├── shims/
│ │ │ ├── atom.ex
│ │ │ ├── enum.ex
│ │ │ ├── io.ex
│ │ │ ├── json.ex
│ │ │ ├── kernel.ex
│ │ │ ├── list.ex
│ │ │ └── string.ex
│ │ └── success.ex
│ └── dune.ex
├── mix.exs
└── test/
├── dune/
│ ├── allowlist/
│ │ └── default_test.exs
│ ├── allowlist_test.exs
│ ├── atom_mapping_test.exs
│ ├── opts_test.exs
│ ├── parser/
│ │ ├── atom_encoder_test.exs
│ │ └── string_parser_test.exs
│ ├── session_test.exs
│ ├── shims_test.exs
│ └── validation_test.exs
├── dune_modules_test.exs
├── dune_oom_safety_test.exs
├── dune_quoted_test.exs
├── dune_string_test.exs
├── dune_string_to_quoted_test.exs
├── dune_test.exs
└── test_helper.exs
================================================
FILE CONTENTS
================================================
================================================
FILE: .formatter.exs
================================================
# Used by "mix format"
locals_without_parens = [allow: 2]
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
locals_without_parens: locals_without_parens,
export: [locals_without_parens: locals_without_parens]
]
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}}
env:
MIX_ENV: test
strategy:
matrix:
include:
- elixir: "1.14.5"
otp: "26.0"
testArgs: "--exclude=lts_only"
- elixir: "1.15.4"
otp: "26.0"
testArgs: "--exclude=lts_only"
- elixir: "1.16.0"
otp: "26.0"
testArgs: "--exclude=lts_only"
- elixir: "1.17.3"
otp: "27.0"
testArgs: "--exclude=lts_only"
- elixir: "1.18.0"
otp: "27.0"
testArgs: "--exclude=lts_only"
- elixir: "1.18.4"
otp: "27.0"
testArgs: "--exclude=lts_only"
- elixir: "1.19.0"
otp: "28.1"
testArgs: ""
steps:
- uses: actions/checkout@v2
- uses: erlef/setup-beam@v1
with:
otp-version: ${{matrix.otp}}
elixir-version: ${{matrix.elixir}}
- name: Install Dependencies
run: mix deps.get
- name: Check compile warnings
run: mix compile --warnings-as-errors
- name: Check format
run: mix format --check-formatted
# TODO add dialyzer?
- name: Unit tests
run: mix test ${{ matrix.testArgs }}
================================================
FILE: .gitignore
================================================
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
dune-*.tar
# Temporary files, for example, from tests.
/tmp/
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## Dev
## v0.3.15 (2025-10-19)
- Support Elixir 1.19
## v0.3.14 (2025-07-29)
### Security fixes
- Use safe shims for chardata -> string conversions (`List.to_string/1`, ...) in
`Dune.Allowlist.Default`
- Restrict more unsafe modules and functions in `Dune.Allowlist.Default`:
- `:unicode`
- `:erts_debug.flat_size`
## v0.3.13 (2025-07-27)
- Fix older versions pre-1.18 that don't have `JSON`
## v0.3.12 (2025-07-27)
- `Dune.Allowlist.Default` exposes a safe shim for the `JSON` module
## v0.3.11 (2024-12-21)
- Enable support for Elixir 1.18
## v0.3.10 (2024-07-14)
- Enable support for Elixir 1.17
## v0.3.9 (2024-06-25)
- `Dune.Allowlist.Default` allows the `Version` module
## v0.3.8 (2024-05-26)
### Bug fixes
- Make sure the `Duration` atom is available
## v0.3.7 (2024-05-26)
### Bug fixes
- Fix incorrect type definitions, remove unused ones
### Enhancements
- `Dune.Allowlist.Default` allows the new `Duration` module and new kernel
functions from Elixir 1.17
- Add an `:inspect_sort_maps` option for deterministic outputs
- Capture and return parser warnings in `stdio`
## v0.3.6 (2023-12-23)
- Support Elixir 1.16
- `Dune.Allowlist.Default` allows `**/2`
## v0.3.5 (2023-11-10)
### Enhancements
- Prepare Elixir 1.16 support(handle line-column positions in diagnostics)
## v0.3.4 (2023-09-14)
### Bug fixes
- Fix `UndefinedFunctionError` when using external modules in a custom allowlist
## v0.3.3 (2023-08-13)
### Bug fixes
- Fix vulnerability allowing an attacker to crash the VM using bitstrings
## v0.3.2 (2023-08-12)
### Enhancements
- `dbg/1` uses pretty printing
### Bug fixes
- Fix error message on restricted `dbg/0`
## v0.3.1 (2023-08-12)
### Enhancements
- Add support for `dbg/1`
### Bug fixes
- Properly distinguish user code `throw/1` from internal ones
## v0.3.0 (2023-08-09)
### Breaking changes
- Drop support for Elixir 1.13
- Compile errors are now returned as a separate type `:compile_error`
### Enhancements
- Support Elixir 1.15
- Capture compile diagnostics (Elixir >= 1.15)
### Bug fixes
- Better handle `UndefinedFunctionError` for dynamic module names
## v0.2.6 (2022-10-17)
### Enhancements
- Support Elixir 1.14
## v0.2.5 (2022-08-25)
### Bug fixes
- Restrict the use of `:counters` in `Dune.Allowlist.Default`, since it can leak
memory
## v0.2.4 (2022-07-13)
### Bug fixes
- Validate module names in `defmodule`, reject `nil` or booleans
## v0.2.3 (2022-04-13)
### Bug fixes
- `Dune.string_to_quoted/2` quotes modules with `.` correctly
- OTP 25 regression: keep a clean stacktrace for exceptions
## v0.2.2 (2022-04-05)
### Enhancements
- Add `Dune.string_to_quoted/2` to make it possible to visualize AST
- Merged parsing and eval options in a single `Dune.Opts` for simplicity
- Add a `pretty` option to inspect result
- Better error message when `def/2` and `defp/2` called outside a module
### Breaking changes
- Removed Dune.Parser.Opts and Dune.Eval.Opts
## v0.2.1 (2022-03-19)
### Bug fixes
- Handle default arguments in functions
- Handle conflicting `def` and `defp` with same name/arity
## v0.2.0 (2022-01-02)
### Breaking changes
- Support Elixir 1.13, drop support for 1.12
- This fixes a [bug in atoms](https://github.com/elixir-lang/elixir/pull/11313)
was due to the Elixir parser
## v0.1.2 (2021-10-17)
### Enhancements
- Allow safe functions from the `:erlang` module
### Bug fixes
- Fix bug when calling custom function in nested AST
## v0.1.1 (2021-10-16)
### Bug fixes
- Prevent atom leaks due to `Code.string_to_quoted/2` not respecting
`static_atoms_encoder`
- Handle Elixir 1.12 bug on single atom ASTs
- Handle atoms prefixed with `Elixir.` properly
- Fix inspect for quoted atoms
## v0.1.0 (2021-09-19)
- Initial release
================================================
FILE: LICENSE.md
================================================
MIT License
Copyright (c) 2021 Sabiwara
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.
================================================
FILE: README.md
================================================
# Dune
[](https://hex.pm/packages/dune)
[](https://hexdocs.pm/dune/)
[](https://github.com/functional-rewire/dune/actions?query=workflow%3ACI)
A sandbox for Elixir to safely evaluate untrusted code from user input.
[**Try it out on our online playground!**](https://playground.functional-rewire.com/)
`Dune` can be useful to develop playgrounds, online REPL, coding games, or
customizable business logic.
**Warning:** `Dune` cannot offer strong security guarantees (see the
[Security guarantees](#security-guarantees) section below). Besides, it is still
early stage: expect bugs and vulnerabilities.
## Features
- allowlist mechanism (customizable) to restrict execution to safe modules and
functions: no access to environment variables, file system, network...
- code executed in an isolated process
- execution within configurable limits: timeout, maximum reductions and memory
(inspired by [Luerl](https://github.com/rvirding/luerl))
- captured standard output
- atoms, without atom leaks: parsing and runtime do not
[leak atoms](https://hexdocs.pm/elixir/String.html#to_atom/1) (i.e. does not
keep
[filling the atom table](https://learnyousomeerlang.com/starting-out-for-real#atoms)
until the VM crashes)
- modules, without actual module creation: Dune does not let users define any
actual module (would leak memory and modify the state of the VM globally), but
`defmodule` simulates the basic behavior of a module, including private and
recursive functions
```elixir
iex> Dune.eval_string("IO.puts(\"Hello world!\")")
%Dune.Success{inspected: ":ok", stdio: "Hello world!\n", value: :ok}
iex> Dune.eval_string("File.cwd!()")
%Dune.Failure{message: "** (DuneRestrictedError) function File.cwd!/0 is restricted", type: :restricted}
iex> Dune.eval_string("List.duplicate(:spam, 100_000)")
%Dune.Failure{message: "Execution stopped - memory limit exceeded", stdio: "", type: :memory}
iex> Dune.eval_string("Enum.product(1..100_000)")
%Dune.Failure{message: "Execution stopped - reductions limit exceeded", stdio: "", type: :reductions}
```
The list of modules and functions authorized by default is defined by the
[`Dune.Allowlist.Default`](https://hexdocs.pm/dune/Dune.Allowlist.Default.html#module-allowed-modules-functions)
module, but this list can be extended and customized (at your own risk!) using
[`Dune.Allowlist`](https://hexdocs.pm/dune/Dune.Allowlist.html).
If you need to keep the state between evaluations, you might consider
[`Dune.Session`](https://hexdocs.pm/dune/Dune.Session.html):
```elixir
iex> Dune.Session.new()
...> |> Dune.Session.eval_string("x = 1")
...> |> Dune.Session.eval_string("x + 2")
#Dune.Session<last_result: %Dune.Success{inspected: "3", stdio: "", value: 3}, ...>
```
`Dune.string_to_quoted/2` returns the AST corresponding to the provided
`string`, without leaking atoms:
```elixir
iex> Dune.string_to_quoted("foo(:bar)").inspected
"{:foo, [line: 1], [:bar]}"
```
## Limitations
`Dune` supports a fair subset of the base language, but it cannot safely support
advanced features (at least at this stage) such as:
- custom structs / behaviours / protocols
- concurrency / processes / OTP
- metaprogramming
## Security guarantees
Because of the approch being used, Dune cannot offer strong security guarantees,
and should not be considered a sufficient security layer by itself.
A best-effort approach is made to prevent attackers from getting outside of the
original process and from calling any function/macro outside of the allowlist.
However, it is impossible to prove that all escape paths have been completely
blocked. Due to how the Erlang VM works, an attacker able to escape the sandbox
could get full access to the VM without restriction.
See the
[EEF guidelines about sandboxing](https://erlef.github.io/security-wg/secure_coding_and_deployment_hardening/sandboxing)
for more information.
Use at your own risk and avoid running it directly on a server with any
sensitive access, e.g. to a database.
## Installation
The package can be installed by adding `dune` to your list of dependencies in
`mix.exs`:
```elixir
def deps do
[
{:dune, "~> 0.3.15"}
]
end
```
Documentation can be found at
[https://hexdocs.pm/dune](https://hexdocs.pm/dune).
## FAQ
### Why can I still create atoms?
Atoms are converted by the parser and mapped to always use a given set of atoms.
So when you type `foo = bar(:baz)`, the atoms `:foo`, `:bar` and `:baz` won't
actually be created, but other atoms like `:atom1`, `:atom2` and `:atom3` are
going to be used instead.
So even if many user run various codes using many different variable and
function names, they will all pull from the same pool of atoms.
### Why can I still create modules?
Modules are being defined globally within the VM, making it both an isolation
concern and a memory concern. But modules are an important part of the Elixir
language, and are especially important for learning platforms.
Dune implements an alternative `defmodule`, relying on maps of anonymous
functions at runtime and should reproduce the basic behavior of actual modules,
with support for recursive and private functions.
### Why is the behavior different than Elixir when doing X?
As explained above, some parts of the language are actually being completely
reimplemented because the original version could not be safely sandboxed: atoms,
modules... While these alternative implementation aim to be as close as possible
to the original ones, they might differ in some cases, because the code being
executed is actually different.
### Why can't I do X?
Some parts of the language are being restricted because they present a direct
security risk, while some other would need to be reimplemented in an alternative
way and therefore need a consequent amount of work that hasn't been done yet.
================================================
FILE: lib/dune/allowlist/default.ex
================================================
defmodule Dune.Allowlist.Default do
@moduledoc """
The default `Dune.Allowlist` module to be used to allow or restrict
functions and macros that can be safely executed.
## Examples
iex> Dune.Allowlist.Default.fun_status(Kernel, :+, 2)
:allowed
iex> Dune.Allowlist.Default.fun_status(String, :to_atom, 1)
:restricted
iex> Dune.Allowlist.Default.fun_status(Atom, :to_string, 1)
{:shimmed, Dune.Shims.Atom, :to_string}
iex> Dune.Allowlist.Default.fun_status(Kernel, :foo, 1)
:undefined_function
iex> Dune.Allowlist.Default.fun_status(Bar, :foo, 1)
:undefined_module
iex> Dune.Allowlist.Default.fun_status(Kernel.SpecialForms, :quote, 2)
:restricted
## Allowed modules / functions
__DUNE_ALLOWLIST_FUNCTIONS__
"""
use Dune.Allowlist
alias Dune.Shims
@special_forms_allowed ~w[
{}
%{}
<<>>
=
^
case
cond
fn
for
with
::
__aliases__
]a
@kernel_operators ~w[
|>
+
++
-
--
*
**
/
<>
==
===
!=
!==
=~
>
>=
<
<=
and
or
&&
||
!
..
..//
]a
@kernel_guards ~w[
is_integer
is_binary
is_bitstring
is_atom
is_boolean
is_integer
is_float
is_number
is_list
is_map
is_map_key
is_nil
is_reference
is_tuple
is_exception
is_struct
is_function
]a
@kernel_macros ~w[
if
unless
in
match?
then
tap
raise
]a
@kernel_sigils ~w[
sigil_C
sigil_D
sigil_N
sigil_R
sigil_S
sigil_T
sigil_U
sigil_c
sigil_r
sigil_s
sigil_w
]a
@kernel_functions ~w[
abs
binary_part
bit_size
byte_size
ceil
div
elem
floor
get_and_update_in
get_in
hd
length
make_ref
map_size
max
min
not
pop_in
put_elem
put_in
rem
round
self
tl
trunc
tuple_size
update_in
]a
# TODO Remove when dropping support for Elixir 1.16
extra_kernel_functions =
if System.version() |> Version.compare("1.17.0-rc.0") != :lt,
do: [:to_timeout, :is_non_struct_map],
else: []
@kernel_allowed extra_kernel_functions ++
@kernel_operators ++
@kernel_guards ++ @kernel_macros ++ @kernel_sigils ++ @kernel_functions
@kernel_shims [
apply: {Shims.Kernel, :safe_apply},
inspect: {Shims.Kernel, :safe_inspect},
to_string: {Shims.Kernel, :safe_to_string},
to_charlist: {Shims.Kernel, :safe_to_charlist},
sigil_w: {Shims.Kernel, :safe_sigil_w},
sigil_W: {Shims.Kernel, :safe_sigil_W},
throw: {Shims.Kernel, :safe_throw},
dbg: {Shims.Kernel, :safe_dbg}
]
@erlang_allowed [
:*,
:+,
:++,
:-,
:--,
:/,
:"/=",
:<,
:"=/=",
:"=:=",
:"=<",
:==,
:>,
:>=,
:abs,
:adler32,
:adler32_combine,
:and,
:append_element,
:band,
:binary_part,
:binary_to_float,
:binary_to_integer,
:binary_to_list,
:bit_size,
:bitstring_to_list,
:bnot,
:bor,
:bsl,
:bsr,
:bxor,
:byte_size,
:ceil,
:convert_time_unit,
:crc32,
:crc32_combine,
:date,
:delete_element,
:div,
:element,
:float,
:float_to_binary,
:float_to_list,
:floor,
:hd,
:insert_element,
:integer_to_binary,
:integer_to_list,
:iolist_size,
:iolist_to_binary,
:iolist_to_iovec,
:is_atom,
:is_binary,
:is_bitstring,
:is_boolean,
:is_float,
:is_function,
:is_integer,
:is_list,
:is_map,
:is_map_key,
:is_number,
:is_pid,
:is_port,
:is_record,
:is_reference,
:is_tuple,
:length,
:list_to_binary,
:list_to_bitstring,
:list_to_float,
:list_to_integer,
:localtime,
:localtime_to_universaltime,
:make_ref,
:make_tuple,
:map_get,
:map_size,
:max,
:md5,
:md5_final,
:md5_init,
:md5_update,
:min,
:monotonic_time,
:not,
:or,
:phash2,
:ref_to_list,
:rem,
:round,
:setelement,
:size,
:split_binary,
:system_time,
:time,
:time_offset,
:timestamp,
:tl,
:trunc,
:tuple_size,
:tuple_to_list,
:unique_integer,
:universaltime,
:universaltime_to_localtime,
:xor
]
@erlang_shims [
apply: {Shims.Kernel, :safe_apply}
]
@io_shims [
puts: {Shims.IO, :puts},
inspect: {Shims.IO, :inspect},
chardata_to_string: {Shims.List, :to_string}
]
allow Kernel.SpecialForms, only: @special_forms_allowed
allow Kernel, only: @kernel_allowed, shims: @kernel_shims
allow Access, :all
allow String, except: ~w[to_atom to_existing_atom]a
allow Regex, :all
allow Map, :all
allow MapSet, :all
allow Keyword, :all
allow Tuple, :all
allow List, shims: [to_string: {Shims.List, :to_string}], except: ~w[to_atom to_existing_atom]a
allow Enum, shims: [join: {Shims.Enum, :join}, map_join: {Shims.Enum, :map_join}]
# TODO double check
allow Stream, :all
allow Range, :all
allow Integer, :all
allow Float, :all
allow Atom,
except: ~w[to_char_list]a,
shims: [to_string: {Shims.Atom, :to_string}, to_charlist: {Shims.Atom, :to_charlist}]
if Code.ensure_loaded?(JSON) do
allow JSON,
only: ~w[decode decode!]a,
shims: Enum.map(~w[protocol_encode encode! encode_to_iodata!]a, &{&1, {Shims.JSON, &1}})
end
allow Date, :all
allow DateTime, :all
allow NaiveDateTime, :all
# TODO Remove when dropping support for Elixir 1.16
if System.version() |> Version.compare("1.17.0-rc.0") != :lt do
allow Duration, :all
end
allow Calendar, except: ~w[put_time_zone_database]a
allow Calendar.ISO, :all
allow Time, :all
allow Base, :all
allow URI, :all
allow Version, :all
allow Bitwise, :all
allow Function, only: ~w[identity]a
allow IO, only: ~w[iodata_length iodata_to_binary]a, shims: @io_shims
allow Process, only: [:sleep]
allow :erlang, only: @erlang_allowed, shims: @erlang_shims
allow :math, :all
allow :binary, :all
allow :lists, :all
allow :array, :all
allow :maps, :all
allow :gb_sets, :all
allow :gb_trees, :all
allow :ordsets, :all
allow :orddict, :all
allow :proplists, :all
allow :queue, :all
allow :string, :all
allow :rand, :all
# note: :unicode is not safe and should be shimmed due to "structural sharing bombs"
# note: flat_size is unsafe due to "structural sharing bombs"
allow :erts_debug, only: ~w[same size size_shared]a
allow :zlib, only: ~w[zip unzip gzip gunzip compress uncompress]a
end
================================================
FILE: lib/dune/allowlist/docs.ex
================================================
defmodule Dune.Allowlist.Docs do
@moduledoc false
def document_allowlist(spec) do
spec
|> Dune.Allowlist.Spec.list_ordered_modules()
|> Enum.map_join("\n", &do_doc_funs/1)
end
def public_functions(module) when is_atom(module) do
case Code.fetch_docs(module) do
{:docs_v1, _, _, _, _, _, list} ->
for {{:function, function_name, _}, _, _, %{}, %{}} <- list, into: MapSet.new() do
function_name
end
_ ->
[]
end
end
defp do_doc_funs({module, grouped_funs}) do
public_funs = public_functions(module)
head = ["- `", inspect(module), "`"]
tail =
Enum.map(grouped_funs, fn {status, funs} ->
[
"**",
format_status(status),
"**: " | Enum.map_intersperse(funs, ", ", &format_fun(module, &1, status, public_funs))
]
end)
Enum.intersperse([head | tail], "\n - ") |> to_string()
end
defp format_fun(module, {fun, arity}, status, public_funs) do
if fun in public_funs or module in [Kernel, Kernel.SpecialForms] do
[
?[,
maybe_strike(status),
?`,
Atom.to_string(fun),
?`,
maybe_strike(status),
"](`",
inspect(module),
?.,
to_string(fun),
?/,
to_string(arity),
"`)"
]
else
[
maybe_strike(status),
?`,
Atom.to_string(fun),
?`,
maybe_strike(status)
]
end
end
defp maybe_strike(:restricted), do: "~~"
defp maybe_strike(_status), do: []
defp format_status(:allowed), do: "Allowed"
defp format_status(:shimmed), do: "Alernative implementation"
defp format_status(:restricted), do: "Restricted"
end
Dune.Allowlist.Docs.public_functions(:rand)
================================================
FILE: lib/dune/allowlist/spec.ex
================================================
defmodule Dune.Allowlist.Spec do
@moduledoc false
alias Dune.Allowlist
alias Dune.Parser.RealModule
@type t :: %__MODULE__{
modules: %{optional(module) => [{atom, non_neg_integer, Allowlist.status()}]}
}
@enforce_keys [:modules]
defstruct @enforce_keys
def new do
%__MODULE__{modules: %{}}
end
@spec list_fun_statuses(t) :: list({module, atom, Allowlist.status()})
def list_fun_statuses(%__MODULE__{modules: modules}) do
for {module, funs} <- modules, {fun_name, _arity, status} <- funs do
{module, fun_name, status}
end
|> Enum.sort()
|> Enum.dedup()
end
@spec list_ordered_modules(t) :: list({module, {atom, Allowlist.status()}})
def list_ordered_modules(%__MODULE__{modules: modules}) do
modules
|> Enum.map(fn {module, funs} ->
{module, group_funs_by_status(funs)}
end)
|> Enum.sort()
end
defp group_funs_by_status(funs) do
Enum.group_by(
funs,
fn {_fun, _arity, status} -> extract_status_atom(status) end,
fn {fun, arity, _status} -> {fun, arity} end
)
|> Enum.map(fn {status, funs} -> {status, Enum.sort(funs) |> Enum.dedup_by(&elem(&1, 0))} end)
|> Enum.sort_by(fn {status, _} -> status_sort(status) end)
end
defp extract_status_atom(:restricted), do: :restricted
defp extract_status_atom(:allowed), do: :allowed
defp extract_status_atom({:shimmed, _, _}), do: :shimmed
defp status_sort(:allowed), do: 1
defp status_sort(:shimmed), do: 2
defp status_sort(:restricted), do: 3
@spec add_new_module(t, module, :all) :: t
def add_new_module(%__MODULE__{modules: modules}, module, _status)
when :erlang.map_get(module, modules) != nil do
# TODO proper error type
raise "ModuleConflict: module #{inspect(module)} already defined"
end
def add_new_module(spec = %__MODULE__{modules: modules}, module, status) when is_atom(module) do
Code.ensure_compiled!(module)
functions =
RealModule.list_functions(module)
|> classify_functions(status)
%{spec | modules: Map.put(modules, module, functions)}
end
defp classify_functions(functions, :all) do
Enum.map(functions, fn {fun_name, arity} -> {fun_name, arity, :allowed} end)
end
defp classify_functions(functions, only: only) when is_list(only) do
do_classify(functions, only, :allowed, :restricted)
end
defp classify_functions(functions, except: except) when is_list(except) do
do_classify(functions, except, :restricted, :allowed)
end
defp classify_functions(functions, opts) do
case Keyword.pop(opts, :shims) do
{shims, remaining_opts} when is_list(shims) ->
new_opts =
case remaining_opts do
[] -> :all
other -> other
end
classify_functions(functions, new_opts) |> shim_functions(shims)
{nil, _} ->
raise "Invalid opts #{inspect(opts)}"
end
end
defp do_classify(functions, set_list, member_atom, non_member_atom) do
set = to_atom_set(set_list)
functions
|> Enum.map_reduce(set, fn {fun_name, arity}, acc ->
case set do
%{^fun_name => _} -> {{fun_name, arity, member_atom}, Map.delete(acc, fun_name)}
_ -> {{fun_name, arity, non_member_atom}, acc}
end
end)
|> unwrap_classify()
end
defp to_atom_set(list) do
Enum.each(list, fn atom when is_atom(atom) -> :ok end)
:maps.from_keys(list, nil)
end
defp unwrap_classify({result, remaining}) when remaining == %{}, do: result
defp unwrap_classify({_, remaining}) do
[{key, _}] = Enum.take(remaining, 1)
raise "Unknown function #{key}"
end
defp shim_functions(functions, shims) do
# TODO validate shims
Enum.map(functions, fn fun = {fun_name, arity, _status} ->
case Keyword.get(shims, fun_name) do
nil ->
fun
{shim_module, shim_fun_name} ->
validate_shim!(shim_module, shim_fun_name, arity + 1)
{fun_name, arity, {:shimmed, shim_module, shim_fun_name}}
end
end)
end
defp validate_shim!(module, fun_name, arity) do
Code.ensure_compiled!(module)
unless function_exported?(module, fun_name, arity) or macro_exported?(module, fun_name, arity) do
raise "Invalid shim: function #{inspect(module)}.#{fun_name}/#{arity} doesn't exist!"
end
end
end
================================================
FILE: lib/dune/allowlist.ex
================================================
defmodule Dune.Allowlist do
@moduledoc """
Behaviour to customize the modules and functions that are allowed or restricted.
## Warning: security considerations
The default implementation is `Dune.Allowlist.Default`, and should only allow safe
functions: no atom leaks, no execution of arbitrary code, no access to the filesystem / network...
Defining or extending a custom `Dune.Allowlist` module can introduce security risks or bugs.
Please also note that using custom allowlists is still **experimental** and the API for it
might change faster than the rest of the library.
## Defining a new allowlist
In order to define a custom allowlist from scratch, `use Dune.Allowlist` can be used:
defmodule CustomAllowlist do
use Dune.Allowlist
allow Kernel, only: [:+, :*, :-, :/, :div, :rem]
end
Dune.eval_string("4 + 9", allowlist: CustomAllowlist)
## Extending an existing allowlist
Defining an allowlist from scratch can be both daunting and risky.
It is possible to extend an exisiting allowlist instead using the `extend` option:
defmodule ExtendedAllowlist do
use Dune.Allowlist, extend: Dune.Allowlist.Default
allow SomeModule, only: [:authorized]
end
Dune.eval_string("SomeModule.authorized(123)", allowlist: ExtendedAllowlist)
Note: currently, it is not possible to add or restrict functions from modules
that have already been specified.
## Documentation generation
The list of modules and functions with their status can be generated in the `@moduledoc`.
An example can be found in the `Dune.Allowlist.Default` documentation.
If the `__DUNE_ALLOWLIST_FUNCTIONS__` string is found in the `@moduledoc` string,
it will be replaced.
defmodule CustomAllowlist do
@moduledoc \"\"\"
Only allows simple arithmetic
## Allowlist functions
__DUNE_ALLOWLIST_FUNCTIONS__
\"\"\"
use Dune.Allowlist
allow Kernel, only: [:+, :*, :-, :/, :div, :rem]
end
"""
@type status :: :allowed | :restricted | {:shimmed, module, atom}
@doc """
Returns the trust status of a function or macro, specified as a `module`, `fun_name` and `arity` (`mfa`):
- `:allowed` if can be safely use
- `:restricted` if its usage should be forbidden
- a `{:shimmed, module, function_name}` if the function call should be replaced with an alternative implementation
"""
@callback fun_status(module, atom, non_neg_integer) :: Dune.Allowlist.status()
@doc """
Validates the fact that a module implements the `Dune.Allowlist` behaviour.
Raises if not the case.
## Examples
iex> Dune.Allowlist.ensure_implements_behaviour!(DoesNotExists)
** (ArgumentError) could not load module DoesNotExists due to reason :nofile
iex> Dune.Allowlist.ensure_implements_behaviour!(List)
** (ArgumentError) List does not implement the Dune.Allowlist behaviour
"""
@spec ensure_implements_behaviour!(module) :: module
def ensure_implements_behaviour!(module) when is_atom(module) do
Code.ensure_compiled!(module)
implemented? =
module.module_info(:attributes)
|> Keyword.get(:behaviour, [])
|> Enum.member?(Dune.Allowlist)
unless implemented? do
raise ArgumentError,
message: "#{inspect(module)} does not implement the Dune.Allowlist behaviour"
end
module
end
defmacro __using__(opts) do
extend = extract_extend_opt(opts, __CALLER__)
quote do
import Dune.Allowlist, only: [allow: 2]
@behaviour Dune.Allowlist
Module.register_attribute(__MODULE__, :allowlist, accumulate: true)
Module.put_attribute(__MODULE__, :extend_allowlist, unquote(extend))
@before_compile Dune.Allowlist
end
end
@doc """
Adds a new module to the allowlist and specifices which functions to use.
The module must not be already specified in the allowlist.
Must be called after `use Dune.Allowlist`.
## Examples
# allow all functions in a module
allow Time, :all
# only allow specific functions
allow Function, only: [:identity]
# exclude specific functions
allow Calendar, except: [:put_time_zone_database]
Note: `only` and `except` will cover all arities if several functions
share a name.
"""
defmacro allow(module, status) do
quote do
Module.put_attribute(__MODULE__, :allowlist, {unquote(module), unquote(status)})
end
end
defmacro __before_compile__(env) do
Dune.Allowlist.__postprocess__(env.module)
end
defp extract_extend_opt(opts, caller) do
case Keyword.fetch(opts, :extend) do
{:ok, module_ast} ->
Macro.expand(module_ast, caller) |> ensure_implements_behaviour!()
_ ->
nil
end
end
@doc false
def __postprocess__(module) do
extend = Module.get_attribute(module, :extend_allowlist)
spec = generate_spec(module, extend)
update_module_doc(module, spec)
quote do
unquote(def_spec(spec))
unquote(def_fun_status(spec))
@on_load :ensure_alias_atoms
# Aliases like Foo.Bar are represented on the AST level as {:alias, _, [:Foo, :Bar]}
# We need to force the creation of these atoms, which might otherwise not be
# available when we parse due to atom encoding logic.
def ensure_alias_atoms do
# do nothing and returns :ok
Enum.each(unquote(alias_atoms(spec)), & &1)
end
end
end
defp generate_spec(module, extend) do
base_spec =
case extend do
nil -> Dune.Allowlist.Spec.new()
allowlist when is_atom(allowlist) -> allowlist.spec()
end
Module.get_attribute(module, :allowlist)
|> Enum.reduce(base_spec, fn {module, status}, acc ->
Dune.Allowlist.Spec.add_new_module(acc, module, status)
end)
end
defp def_spec(spec) do
quote do
@doc false
@spec spec :: Dune.Allowlist.Spec.t()
def spec do
unquote(Macro.escape(spec))
end
end
end
defp def_fun_status(spec) do
defps =
for {m, f, status} = _ <- Dune.Allowlist.Spec.list_fun_statuses(spec) do
quote do
defp do_fun_status(unquote(m), unquote(f)),
do: unquote(Macro.escape(status))
end
end
quote do
@impl Dune.Allowlist
@doc "Implements `c:Dune.Allowlist.fun_status/3`"
def fun_status(module, fun_name, arity)
when is_atom(module) and is_atom(fun_name) and is_integer(arity) and arity >= 0 do
with :defined <- Dune.Parser.RealModule.fun_status(module, fun_name, arity) do
do_fun_status(module, fun_name)
end
end
unquote(defps)
defp do_fun_status(_module, _fun_name), do: :restricted
end
end
defp update_module_doc(module, spec) do
case Module.get_attribute(module, :moduledoc) do
{line, doc} when is_binary(doc) ->
doc =
String.replace(doc, "__DUNE_ALLOWLIST_FUNCTIONS__", fn _ ->
Dune.Allowlist.Docs.document_allowlist(spec)
end)
Module.put_attribute(module, :moduledoc, {line, doc})
_other ->
:ok
end
end
defp alias_atoms(spec) do
spec.modules
|> Enum.flat_map(fn {mod, _} ->
try do
Module.split(mod)
rescue
ArgumentError ->
# "expected an Elixir module" error -> erlang module
[]
end
end)
|> Enum.map(&String.to_atom/1)
|> Enum.uniq()
end
end
================================================
FILE: lib/dune/atom_mapping.ex
================================================
defmodule Dune.AtomMapping do
@moduledoc false
alias Dune.{Success, Failure}
@type substitute_atom :: atom
@type original_string :: String.t()
@type sub_mapping :: %{optional(substitute_atom) => original_string}
@type extra_info :: %{optional(substitute_atom) => :wrapped}
@typedoc """
Should be considered opaque
"""
@type t :: %__MODULE__{atoms: sub_mapping, modules: sub_mapping, extra_info: extra_info}
@enforce_keys [:atoms, :modules, :extra_info]
defstruct @enforce_keys
@spec new :: t()
def new do
%__MODULE__{atoms: %{}, modules: %{}, extra_info: %{}}
end
@spec from_atoms([{substitute_atom, original_string}], [{substitute_atom, :wrapped}]) :: t
def from_atoms(list, extra_info) when is_list(list) do
atoms = build_mapping(list)
extra_info = Map.new(extra_info)
%__MODULE__{atoms: atoms, modules: %{}, extra_info: extra_info}
end
@spec add_modules(t, [{substitute_atom, original_string}]) :: t
def add_modules(mapping = %__MODULE__{}, list) do
%{mapping | modules: build_mapping(list)}
end
defp build_mapping(list) do
Map.new(list, fn {substitute_atom, original_string}
when is_atom(substitute_atom) and is_binary(original_string) ->
{substitute_atom, original_string}
end)
end
@spec to_string(t, atom) :: String.t()
def to_string(mapping = %__MODULE__{}, atom) when is_atom(atom) do
case lookup_original_string(mapping, atom) do
{:atom, string} -> string
{:wrapped_atom, string} -> string
{:module, string} -> "Elixir.#{string}"
:error -> Atom.to_string(atom)
end
end
@spec inspect(t, atom) :: String.t()
def inspect(mapping = %__MODULE__{}, atom) when is_atom(atom) do
case lookup_original_string(mapping, atom) do
{:atom, string} -> ":#{string}"
{:wrapped_atom, string} -> ~s(:"#{string}")
{:module, string} -> string
:error -> inspect(atom)
end
end
@spec lookup_original_string(t, atom) :: {:atom | :wrapped_atom | :module, String.t()} | :error
def lookup_original_string(mapping = %__MODULE__{}, atom) when is_atom(atom) do
case mapping.modules do
%{^atom => string} ->
{:module, string}
_ ->
case mapping.atoms do
%{^atom => string} ->
case mapping.extra_info do
%{^atom => :wrapped} -> {:wrapped_atom, string}
_ -> {:atom, string}
end
_ ->
:error
end
end
end
@spec replace_in_string(t, String.t()) :: String.t()
def replace_in_string(mapping, string) when is_binary(string) do
if string =~ "Dune" do
do_replace_in_string(mapping, string)
else
string
end
end
@dune_atom_regex ~r/(Dune_Atom_\d+__|a?__Dune_atom_\d+__|Dune_Module_\d+__)/
defp do_replace_in_string(mapping = %__MODULE__{}, string) do
string_replace_map =
%{}
|> build_replace_map(mapping.modules, nil, &inspect/1)
|> build_replace_map(mapping.atoms, mapping.extra_info, &Atom.to_string/1)
String.replace(string, @dune_atom_regex, &Map.get(string_replace_map, &1, &1))
end
defp build_replace_map(map, sub_mapping, extra_info, to_string_fun) do
for {subsitute_atom, original_string} <- sub_mapping, into: map do
replace_by =
case extra_info do
%{^subsitute_atom => :wrapped} -> ~s("#{original_string}")
_ -> original_string
end
{to_string_fun.(subsitute_atom), replace_by}
end
end
@spec replace_in_result(t, Success.t() | Failure.t()) ::
Success.t() | Failure.t()
def replace_in_result(mapping, result)
def replace_in_result(mapping, %Success{} = success) do
%Success{
success
| inspected: replace_in_string(mapping, success.inspected),
stdio: replace_in_string(mapping, success.stdio)
}
end
def replace_in_result(mapping, %Failure{} = error) do
%Failure{
error
| message: replace_in_string(mapping, error.message),
stdio: replace_in_string(mapping, error.stdio)
}
end
@spec to_existing_atom(t, String.t()) :: atom
def to_existing_atom(mapping = %__MODULE__{}, string) when is_binary(string) do
case fetch_existing_atom(mapping, string) do
nil -> String.to_existing_atom(string)
atom -> atom
end
end
defp fetch_existing_atom(mapping, "Elixir." <> module_name) do
Enum.find_value(mapping.modules, fn {subsitute_atom, original_string} ->
if original_string == module_name do
subsitute_atom
end
end)
end
defp fetch_existing_atom(mapping, atom_name) do
Enum.find_value(mapping.atoms, fn {subsitute_atom, original_string} ->
if original_string == atom_name do
subsitute_atom
end
end)
end
end
================================================
FILE: lib/dune/eval/env.ex
================================================
defmodule Dune.Eval.Env do
@moduledoc false
alias Dune.AtomMapping
alias Dune.Eval.FakeModule
@type t :: %__MODULE__{
atom_mapping: AtomMapping.t(),
allowlist: module,
fake_modules: %{optional(atom) => FakeModule.t()}
}
@enforce_keys [:atom_mapping, :allowlist, :fake_modules]
defstruct @enforce_keys
def new(atom_mapping = %AtomMapping{}, allowlist) when is_atom(allowlist) do
%__MODULE__{atom_mapping: atom_mapping, allowlist: allowlist, fake_modules: %{}}
end
def add_module(env = %__MODULE__{fake_modules: modules}, module_name, module = %FakeModule{})
when is_atom(module_name) do
# TODO check a bunch of things here:
# - warn if module redefined
# - fail if overriding existing module
# - fail if overriding Kernel/Special forms
%{env | fake_modules: Map.put(modules, module_name, module)}
end
def apply_fake(env = %__MODULE__{}, module, fun_name, args)
when is_atom(module) and is_atom(fun_name) and is_list(args) do
arity = length(args)
case fetch_fake_function(env, module, fun_name, arity) do
{:def, fun} -> fun.(env, args)
other -> throw({other, module, fun_name, arity})
end
end
defp fetch_fake_function(%{fake_modules: modules}, module, fun_name, arity) do
case modules do
%{^module => fake_module} ->
case FakeModule.get_function(fake_module, fun_name, arity) do
nil -> :undefined_function
{:def, fun} -> {:def, fun}
end
_ ->
:undefined_module
end
end
end
================================================
FILE: lib/dune/eval/fake_module.ex
================================================
defmodule Dune.Eval.FakeModule do
@moduledoc false
@type t :: %__MODULE__{
public_funs: %{optional(atom) => %{required(non_neg_integer) => function}}
}
@enforce_keys [:public_funs]
defstruct @enforce_keys
def get_function(%__MODULE__{public_funs: funs}, fun_name, arity)
when is_atom(fun_name) and is_integer(arity) do
case funs do
%{^fun_name => %{^arity => fun}} -> {:def, fun}
_ -> nil
end
end
end
================================================
FILE: lib/dune/eval/function_clause_error.ex
================================================
defmodule Dune.Eval.FunctionClauseError do
@moduledoc false
defexception [:module, :function, :args]
def message(err = %__MODULE__{function: function, args: args}) do
module = inspect(err.module)
arity = length(args)
args = inspect(args) |> String.slice(1..-2//1)
"no function clause matching in #{module}.#{function}/#{arity}: #{module}.#{function}(#{args})"
end
end
================================================
FILE: lib/dune/eval/macro_env.ex
================================================
defmodule Dune.Eval.MacroEnv do
@moduledoc false
# Recommended way to generate a Macro.Env struct
# https://hexdocs.pm/elixir/main/Macro.Env.html
def make_env do
import Dune.Shims.Kernel, only: [safe_sigil_w: 3, safe_sigil_W: 3], warn: false
%Macro.Env{__ENV__ | file: "nofile", module: nil, function: nil, line: 1}
end
end
================================================
FILE: lib/dune/eval/process.ex
================================================
defmodule Dune.Eval.Process do
@moduledoc false
alias Dune.Failure
alias Dune.Helpers.Diagnostics
def run(fun, opts = %Dune.Opts{}) when is_function(fun, 0) do
with_string_io(fn string_io ->
do_run(fun, opts, string_io)
end)
end
defp do_run(fun, opts, string_io) do
task =
Task.async(fn ->
# spawn within a task to avoid trapping exits in the caller
spawn_trapped_process(
fun,
opts.max_heap_size,
opts.max_reductions,
string_io
)
end)
result =
case Task.yield(task, opts.timeout) || Task.shutdown(task) do
{:ok, result} ->
result
nil ->
%Failure{type: :timeout, message: "Execution timeout - #{opts.timeout}ms"}
end
case result do
%Failure{type: :compile_error} -> result
_ -> %{result | stdio: result.stdio <> StringIO.flush(string_io)}
end
end
defp with_string_io(fun) do
{:ok, string_io} = StringIO.open("")
try do
fun.(string_io)
after
StringIO.close(string_io)
end
end
# returns a Dune.Failure struct or exits
defp spawn_trapped_process(fun, max_heap_size, max_reductions, string_io) do
report_to = self()
Process.flag(:trap_exit, true)
# unlike plain spawn / Process.spawn, proc_lib doesn't trigger the logger:
# | Unlike in "plain Erlang", proc_lib processes will not generate error reports,
# | which are written to the terminal by the emulator. All exceptions are converted
# | to exits which are ignored by the default logger handler.
opts = [
:link,
priority: :low,
max_heap_size: %{size: max_heap_size, kill: true, error_logger: false}
]
pid =
:proc_lib.spawn_opt(
fn ->
Process.group_leader(self(), string_io)
fun
|> catch_diagnostics()
|> then(&send(report_to, &1))
end,
opts
)
spawn(fn -> check_max_reductions(pid, report_to, max_reductions) end)
receive do
{:ok, result, diagnostics} ->
Diagnostics.prepend_diagnostics(result, diagnostics)
{:compile_error, error, diagnostics, stacktrace} ->
format_compile_error(error, diagnostics, stacktrace)
{:EXIT, ^pid, reason} ->
case reason do
:normal ->
exit(:normal)
:killed ->
%Failure{type: :memory, message: "Execution stopped - memory limit exceeded"}
{error, stacktrace} ->
format_error(error, stacktrace)
end
{:EXIT, _other_pid, reason} ->
# avoid the process to become immune to parent death
exit(reason)
{:reductions_exceeded, _reductions} ->
%Failure{type: :reductions, message: "Execution stopped - reductions limit exceeded"}
end
end
defp catch_diagnostics(fun) do
{result, diagnostics} =
Diagnostics.with_diagnostics_polyfill(fn ->
try do
{:ok, fun.()}
rescue
err in CompileError ->
{err, __STACKTRACE__}
end
end)
case result do
{:ok, value} ->
{:ok, value, diagnostics}
{%CompileError{} = err, stacktrace} ->
{:compile_error, err, diagnostics, stacktrace}
end
end
defp check_max_reductions(pid, report_to, max_reductions) when is_integer(max_reductions) do
# approach inspired from luerl
# https://github.com/rvirding/luerl/blob/develop/src/luerl_sandbox.erl
case Process.info(pid, :reductions) do
nil ->
:ok
{:reductions, reductions} when reductions > max_reductions ->
# if send immediately, might arrive before an EXIT signal
Process.send_after(report_to, {:reductions_exceeded, reductions}, 1)
{:reductions, _reductions} ->
check_max_reductions(pid, report_to, max_reductions)
end
end
defp format_error(error, stacktrace)
defp format_error({:nocatch, value}, _stacktrace) do
case value do
{:undefined_module, module, fun, arity} -> Failure.undefined_module(module, fun, arity)
{:undefined_function, module, fun, arity} -> Failure.undefined_function(module, fun, arity)
{:safe_throw, thrown} -> %Failure{type: :throw, message: "** (throw) " <> inspect(thrown)}
end
end
defp format_error(error, stacktrace) do
[head | _] = stacktrace
parts =
case head do
{:erl_eval, :do_apply, _, _} -> 3
{:elixir_eval, :__FILE__, _, _} -> 3
_ -> 2
end
message =
{error, [head]}
|> Exception.format_exit()
|> String.split("\n ", parts: parts)
|> Enum.at(1)
# TODO properly pass stacktrace
%Failure{type: :exception, message: message}
end
defp format_compile_error(error, diagnostics, stacktrace) do
message =
{error, stacktrace}
|> Exception.format_exit()
|> String.split("\n ")
|> Enum.at(1)
%Failure{
type: :compile_error,
message: message,
stdio: Diagnostics.format_diagnostics(diagnostics)
}
end
end
================================================
FILE: lib/dune/eval.ex
================================================
defmodule Dune.Eval do
@moduledoc false
alias Dune.{AtomMapping, Success, Failure, Opts}
alias Dune.Eval.Env
alias Dune.Eval.MacroEnv
alias Dune.Parser.SafeAst
alias Dune.Shims
@typep previous_session :: %{:bindings => keyword, :env => Env.t(), optional(any) => any}
@spec run(SafeAst.t() | Failure.t(), Opts.t(), previous_session | nil) ::
Success.t() | Failure.t()
def run(parsed, opts, previous_session \\ nil)
def run(
%SafeAst{
ast: ast,
atom_mapping: atom_mapping,
compile_env: %{allowlist: allowlist},
stdio: parser_stdio
},
opts = %Opts{},
previous_session
) do
case previous_session do
nil ->
env = Env.new(atom_mapping, allowlist)
do_run(ast, atom_mapping, opts, env, nil)
%{bindings: bindings, env: env} ->
env = %{env | atom_mapping: atom_mapping, allowlist: allowlist}
do_run(ast, atom_mapping, opts, env, bindings)
end
|> prepend_parser_stdio(parser_stdio)
end
def run(%Failure{} = failure, _opts, _bindings), do: failure
defp do_run(ast, atom_mapping, opts, env, bindings) do
result =
Dune.Eval.Process.run(
fn ->
safe_eval(ast, env, bindings, opts.pretty, opts.inspect_sort_maps)
end,
opts
)
AtomMapping.replace_in_result(atom_mapping, result)
end
defp prepend_parser_stdio(result, ""), do: result
defp prepend_parser_stdio(result, parser_stdio) do
Map.update!(result, :stdio, &(parser_stdio <> &1))
end
defp safe_eval(safe_ast, env, bindings, pretty, sort_maps) do
try do
inspect_opts = [pretty: pretty, custom_options: [sort_maps: sort_maps]]
do_safe_eval(safe_ast, env, bindings, inspect_opts)
catch
failure = %Failure{} ->
failure
end
end
defp do_safe_eval(safe_ast, env, nil, inspect_opts) do
binding = [env__Dune__: env]
{value, new_env, _new_bindings} = eval_quoted(safe_ast, binding)
%Success{
value: value,
# another important thing about inspect is that it force-evaluates
# potentially huge shared structs => OOM before sending
inspected: Shims.Kernel.safe_inspect(new_env, value, inspect_opts),
stdio: ""
}
end
defp do_safe_eval(safe_ast, env, bindings, inspect_opts) when is_list(bindings) do
binding = [env__Dune__: env] ++ bindings
{value, new_env, new_bindings} = eval_quoted(safe_ast, binding)
%Success{
value: {value, new_env, new_bindings},
inspected: Shims.Kernel.safe_inspect(new_env, value, inspect_opts),
stdio: ""
}
end
defp eval_quoted(safe_ast, binding) do
{value, bindings, _env} = Code.eval_quoted_with_env(safe_ast, binding, MacroEnv.make_env())
{new_env, new_bindings} = Keyword.pop!(bindings, :env__Dune__)
{value, new_env, new_bindings}
end
end
================================================
FILE: lib/dune/failure.ex
================================================
defmodule Dune.Failure do
@moduledoc """
A struct returned when `Dune` parsing or evaluation fails.
Fields:
- `message` (string): the error message to display to the user
- `type` (atom): the nature of the error
- `stdio` (string): captured standard output
"""
@type error_type ::
:restricted
| :module_restricted
| :module_conflict
| :timeout
| :exception
| :compile_error
| :parsing
| :memory
| :reductions
@type t :: %__MODULE__{type: error_type, message: String.t(), stdio: binary}
@enforce_keys [:type, :message]
defstruct @enforce_keys ++ [stdio: ""]
@doc false
def restricted_function(module, fun, arity) do
formatted_fun = format_function(module, fun, arity)
message = "** (DuneRestrictedError) function #{formatted_fun} is restricted"
%__MODULE__{type: :restricted, message: message}
end
@doc false
def undefined_module(module, function, arity) do
base_message = base_undefined_message(module, function, arity)
message =
IO.iodata_to_binary([base_message, "(module ", inspect(module), " is not available)"])
%__MODULE__{type: :exception, message: message}
end
@doc false
def undefined_function(module, function, arity) do
base_message = base_undefined_message(module, function, arity)
message = IO.iodata_to_binary([base_message, "or private"])
%__MODULE__{type: :exception, message: message}
end
defp base_undefined_message(module, function, arity) do
formatted_fun = format_function(module, function, arity)
["** (UndefinedFunctionError) function ", formatted_fun, " is undefined "]
end
defp format_function(kernel, fun, arity) when kernel in [nil, Kernel, Kernel.SpecialForms] do
"#{fun}/#{arity}"
end
defp format_function(module, fun, arity) do
"#{inspect(module)}.#{fun}/#{arity}"
end
end
================================================
FILE: lib/dune/helpers/diagnostics.ex
================================================
defmodule Dune.Helpers.Diagnostics do
@moduledoc false
# used for formatting errors and warnings consistently
@type result_with_stdio :: %{stdio: binary()}
@spec prepend_diagnostics(
result_with_stdio(),
[Code.diagnostic(:warning | :error)]
) :: result_with_stdio()
def prepend_diagnostics(result, []), do: result
def prepend_diagnostics(result, diagnostics) do
%{result | stdio: format_diagnostics(diagnostics) <> "\n\n"}
end
@spec format_diagnostics([Code.diagnostic(:warning | :error)]) :: String.t()
def format_diagnostics(diagnostics) do
Enum.map_join(
diagnostics,
"\n",
&"#{&1.severity}: #{&1.message}\n #{&1.file}:#{format_pos(&1.position)}"
)
end
defp format_pos(integer) when is_integer(integer), do: Integer.to_string(integer)
defp format_pos({line, col}), do: [Integer.to_string(line), ?:, Integer.to_string(col)]
@doc """
A polyfill for `Code.with_diagnostics/1` for older versions of Elixir, which returns
an empty list of diagnostics if not available.
"""
# TODO remove then dropping support for 1.14
if System.version() |> Version.compare("1.15.0") != :lt do
defdelegate with_diagnostics_polyfill(fun), to: Code, as: :with_diagnostics
else
def with_diagnostics_polyfill(fun) do
{fun.(), []}
end
end
end
================================================
FILE: lib/dune/helpers/term_checker.ex
================================================
defmodule Dune.Helpers.TermChecker do
@moduledoc false
defguardp is_simple_term(term)
when is_atom(term) or is_bitstring(term) or is_number(term) or is_reference(term) or
is_function(term) or is_pid(term) or is_port(term) or term == []
@doc """
Walks the term recursively to make sure it is not a humongous tree built using structural sharing
"""
def check(term), do: do_check(term)
defp do_check(term) when is_simple_term(term), do: :ok
defp do_check([left | right]) when is_simple_term(left) do
do_check(right)
end
defp do_check([left | right]) do
do_check(left)
do_check(right)
end
defp do_check(map) when is_map(map) do
Map.to_list(map) |> do_check()
end
defp do_check(tuple) when is_tuple(tuple) do
Tuple.to_list(tuple) |> do_check()
end
end
================================================
FILE: lib/dune/opts.ex
================================================
defmodule Dune.Opts do
@moduledoc """
Defines and validates the options for `Dune`.
The available options are explained below:
### Parsing restriction options
- `atom_pool_size`:
Defines the maximum total number of atoms that can be created.
Must be an integer `>= 0`. Defaults to `5000`.
See the [section below](#module-extra-note-about-atom_pool_size) for more information.
- `max_length`:
Defines the maximum length of code strings that can be parsed.
Defaults to `5000`.
### Execution restriction options
- `allowlist`:
Defines which module and functions are considered safe or restricted.
Should be a module implementing the `Dune.Allowlist` behaviour.
Defaults to `Dune.Allowlist.Default`.
- `max_heap_size`:
Limits the memory usage of the evaluation process using the
[`max_heap_size` flag](https://erlang.org/doc/man/erlang.html#process_flag_max_heap_size).
Should be an integer `> 0`. Defaults to `30_000`.
- `max_reductions`:
Limits the number of CPU cycles of the evaluation process.
The erlang pre-emptive scheduler is using reductions to measure work being done by processes,
which is useful to prevent users to run CPU intensive code such as infinite loops.
Should be an integer `> 0`. Defaults to `30_000`.
- `timeout`:
Limits the time the evaluation process is authorized to run (in milliseconds).
Should be an integer `> 0`. Defaults to `50`.
The evaluation process will still need to parse and execute the sanitized AST, so using
too low limits here would leave only a small margin to actually run user code.
### Other options
- `pretty`:
Use pretty printing when inspecting the result.
Should be a boolean. Defaults to `false`.
- `inspect_sort_maps`:
Sort maps when inspecting the result, useful to keep the output deterministic.
Should be a boolean. Defaults to `false`. Only works since Elixir >= 1.14.4.
### Extra note about `atom_pool_size`
Atoms are reused from one evaluation to the other so the total is not
expected to grow. Atoms will not be leaked.
Also, the atom pool is actually split into several pools: regular atoms, module names,
unused variable names, ...
So defining a value of `100` does not mean that `100` atoms will be available, but
rather `25` of each type.
Atoms being very lightweight, there is no need to use a low value, as long
as there is an upper bound preventing atom leaks.
"""
alias Dune.Allowlist
@type t :: %__MODULE__{
atom_pool_size: non_neg_integer,
max_length: pos_integer,
allowlist: module,
max_heap_size: pos_integer,
max_reductions: pos_integer,
timeout: pos_integer,
pretty: boolean,
inspect_sort_maps: boolean
}
defstruct atom_pool_size: 5000,
max_length: 5000,
allowlist: Dune.Allowlist.Default,
max_heap_size: 50_000,
max_reductions: 30_000,
timeout: 50,
pretty: false,
inspect_sort_maps: false
@doc """
Validates untrusted options from a keyword or a map and returns a `Dune.Opts` struct.
## Examples
iex> Dune.Opts.validate!([])
%Dune.Opts{
allowlist: Dune.Allowlist.Default,
atom_pool_size: 5000,
max_heap_size: 50000,
max_length: 5000,
max_reductions: 30000,
pretty: false,
timeout: 50
}
iex> Dune.Opts.validate!(atom_pool_size: 10)
%Dune.Opts{atom_pool_size: 10, allowlist: Dune.Allowlist.Default}
iex> Dune.Opts.validate!(atom_pool_size: -10)
** (ArgumentError) atom_pool_size should be an integer >= 0
iex> Dune.Opts.validate!(max_length: 0)
** (ArgumentError) atom_pool_size should be an integer > 0
iex> Dune.Opts.validate!(allowlist: DoesNotExists)
** (ArgumentError) could not load module DoesNotExists due to reason :nofile
iex> Dune.Opts.validate!(allowlist: List)
** (ArgumentError) List does not implement the Dune.Allowlist behaviour
iex> Dune.Opts.validate!(max_reductions: 10_000, max_heap_size: 10_000, timeout: 20)
%Dune.Opts{max_heap_size: 10_000, max_reductions: 10_000, timeout: 20}
iex> Dune.Opts.validate!(max_heap_size: 0)
** (ArgumentError) max_heap_size should be an integer > 0
iex> Dune.Opts.validate!(max_reductions: 0)
** (ArgumentError) max_reductions should be an integer > 0
iex> Dune.Opts.validate!(timeout: "55")
** (ArgumentError) timeout should be an integer > 0
iex> Dune.Opts.validate!(pretty: :maybe)
** (ArgumentError) pretty should be a boolean
"""
@spec validate!(Keyword.t() | map) :: t
def validate!(opts) do
struct(__MODULE__, opts) |> do_validate()
end
defp do_validate(%{atom_pool_size: atom_pool_size})
when not (is_integer(atom_pool_size) and atom_pool_size >= 0) do
raise ArgumentError, message: "atom_pool_size should be an integer >= 0"
end
defp do_validate(%{max_length: max_length})
when not (is_integer(max_length) and max_length > 0) do
raise ArgumentError, message: "atom_pool_size should be an integer > 0"
end
defp do_validate(%{allowlist: allowlist}) when not is_atom(allowlist) do
raise ArgumentError, message: "allowlist should be a module"
end
defp do_validate(%{max_reductions: max_reductions})
when not (is_integer(max_reductions) and max_reductions > 0) do
raise ArgumentError, message: "max_reductions should be an integer > 0"
end
defp do_validate(%{max_heap_size: max_heap_size})
when not (is_integer(max_heap_size) and max_heap_size > 0) do
raise ArgumentError, message: "max_heap_size should be an integer > 0"
end
defp do_validate(%{timeout: timeout}) when not (is_integer(timeout) and timeout > 0) do
raise ArgumentError, message: "timeout should be an integer > 0"
end
defp do_validate(%{pretty: pretty}) when not is_boolean(pretty) do
raise ArgumentError, message: "pretty should be a boolean"
end
defp do_validate(%{inspect_sort_maps: sort}) when not is_boolean(sort) do
raise ArgumentError, message: "inspect_sort_maps should be a boolean"
end
defp do_validate(opts = %{allowlist: allowlist}) do
Allowlist.ensure_implements_behaviour!(allowlist)
opts
end
end
================================================
FILE: lib/dune/parser/atom_encoder.ex
================================================
defmodule Dune.Parser.AtomEncoder do
@moduledoc false
alias Dune.AtomMapping
@type atom_category :: :alias | :private_var | :public_var | :other
@atom_categories 4
# TODO Remove when dropping support for Elixir 1.16
extra_modules =
if System.version() |> Version.compare("1.17.0-rc.0") != :lt, do: [Duration], else: []
@elixir_modules [
Kernel,
Kernel.SpecialForms,
Atom,
Base,
Bitwise,
Date,
DateTime,
Duration,
Exception,
Float,
Function,
Integer,
Module,
NaiveDateTime,
Record,
Regex,
String,
Time,
Tuple,
URI,
Version,
Version.Requirement,
Access,
Date.Range,
Enum,
Keyword,
List,
Map,
MapSet,
Range,
Stream,
File,
File.Stat,
File.Stream,
IO,
IO.ANSI,
IO.Stream,
OptionParser,
Path,
Port,
StringIO,
System,
Calendar,
Calendar.ISO,
Calendar.TimeZoneDatabase,
Calendar.UTCOnlyTimeZoneDatabase,
Agent,
Application,
Config,
Config.Provider,
Config.Reader,
DynamicSupervisor,
GenServer,
Node,
Process,
Registry,
Supervisor,
Task,
Task.Supervisor,
Collectable,
Enumerable,
Inspect,
Inspect.Algebra,
Inspect.Opts,
List.Chars,
Protocol,
String.Chars,
Code,
Kernel.ParallelCompiler,
Macro,
Macro.Env,
Behaviour,
Dict,
GenEvent,
HashDict,
HashSet,
Set,
Supervisor.Spec,
ArgumentError,
ArithmeticError,
BadArityError,
BadBooleanError,
BadFunctionError,
BadMapError,
BadStructError,
CaseClauseError,
Code.LoadError,
CompileError,
CondClauseError,
Enum.EmptyError,
Enum.OutOfBoundsError,
ErlangError,
File.CopyError,
File.Error,
File.LinkError,
File.RenameError,
FunctionClauseError,
IO.StreamError,
Inspect.Error,
KeyError,
MatchError,
Module.Types.Error,
OptionParser.ParseError,
Protocol.UndefinedError,
Regex.CompileError,
RuntimeError,
SyntaxError,
SystemLimitError,
TokenMissingError,
TryClauseError,
DuneRestrictedError,
UnicodeConversionError,
Version.InvalidRequirementError,
Version.InvalidVersionError,
WithClauseError
] ++ extra_modules
@module_reprs @elixir_modules
|> Enum.flat_map(&Module.split/1)
|> Map.new(&{&1, String.to_existing_atom(&1)})
@spec load_atom_mapping(AtomMapping.t() | nil) :: :ok
def load_atom_mapping(nil), do: :ok
def load_atom_mapping(%AtomMapping{atoms: atoms}) do
count = Enum.count(atoms)
Process.put(:__Dune_atom_count__, count)
Enum.each(atoms, fn {atom, binary} ->
Process.put({:__Dune_atom__, binary}, atom)
end)
end
@spec static_atoms_encoder(String.t(), non_neg_integer()) :: {:ok, atom} | {:error, String.t()}
def static_atoms_encoder(binary, pool_size)
when is_binary(binary) and is_integer(pool_size) do
case @module_reprs do
%{^binary => atom} ->
{:ok, atom}
_ ->
if binary =~ "Dune" do
{:error, "Atoms containing `Dune` are restricted for safety"}
else
atom_category = categorize_atom_binary(binary)
do_static_atoms_encoder(binary, atom_category, pool_size)
end
end
end
@spec categorize_atom_binary(binary) :: atom_category
def categorize_atom_binary(atom_binary) do
charlist = String.to_charlist(atom_binary)
case {Code.Fragment.cursor_context(charlist), atom_binary} do
{{:alias, ^charlist}, _} -> :alias
{{:local_or_var, ^charlist}, "_" <> _} -> :private_var
{{:local_or_var, ^charlist}, _} -> :public_var
_ -> :other
end
end
defp do_static_atoms_encoder("Elixir." <> rest, :alias, pool_size) do
rest
|> String.split(".")
|> encode_many_atoms(pool_size, [])
end
defp do_static_atoms_encoder(binary, atom_category, pool_size) do
process_key = {:__Dune_atom__, binary}
case Process.get(process_key, nil) do
nil -> do_static_atoms_encoder(binary, atom_category, process_key, pool_size)
atom when is_atom(atom) -> {:ok, atom}
end
end
defp do_static_atoms_encoder(binary, atom_category, process_key, pool_size) do
{:ok, String.to_existing_atom(binary)}
rescue
ArgumentError ->
case new_atom(atom_category, pool_size) do
{:ok, atom} ->
Process.put(process_key, atom)
if atom_category == :other do
Process.put({:__Dune_atom_extra_info__, atom}, :wrapped)
end
{:ok, atom}
{:error, error} ->
{:error, error}
end
end
defp encode_many_atoms([], _pool_size, acc) do
{:ok, {:__aliases__, [], [Elixir | Enum.reverse(acc)]}}
end
defp encode_many_atoms([head | tail], pool_size, acc) do
case do_static_atoms_encoder(head, :alias, pool_size) do
{:ok, atom} -> encode_many_atoms(tail, pool_size, [atom | acc])
{:error, error} -> {:error, error}
end
end
@spec plain_atom_mapping :: AtomMapping.t()
def plain_atom_mapping() do
atoms =
for {{:__Dune_atom__, binary}, atom} <- Process.get() do
{atom, binary}
end
extra_info =
for {{:__Dune_atom_extra_info__, atom}, info} <- Process.get() do
{atom, info}
end
AtomMapping.from_atoms(atoms, extra_info)
end
defp new_atom(atom_category, pool_size) do
count = Process.get(:__Dune_atom_count__, 0) + 1
if count * @atom_categories > pool_size do
{:error, "atom_pool_size exceeded, failed to parse atom"}
else
Process.put(:__Dune_atom_count__, count)
atom = do_new_atom(atom_category, count)
{:ok, atom}
end
end
defp do_new_atom(:alias, count) do
:"Dune_Atom_#{count}__"
end
defp do_new_atom(:public_var, count) do
:"a__Dune_atom_#{count}__"
end
defp do_new_atom(category, count) when category in [:private_var, :other] do
:"__Dune_atom_#{count}__"
end
@spec encode_modules(Macro.t(), AtomMapping.t(), AtomMapping.t() | nil) ::
{Macro.t(), AtomMapping.t()}
def encode_modules(ast, plain_atom_mapping, existing_mapping) do
initial_acc = get_module_acc(existing_mapping)
{new_ast, acc} =
Macro.postwalk(ast, initial_acc, fn
{:__aliases__, ctx, atoms}, acc ->
{modules, new_acc} = remove_elixir_prefix(atoms) |> map_modules_ast(acc)
{{:__aliases__, ctx, modules}, new_acc}
other, acc ->
{other, acc}
end)
atom_mapping = build_module_mapping(acc, plain_atom_mapping)
{new_ast, atom_mapping}
end
defp get_module_acc(nil), do: %{}
defp get_module_acc(%AtomMapping{atoms: atoms, modules: modules}) do
reverse_atoms = Map.new(atoms, fn {atom, string} -> {string, atom} end)
Map.new(modules, fn {atom, string} ->
atoms = String.split(string, ".") |> Enum.map(&Map.fetch!(reverse_atoms, &1))
{atoms, atom}
end)
end
defp remove_elixir_prefix(atoms = [Elixir, Elixir | _]), do: atoms
defp remove_elixir_prefix([Elixir | atoms]) when atoms != [], do: atoms
defp remove_elixir_prefix(atoms), do: atoms
defp map_modules_ast(atoms, acc) do
case acc do
%{^atoms => module_name} ->
{[module_name], acc}
_ ->
try do
atoms |> Enum.join(".") |> then(&"Elixir.#{&1}") |> String.to_existing_atom()
rescue
ArgumentError ->
module_name = :"Dune_Module_#{map_size(acc) + 1}__"
{[module_name], Map.put(acc, atoms, module_name)}
else
_ ->
{atoms, acc}
end
end
end
defp build_module_mapping(acc, plain_atom_mapping) do
modules =
Enum.map(acc, fn {atoms, module_name} ->
string = Enum.map_join(atoms, ".", &AtomMapping.to_string(plain_atom_mapping, &1))
module = Module.concat([module_name])
{module, string}
end)
AtomMapping.add_modules(plain_atom_mapping, modules)
end
end
================================================
FILE: lib/dune/parser/compile_env.ex
================================================
defmodule Dune.Parser.CompileEnv do
@moduledoc false
@type name_arity :: {atom, non_neg_integer}
@type maybe_fake_module :: {:real | :fake, module}
@type t :: %__MODULE__{
module: module | nil,
allowlist: module,
fake_modules: %{optional(module) => %{optional(name_arity) => :def | :defp}}
# aliases
# struct info
# requires
}
@enforce_keys [:module, :allowlist, :fake_modules]
defstruct @enforce_keys
def new(allowlist) do
%__MODULE__{
allowlist: allowlist,
module: nil,
fake_modules: %{}
}
end
def define_fake_module(env = %__MODULE__{fake_modules: fake_modules}, module, name_arities)
when is_atom(module) and is_map(name_arities) do
if module_already_exists?(module, fake_modules) do
throw({:module_conflict, module})
end
new_modules = Map.put(fake_modules, module, name_arities)
%{env | fake_modules: new_modules}
end
defp module_already_exists?(module, fake_modules) do
case fake_modules do
%{^module => _conflict} -> true
_ -> Code.ensure_loaded?(module)
end
end
def resolve_mfa(%__MODULE__{}, module, fun_name, arity)
when module in [Kernel, nil] and fun_name in [:def, :defp] and arity in [1, 2] do
:outside_module
end
def resolve_mfa(env = %__MODULE__{}, module, fun_name, arity)
when is_atom(module) and is_atom(fun_name) and is_integer(arity) do
actual_module = resolve_module(module, fun_name, arity)
case env.allowlist.fun_status(actual_module, fun_name, arity) do
:undefined_module ->
resolve_fake_module(env, module, fun_name, arity)
:undefined_function ->
case module do
nil -> resolve_fake_module(env, nil, fun_name, arity)
_ -> :undefined_function
end
:restricted ->
{:restricted, actual_module}
other ->
other
end
end
defp resolve_module(nil, fun_name, arity) do
if Macro.special_form?(fun_name, arity) do
Kernel.SpecialForms
else
Kernel
end
end
defp resolve_module(module, _fun_name, _arity), do: module
defp resolve_fake_module(%{module: nil}, nil, _fun_name, _arity), do: :undefined_function
defp resolve_fake_module(env = %{module: module}, nil, fun_name, arity) do
resolve_fake_module(env, module, fun_name, arity)
end
defp resolve_fake_module(env, module, fun_name, arity) do
# TODO check current module to know if defp OK
fun_with_arity = {fun_name, arity}
case env.fake_modules do
%{^module => %{^fun_with_arity => def_or_defp}} -> check_private(env, module, def_or_defp)
_ -> :undefined_module
end
end
defp check_private(_env, module, :def), do: {:fake, module}
defp check_private(%{module: module}, module, :defp), do: {:fake, module}
defp check_private(_env, _module, _def), do: :undefined_function
end
================================================
FILE: lib/dune/parser/debug.ex
================================================
defmodule Dune.Parser.Debug do
@moduledoc false
def io_debug(ast) do
debug(ast) |> IO.puts()
ast
end
def debug(%{ast: ast}) when is_tuple(ast) do
ast_to_string(ast)
end
def debug(ast) when is_tuple(ast) do
ast_to_string(ast)
end
defp ast_to_string({:__block__, _, list}) do
Enum.map_join(list, "\n", &Macro.to_string/1)
end
defp ast_to_string(ast) do
Macro.to_string(ast)
end
end
================================================
FILE: lib/dune/parser/real_module.ex
================================================
defmodule Dune.Parser.RealModule do
@moduledoc false
@spec elixir_module?(module) :: boolean
def elixir_module?(module) do
module
|> Atom.to_string()
|> String.starts_with?("Elixir.")
end
def list_functions(module)
def list_functions(Kernel.SpecialForms) do
[{:%{}, 2}] ++ Kernel.SpecialForms.__info__(:macros)
end
@spec list_functions(module) :: [{atom, non_neg_integer}]
def list_functions(module) when is_atom(module) do
if elixir_module?(module) do
module.__info__(:functions) ++ module.__info__(:macros)
else
for {f, _a} = fa <- module.module_info(:exports), f != :module_info, do: fa
end
end
def fun_exists?(module, fun_name, arity) do
# TODO replace with fun_status
fun_status(module, fun_name, arity) == :defined
end
def fun_status(module, fun_name, arity)
def fun_status(Kernel.SpecialForms, fun_name, arity) do
if Macro.special_form?(fun_name, arity) do
:defined
else
:undefined_function
end
end
def fun_status(module, fun_name, arity) do
cond do
not Code.ensure_loaded?(module) -> :undefined_module
function_exported?(module, fun_name, arity) -> :defined
macro_exported?(module, fun_name, arity) -> :defined
true -> :undefined_function
end
end
end
================================================
FILE: lib/dune/parser/safe_ast.ex
================================================
defmodule Dune.Parser.SafeAst do
@moduledoc false
@type t :: %__MODULE__{
ast: Macro.t(),
atom_mapping: Dune.AtomMapping.t(),
compile_env: Dune.Parser.CompileEnv.t(),
stdio: binary()
}
@enforce_keys [:ast, :atom_mapping, :compile_env]
defstruct @enforce_keys ++ [stdio: <<>>]
end
================================================
FILE: lib/dune/parser/sanitizer.ex
================================================
defmodule Dune.Parser.Sanitizer do
@moduledoc false
alias Dune.{Failure, AtomMapping, Opts}
alias Dune.Parser.{CompileEnv, RealModule, UnsafeAst, SafeAst}
@env_variable_name :env__Dune__
@spec sanitize(UnsafeAst.t() | Failure.t(), Opts.t()) :: SafeAst.t() | Failure.t()
def sanitize(unsafe = %UnsafeAst{}, compile_env = %CompileEnv{}) do
case try_sanitize(unsafe.ast, compile_env) do
{:ok, safe_ast, new_env} ->
%SafeAst{
ast: safe_ast,
atom_mapping: unsafe.atom_mapping,
compile_env: new_env,
stdio: unsafe.stdio
}
{:restricted, module, fun, arity} ->
failure = Failure.restricted_function(module, fun, arity)
AtomMapping.replace_in_result(unsafe.atom_mapping, failure)
{:undefined_module, module, func_name, arity} ->
failure = Failure.undefined_module(module, func_name, arity)
AtomMapping.replace_in_result(unsafe.atom_mapping, failure)
{:undefined_function, module, func_name, arity} ->
failure = Failure.undefined_function(module, func_name, arity)
AtomMapping.replace_in_result(unsafe.atom_mapping, failure)
{:outside_module, def_or_defp} ->
message = "** (ArgumentError) cannot invoke #{def_or_defp}/2 inside function/macro"
new_failure(:exception, message, unsafe.atom_mapping)
{:module_invalid, module_ast, error_ctx} ->
line = Keyword.get(error_ctx, :line)
name = Macro.to_string(module_ast)
message = "** (Dune.Eval.CompileError) nofile:#{line}: invalid module name: #{name}"
new_failure(:exception, message, unsafe.atom_mapping)
{:module_restricted, ast} ->
message =
"** (DuneRestrictedError) the following syntax is restricted inside defmodule:\n #{Macro.to_string(ast)}"
new_failure(:module_restricted, message, unsafe.atom_mapping)
{:module_conflict, module} ->
message =
"** (DuneRestrictedError) Following module cannot be defined/redefined: #{inspect(module)}"
new_failure(:module_conflict, message, unsafe.atom_mapping)
{:definition_conflict, name_arity, previous_def, previous_ctx, conflict_def, conflict_ctx} ->
conflict_line = Keyword.get(conflict_ctx, :line)
previous_line = Keyword.get(previous_ctx, :line)
{name, arity} = name_arity
message =
"** (Dune.Eval.CompileError) nofile:#{conflict_line}: " <>
"#{conflict_def} #{name}/#{arity} already defined as #{previous_def} in nofile:#{previous_line}"
new_failure(:exception, message, unsafe.atom_mapping)
{:parsing_error, ast} ->
message = "dune parsing error: failed to safe parse\n #{Macro.to_string(ast)}"
new_failure(:parsing, message, unsafe.atom_mapping)
{:bin_modifier_restricted, ast} ->
message =
"** (DuneRestrictedError) bitstring modifier is restricted:\n #{Macro.to_string(ast)}"
new_failure(:restricted, message, unsafe.atom_mapping)
{:bin_modifier_size, max_size} ->
message = "** (DuneRestrictedError) size modifiers above #{max_size} are restricted"
new_failure(:restricted, message, unsafe.atom_mapping)
{:exception, error} ->
message = Exception.format(:error, error)
new_failure(:exception, message, unsafe.atom_mapping)
end
end
def sanitize(%Failure{} = failure, _opts), do: failure
defp new_failure(type, message, atom_mapping) when is_atom(type) and is_binary(message) do
failure = %Failure{type: type, message: message, stdio: ""}
AtomMapping.replace_in_result(atom_mapping, failure)
end
# XXX this is a bit hacky and brute-force approach!
# ideally the AST transformation is robust enough so we don't need it
defp try_sanitize(ast, env) do
do_sanitize_main(ast, env)
rescue
error ->
error
|> then(&Exception.blame(:error, &1, __STACKTRACE__))
|> elem(0)
|> then(&Exception.format(:error, &1))
|> IO.warn()
{:parsing_error, ast}
catch
thrown -> thrown
end
defp do_sanitize_main({:__block__, ctx, list}, env) do
{list_ast, env} = do_sanitize_main_list(list, env)
block_ast = {:__block__, ctx, list_ast}
{:ok, block_ast, env}
end
defp do_sanitize_main(single, env) do
case do_sanitize_main_list([single], env) do
{[safe_single], env} ->
{:ok, safe_single, env}
{list_ast, env} when is_list(list_ast) ->
block_ast = {:__block__, [], list_ast}
{:ok, block_ast, env}
end
end
defp do_sanitize_main_list(list, env) when is_list(list) do
{defmodules, instructions} = Enum.split_with(list, &defmodule_block?/1)
raw_fun_definitions = Enum.map(defmodules, &parse_module_definition/1)
env =
Enum.reduce(raw_fun_definitions, env, fn {module, fun_defs}, acc ->
fun_name_arities =
Map.new(fun_defs, fn {name_arity, [raw_definition | _]} ->
{name_arity, elem(raw_definition, 0)}
end)
CompileEnv.define_fake_module(acc, module, fun_name_arities)
end)
module_definitions = Enum.map(raw_fun_definitions, &sanitize_module_definition(&1, env))
sanitized_instructions =
case {raw_fun_definitions, do_sanitize(instructions, env)} do
{[_ | _], []} ->
{last_module, _} = List.last(raw_fun_definitions)
[quote(do: {:module, unquote(last_module), nil, nil})]
{_, sanitized_instructions} ->
sanitized_instructions
end
{module_definitions ++ sanitized_instructions, env}
end
defp defmodule_block?({:defmodule, _, _}), do: true
defp defmodule_block?(_), do: false
defp parse_module_definition({:defmodule, ctx, [module_name, [do: do_ast]]}) do
module_name
|> validate_module_name(ctx)
|> do_parse_module_definition(do_ast)
end
defp parse_module_definition(ast = {:defmodule, _, _}) do
throw({:parsing_error, ast})
end
defp validate_module_name(module, _ctx)
when is_atom(module) and module not in [nil, false, true] do
module
end
defp validate_module_name(module_def = {:__aliases__, _, [_module_atom]}, _ctx) do
Macro.expand_once(module_def, __ENV__)
end
defp validate_module_name(module_ast, ctx) do
throw({:module_invalid, module_ast, ctx})
end
defp do_parse_module_definition(module_name, do_ast) do
fun_definitions =
block_to_list(do_ast)
|> Enum.map(&parse_fun_definition/1)
|> Enum.filter(& &1)
|> Enum.flat_map(&expand_defaults/1)
|> Enum.group_by(&elem(&1, 0), &elem(&1, 1))
|> tap(&check_definition_conflicts/1)
{module_name, fun_definitions}
end
defp block_to_list({:__block__, _, list}) when is_list(list), do: list
defp block_to_list(single) when is_tuple(single), do: [single]
defp parse_fun_definition({def_or_defp, ctx, [signature, [do: body]]})
when def_or_defp in [:def, :defp] do
{header, guards} = parse_fun_signature(signature)
{name, args} = Macro.decompose_call(header)
{args, defaults} = extract_default_args(args, 0, [], [])
name_arity = {name, length(args)}
definition = {def_or_defp, ctx, args, body, guards}
{name_arity, definition, defaults}
end
defp parse_fun_definition({:@, _, [{doc, _, [value]}]})
when doc in ~w[moduledoc doc]a and (value == false or is_binary(value)) do
nil
end
defp parse_fun_definition({:@, _, [{typespec, _, [{:"::", _, _}]}]})
when typespec in ~w[spec type typep opaque]a do
nil
end
defp parse_fun_definition(unsupported_ast) do
throw({:module_restricted, unsupported_ast})
end
# TODO else raise unsupported!
defp extract_default_args([], _index, arg_acc, defaults) do
{Enum.reverse(arg_acc), defaults}
end
defp extract_default_args([{:\\, _, [arg, default]} | args], index, arg_acc, defaults) do
extract_default_args(args, index + 1, [arg | arg_acc], [{index, default} | defaults])
end
defp extract_default_args([arg | args], index, arg_acc, defaults) do
extract_default_args(args, index + 1, [arg | arg_acc], defaults)
end
defp expand_defaults({name_arity, definition, _defaults = []}) do
[{name_arity, definition}]
end
defp expand_defaults({name_arity = {name, arity}, definition, defaults}) do
def_or_defp = elem(definition, 0)
do_expand_defaults(name, arity, def_or_defp, defaults, [{name_arity, definition}])
end
defp do_expand_defaults(_name, _arity, _def_or_defp, [], acc) do
acc
end
defp do_expand_defaults(
name,
arity,
def_or_defp,
[{default_index, default_value} | defaults],
acc
) do
args = Macro.generate_arguments(arity, nil)
arity = arity - 1
args_without_default = List.delete_at(args, default_index)
args_in_expr =
Enum.with_index(args, fn
_arg, ^default_index -> default_value
arg, _index -> arg
end)
definition = {def_or_defp, [], args_without_default, {name, [], args_in_expr}, nil}
acc = [{{name, arity}, definition} | acc]
do_expand_defaults(name, arity, def_or_defp, defaults, acc)
end
defp check_definition_conflicts(grouped_definitions) do
Enum.each(grouped_definitions, fn {name_arity, [head | tail]} ->
check_definition_conflict(name_arity, head, tail)
end)
end
defp check_definition_conflict(_name_arity, _, []), do: :ok
defp check_definition_conflict(
name_arity,
head = {def_or_defp, _, _, _, _},
[
{def_or_defp, _, _, _, _} | rest
]
) do
check_definition_conflict(name_arity, head, rest)
end
defp check_definition_conflict(name_arity, {previous_def, previous_ctx, _, _, _}, [
{conflict_def, conflict_ctx, _, _, _} | _
]) do
throw(
{:definition_conflict, name_arity, previous_def, previous_ctx, conflict_def, conflict_ctx}
)
end
defp parse_fun_signature({:when, _, [header, guards]}) do
{header, guards}
end
defp parse_fun_signature(header) do
{header, nil}
end
defp sanitize_module_definition({module, fun_defs}, env) do
env = %{env | module: module}
public_funs_ast =
fun_defs
|> Enum.map(&sanitize_fun(&1, env))
|> Enum.group_by(&elem(&1, 0), fn {_fun, arity, ast} -> {arity, ast} end)
|> Enum.map(fn {fun_name, list} ->
{fun_name, to_map_ast(list)}
end)
|> to_map_ast()
quote do
unquote(env_variable()) =
Dune.Eval.Env.add_module(
unquote(env_variable()),
unquote(module),
%Dune.Eval.FakeModule{public_funs: unquote(public_funs_ast)}
)
end
end
defp to_map_ast(list_ast) when is_list(list_ast), do: {:%{}, [], list_ast}
defp sanitize_fun({{fun_name, arity}, definitions}, env) do
args = Macro.var(:args, nil)
bottom_clause =
quote do
args ->
raise %Dune.Eval.FunctionClauseError{
module: unquote(env.module),
function: unquote(fun_name),
args: args
}
end
clauses = Enum.map(definitions, &sanitize_fun_clause(&1, env)) ++ bottom_clause
env_var = env_variable_if_used(clauses)
anonymous_ast =
{:fn, [],
[
{:->, [],
[
[env_var, args],
{:case, [],
[
args,
[do: clauses]
]}
]}
]}
{fun_name, arity, anonymous_ast}
end
defp sanitize_fun_clause({_def_or_defp, ctx, args, body, guards}, env) do
safe_args = do_sanitize(args, env)
safe_body = do_sanitize(body, env)
safe_guards = do_sanitize(guards, env)
definition_to_clause(ctx, safe_args, safe_body, safe_guards)
end
defp definition_to_clause(ctx, args, body, _guards = nil) do
{:->, ctx, [[args], body]}
end
defp definition_to_clause(ctx, args, body, guards) when is_tuple(guards) do
args_and_guards = [{:when, [], [args, guards]}]
{:->, ctx, [args_and_guards, body]}
end
defp env_variable_if_used(asts) do
uses_variable?(asts, @env_variable_name)
case uses_variable?(asts, @env_variable_name) do
true -> env_variable()
false -> underscore_env_variable()
end
end
defp uses_variable?([], _variable_name), do: false
defp uses_variable?([head | tail], variable_name) do
case uses_variable?(head, variable_name) do
true -> true
false -> uses_variable?(tail, variable_name)
end
end
defp uses_variable?({variable_name, _, nil}, variable_name), do: true
defp uses_variable?({_, _, list}, variable_name) when is_list(list) do
uses_variable?(list, variable_name)
end
defp uses_variable?({x, y}, variable_name) do
uses_variable?(x, variable_name) or uses_variable?(y, variable_name)
end
defp uses_variable?(_ast, _variable_name), do: false
defp do_sanitize(ast, env)
defp do_sanitize(raw_value, _env)
when is_atom(raw_value) or is_number(raw_value) or is_binary(raw_value) do
raw_value
end
defp do_sanitize(list, env)
when is_list(list) do
sanitize_args(list, env)
end
defp do_sanitize({arg1, arg2}, env) do
[safe_arg1, safe_arg2] = sanitize_args([arg1, arg2], env)
{safe_arg1, safe_arg2}
end
defp do_sanitize({atom, _, _} = raw, env) when atom in [:__block__, :when, :<-, :->, :|] do
sanitize_args_in_node(raw, env)
end
defp do_sanitize({name, _, atom} = variable, _env)
when is_atom(name) and atom in [nil, Elixir] do
unless authorized_var_name?(name) do
throw({:restricted, Kernel, name, 0})
end
variable
end
defp do_sanitize({:&, _, args}, env) do
[ast] = args
sanitize_capture(ast, env)
end
defp do_sanitize({:<<>>, meta, args}, env) do
sanitized_args =
Enum.map(args, fn
{:"::", meta, [expr, modifier]} ->
{:"::", meta, [do_sanitize(expr, env), check_bin_modifier(modifier)]}
arg ->
do_sanitize(arg, env)
end)
{:<<>>, meta, sanitized_args}
end
defp do_sanitize({{:., _, [left, right]}, ctx, args} = raw, env)
when is_atom(right) and is_list(args) do
case left do
atom when is_atom(atom) ->
do_sanitize_function(raw, env)
{:__aliases__, _, list} when is_list(list) ->
do_sanitize_function(raw, env)
_ ->
do_sanitize_dot(left, right, args, ctx, env)
end
end
defp do_sanitize({{:., dot_ctx, [{fn_or_ampersand, _, _} = anonymous]}, ctx, args}, env)
when fn_or_ampersand in [:fn, :&] do
safe_anonymous = do_sanitize(anonymous, env)
safe_args = sanitize_args(args, env)
{{:., dot_ctx, [safe_anonymous]}, ctx, safe_args}
end
defp do_sanitize({:dbg, meta, [expr]}, env) do
quote do
value = unquote(do_sanitize(expr, env))
IO.puts([
"[nofile:",
to_string(unquote(meta[:line])),
": (file)]\n",
unquote(Macro.to_string(expr)),
" #=> ",
inspect(value, pretty: true),
?\n
])
value
end
end
defp do_sanitize({:|>, _, [expr, {:dbg, meta, []}]}, env) do
header =
quote do
["[nofile:", to_string(unquote(meta[:line])), ": (file)]\n"]
end
quote do
value = unquote(dbg_pipeline(expr, env, header))
IO.write("\n")
value
end
end
defp do_sanitize({:|>, _, _} = ast, env) do
case try_expand_once(ast) do
{atom, _, _} = expanded when atom != :|> ->
do_sanitize(expanded, env)
end
end
defp do_sanitize({_, _, args} = raw, env) when is_list(args) do
do_sanitize_function(raw, env)
end
defp try_expand_once(ast) do
Macro.expand_once(ast, __ENV__)
rescue
error ->
throw({:exception, error})
end
defp do_sanitize_dot(left, key, args, ctx, env) do
safe_left = do_sanitize(left, env)
safe_args = sanitize_args(args, env)
if args == [] and {:no_parens, true} in ctx do
quote do
Dune.Shims.Kernel.safe_dot(
unquote(env_variable()),
unquote(safe_left),
unquote(key)
)
end
else
quote do
Dune.Shims.Kernel.safe_apply(
unquote(env_variable()),
unquote(safe_left),
unquote(key),
unquote(safe_args)
)
end
end
end
defp do_sanitize_function({func, ctx, atom}, env) when atom in [nil, Elixir] do
do_sanitize_function({func, ctx, []}, env)
end
defp do_sanitize_function({{:., _, [{_variable_function, _, atom}]}, _, args} = raw, env)
when atom in [nil, Elixir] and is_list(args) do
sanitize_args_in_node(raw, env)
end
defp do_sanitize_function({func, _, args} = raw, env)
when is_list(args) do
{module, func_name} = extract_module_and_fun(func)
arity = length(args)
case CompileEnv.resolve_mfa(env, module, func_name, arity) do
{:restricted, resolved_module} ->
throw({:restricted, resolved_module, func_name, arity})
{:shimmed, shim_module, shim_func} ->
safe_args = sanitize_args(args, env)
quote do
unquote(shim_module).unquote(shim_func)(
unquote(env_variable()),
unquote_splicing(safe_args)
)
end
{:fake, fake_module} ->
safe_args = sanitize_args(args, env)
quote do
Dune.Eval.Env.apply_fake(
unquote(env_variable()),
unquote(fake_module),
unquote(func_name),
unquote(safe_args)
)
end
:allowed ->
sanitize_args_in_node(raw, env)
error ->
handle_mfa_error(error, module, func_name, arity)
end
end
defp extract_module_and_fun({:., _, [{:__aliases__, _, modules}, func_name]}) do
{modules |> Module.concat(), func_name}
end
defp extract_module_and_fun({:., _, [erlang_module, func_name]}) when is_atom(erlang_module) do
{erlang_module, func_name}
end
defp extract_module_and_fun(func_name) when is_atom(func_name) do
{nil, func_name}
end
defp sanitize_capture({:/, _, [{func, _, _}, arity]} = raw, env) when is_integer(arity) do
{module, func_name} = extract_module_and_fun(func)
case CompileEnv.resolve_mfa(env, module, func_name, arity) do
{:restricted, resolved_module} ->
throw({:restricted, resolved_module, func_name, arity})
{:fake, fake_module} ->
args = Macro.generate_unique_arguments(arity, nil)
quote do
fn unquote_splicing(args) ->
Dune.Eval.Env.apply_fake(
unquote(env_variable()),
unquote(fake_module),
unquote(func_name),
unquote(args)
)
end
end
{:shimmed, shim_module, shim_func} ->
args = Macro.generate_unique_arguments(arity, nil)
quote do
# FIXME pass env here!
fn unquote_splicing(args) ->
unquote(shim_module).unquote(shim_func)(
unquote(env_variable()),
unquote_splicing(args)
)
end
end
:allowed ->
{:&, [], [raw]}
error ->
handle_mfa_error(error, module, func_name, arity)
end
end
defp handle_mfa_error(:undefined_module, module, func_name, arity) do
throw({:undefined_module, module, func_name, arity})
end
defp handle_mfa_error(:undefined_function, module, func_name, arity) do
throw({:undefined_function, module, func_name, arity})
end
defp handle_mfa_error(:outside_module, _module, func_name, _arity)
when func_name in [:def, :defp] do
throw({:outside_module, func_name})
end
defp sanitize_capture(capture_arg, env) do
safe_capture_arg = do_sanitize(capture_arg, env)
{:&, [], [safe_capture_arg]}
end
defp sanitize_args(args, env) when is_list(args) do
Enum.map(args, &do_sanitize(&1, env))
end
defp sanitize_args_in_node({_, _, args} = raw, env) when is_list(args) do
safe_args = sanitize_args(args, env)
put_elem(raw, 2, safe_args)
end
defp dbg_pipeline({:|>, _, [left, {fun, meta, args} = right]}, env, header)
when is_list(args) and fun != :dbg do
ast_with_placeholder = do_sanitize({fun, meta, [:__DUNE_RESERVED__ | args]}, env)
ast =
Macro.prewalk(ast_with_placeholder, fn
:__DUNE_RESERVED__ -> dbg_pipeline(left, env, header)
other -> other
end)
quote do
value = unquote(ast)
IO.puts([
"|> ",
unquote(Macro.to_string(right)),
" #=> ",
inspect(value, pretty: true)
])
value
end
end
defp dbg_pipeline(expr, env, header) do
quote do
value = unquote(do_sanitize(expr, env))
IO.puts([
unquote_splicing(header),
unquote(Macro.to_string(expr)),
" #=> ",
inspect(value, pretty: true)
])
value
end
end
@max_segment_size 256
@binary_modifiers [:binary, :bytes]
@allowed_modifiers [:integer, :float, :bits, :bitstring, :utf8, :utf16, :utf32] ++
[:signed, :unsigned, :little, :big, :native]
defp check_bin_modifier(modifier) do
{size, unit} = check_bin_modifier_size(modifier, 8, nil)
unit = unit || 1
if size * unit > @max_segment_size do
throw({:bin_modifier_size, @max_segment_size})
end
modifier
end
defp check_bin_modifier_size({:-, _, [left, right]}, size, unit) do
{size, unit} = check_bin_modifier_size(left, size, unit)
check_bin_modifier_size(right, size, unit)
end
defp check_bin_modifier_size(modifier, size, unit) do
case modifier do
new_size when is_integer(new_size) ->
{new_size, unit}
{:size, _, [new_size]} when is_integer(new_size) ->
{new_size, unit}
{:unit, _, [new_unit]} when is_integer(new_unit) ->
{size, new_unit}
{:*, _, [new_size, new_unit]} when is_integer(new_size) and is_integer(new_unit) ->
{new_size, new_unit}
{:size, _, [{:^, _, [{var, _, ctx}]}]} when is_atom(var) and is_atom(ctx) ->
{size, unit}
{atom, _, ctx} when atom in @binary_modifiers and is_atom(ctx) ->
{size, unit || 8}
{atom, _, ctx} when atom in @allowed_modifiers and is_atom(ctx) ->
{size, unit}
other ->
throw({:bin_modifier_restricted, other})
end
end
defp env_variable do
Macro.var(@env_variable_name, nil)
end
defp underscore_env_variable do
Macro.var(:_env__Dune__, nil)
end
defp authorized_var_name?(name) do
# e.g. recompile could be interpreted as recompile/0
not (RealModule.fun_exists?(Kernel, name, 0) or Macro.special_form?(name, 0))
end
end
================================================
FILE: lib/dune/parser/string_parser.ex
================================================
defmodule Dune.Parser.StringParser do
@moduledoc false
alias Dune.{AtomMapping, Failure, Opts}
alias Dune.Helpers.Diagnostics
alias Dune.Parser.{AtomEncoder, UnsafeAst}
@typep previous_session :: %{atom_mapping: AtomMapping.t()}
# TODO options: parse timeout & max atoms
@spec parse_string(String.t(), Opts.t(), previous_session | nil, boolean) ::
UnsafeAst.t() | Failure.t()
def parse_string(string, opts, previous_session, encode_modules? \\ true)
when is_binary(string) and is_boolean(encode_modules?) do
# import: do in a different process because the AtomEncoder pollutes the Process dict
fn -> do_parse_string(string, opts, previous_session, encode_modules?) end
|> Task.async()
|> Task.await()
end
defp do_parse_string(
string,
%Opts{atom_pool_size: pool_size},
previous_session,
encode_modules?
) do
maybe_load_atom_mapping(previous_session)
encoder = fn binary, _ctx -> AtomEncoder.static_atoms_encoder(binary, pool_size) end
{result, diagnostics} =
Diagnostics.with_diagnostics_polyfill(fn ->
Code.string_to_quoted(string, static_atoms_encoder: encoder, existing_atoms_only: true)
end)
case result do
{:ok, ast} ->
maybe_encode_modules(ast, previous_session, encode_modules?)
|> Diagnostics.prepend_diagnostics(diagnostics)
{:error, {_ctx, error, token}} ->
handle_failure(error, token)
end
end
defp maybe_load_atom_mapping(nil), do: :ok
defp maybe_load_atom_mapping(%{atom_mapping: atom_mapping}) do
AtomEncoder.load_atom_mapping(atom_mapping)
end
defp maybe_encode_modules(ast, previous_session, encode_modules?) do
plain_atom_mapping = AtomEncoder.plain_atom_mapping()
{new_ast, atom_mapping} =
if encode_modules? do
existing_mapping = previous_session[:atom_mapping]
AtomEncoder.encode_modules(ast, plain_atom_mapping, existing_mapping)
else
{ast, plain_atom_mapping}
end
%UnsafeAst{ast: new_ast, atom_mapping: atom_mapping}
end
defp handle_failure("Atoms containing" <> _ = error, token) do
%Failure{message: error <> token, type: :restricted}
end
defp handle_failure(error, token) do
failure = do_handle_failure(error, token)
AtomEncoder.plain_atom_mapping()
|> AtomMapping.replace_in_result(failure)
end
defp do_handle_failure({error, explanation}, token)
when is_binary(error) and is_binary(explanation) do
message = IO.iodata_to_binary([error, token, explanation])
%Failure{message: message, type: :parsing}
end
defp do_handle_failure(error, token) when is_binary(error) do
%Failure{message: error <> token, type: :parsing}
end
end
================================================
FILE: lib/dune/parser/unsafe_ast.ex
================================================
defmodule Dune.Parser.UnsafeAst do
@moduledoc false
@type t :: %__MODULE__{
ast: Macro.t(),
atom_mapping: Dune.AtomMapping.t(),
stdio: binary()
}
@enforce_keys [:ast, :atom_mapping]
defstruct @enforce_keys ++ [stdio: <<>>]
end
================================================
FILE: lib/dune/parser.ex
================================================
defmodule Dune.Parser do
@moduledoc false
alias Dune.{AtomMapping, Success, Failure, Opts}
alias Dune.Parser.{CompileEnv, StringParser, Sanitizer, SafeAst, UnsafeAst}
@typep previous_session :: %{
atom_mapping: AtomMapping.t(),
compile_env: Dune.Parser.CompileEnv.t()
}
@spec parse_string(String.t(), Opts.t(), previous_session | nil) :: SafeAst.t() | Failure.t()
def parse_string(string, opts = %Opts{}, previous_session \\ nil) when is_binary(string) do
compile_env = get_compile_env(opts, previous_session)
string
|> do_parse_string(opts, previous_session)
|> Sanitizer.sanitize(compile_env)
end
defp do_parse_string(string, opts = %{max_length: max_length}, previous_session) do
case String.length(string) do
length when length > max_length ->
%Failure{type: :parsing, message: "max code length exceeded: #{length} > #{max_length}"}
_ ->
StringParser.parse_string(string, opts, previous_session)
end
end
@spec parse_quoted(Macro.t(), Opts.t(), previous_session | nil) :: SafeAst.t()
def parse_quoted(quoted, opts = %Opts{}, previous_session \\ nil) do
compile_env = get_compile_env(opts, previous_session)
quoted
|> unsafe_quoted()
|> Sanitizer.sanitize(compile_env)
end
def unsafe_quoted(ast) do
%UnsafeAst{ast: ast, atom_mapping: AtomMapping.new()}
end
defp get_compile_env(opts, nil) do
CompileEnv.new(opts.allowlist)
end
defp get_compile_env(opts, %{compile_env: compile_env}) do
%{compile_env | allowlist: opts.allowlist}
end
@spec string_to_quoted(String.t(), Opts.t()) :: Success.t() | Failure.t()
def string_to_quoted(string, opts) do
with unsafe = %UnsafeAst{} <- StringParser.parse_string(string, opts, nil, false) do
inspected = inspect(unsafe.ast, pretty: opts.pretty)
inspected = AtomMapping.replace_in_string(unsafe.atom_mapping, inspected)
%Success{
value: unsafe.ast,
inspected: inspected,
stdio: unsafe.stdio
}
end
end
end
================================================
FILE: lib/dune/session.ex
================================================
defmodule Dune.Session do
@moduledoc """
Sessions provide a way to evaluate code and keep state (bindings, modules...) between evaluations.
- Use `Dune.eval_string/2` to execute code as a one-off
- Use `Dune.Session.eval_string/3` to execute consecutive code blocks
`Dune.Session` could be used to implement something like a safe IEx shell, or to compile a module
once and call it several times without the overhead of parsing.
`Dune.Session` is also a struct that is used to store the state of an evaluation.
Only the following fields are public:
- `last_result`: contains the result of the last evaluation, or `nil` for empty sessions
Other fields are private and shouldn't be accessed directly.
"""
alias Dune.{Allowlist, Eval, Parser, Success, Failure, Opts}
@opaque private_env :: Eval.Env.t()
@opaque private_compile_env :: Parser.CompileEnv.t()
@typedoc """
The type of a `Dune.Session`.
"""
@type t :: %__MODULE__{
last_result: nil | Success.t() | Failure.t(),
env: private_env,
compile_env: private_compile_env,
bindings: keyword
}
@enforce_keys [:env, :compile_env, :bindings, :last_result]
defstruct @enforce_keys
@default_env Eval.Env.new(Dune.AtomMapping.new(), Allowlist.Default)
@default_compile_env Parser.CompileEnv.new(Allowlist.Default)
@doc """
Returns a new empty session.
## Examples
iex> Dune.Session.new()
#Dune.Session<last_result: nil, ...>
"""
@spec new :: t
def new do
%__MODULE__{
env: @default_env,
compile_env: @default_compile_env,
bindings: [],
last_result: nil
}
end
@doc """
Evaluates the provided `string` in the context of the `session` and returns a new session.
The result will be available in the `last_result` key.
In case of a success, the variable bindings or created modules will be saved in the session.
In case of a failure, the rest of the session state won't be updated, so it is possible to
keep executing instructions after a failure
## Examples
iex> Dune.Session.new()
...> |> Dune.Session.eval_string("x = 1")
...> |> Dune.Session.eval_string("x + 2")
#Dune.Session<last_result: %Dune.Success{value: 3, inspected: "3", stdio: ""}, ...>
iex> Dune.Session.new()
...> |> Dune.Session.eval_string("x = 1")
...> |> Dune.Session.eval_string("x = x / 0") # will fail, but the previous state is kept
...> |> Dune.Session.eval_string("x + 2")
#Dune.Session<last_result: %Dune.Success{value: 3, inspected: "3", stdio: ""}, ...>
"""
@spec eval_string(t, String.t(), keyword) :: t
def eval_string(session = %__MODULE__{}, string, opts \\ []) do
opts = Opts.validate!(opts)
parse_state = %{atom_mapping: session.env.atom_mapping, compile_env: session.compile_env}
parsed = Parser.parse_string(string, opts, parse_state)
parsed
|> Eval.run(opts, session)
|> add_result_to_session(session, parsed)
end
defp add_result_to_session(result = %Success{value: {value, env, bindings}}, session, %{
compile_env: compile_env
}) do
result = %{result | value: value}
%{session | env: env, compile_env: compile_env, last_result: result, bindings: bindings}
end
defp add_result_to_session(result = %Failure{}, session, _) do
%{session | last_result: result}
end
defimpl Inspect do
import Inspect.Algebra
def inspect(session, opts) do
container_doc(
"#Dune.Session<",
[last_result: session.last_result],
", ...>",
opts,
&do_inspect/2,
break: :strict
)
end
defp do_inspect({key, value}, opts) do
key = inspect_as_key(key) |> color(:atom, opts)
concat(key, concat(" ", to_doc(value, opts)))
end
if Code.ensure_loaded?(Macro) and function_exported?(Macro, :inspect_atom, 2) do
defp inspect_as_key(key), do: Macro.inspect_atom(:key, key)
else
defp inspect_as_key(key), do: Code.Identifier.inspect_as_key(key)
end
end
end
================================================
FILE: lib/dune/shims/atom.ex
================================================
defmodule Dune.Shims.Atom do
@moduledoc false
alias Dune.AtomMapping
def to_string(env, atom) when is_atom(atom) do
AtomMapping.to_string(env.atom_mapping, atom)
end
def to_charlist(env, atom) when is_atom(atom) do
__MODULE__.to_string(env, atom) |> String.to_charlist()
end
end
================================================
FILE: lib/dune/shims/enum.ex
================================================
defmodule Dune.Shims.Enum do
@moduledoc false
def join(env, enumerable, joiner \\ "") when is_binary(joiner) do
enumerable
|> Enum.map_intersperse(joiner, &Dune.Shims.Kernel.safe_to_string(env, &1))
|> IO.iodata_to_binary()
end
def map_join(env, enumerable, joiner \\ "", mapper)
when is_binary(joiner) and is_function(mapper, 1) do
enumerable
|> Enum.map_intersperse(joiner, &Dune.Shims.Kernel.safe_to_string(env, mapper.(&1)))
|> IO.iodata_to_binary()
end
end
================================================
FILE: lib/dune/shims/io.ex
================================================
defmodule Dune.Shims.IO do
@moduledoc false
alias Dune.{Failure, Shims}
def puts(env, device \\ :stdio, item)
def puts(env, :stdio, item) do
env
|> Shims.Kernel.safe_to_string(item)
|> then(&IO.puts(:stdio, &1))
end
def puts(_env, _device, _item) do
error = Failure.restricted_function(IO, :puts, 2)
throw(error)
end
def inspect(env, item, opts \\ []) do
inspect(env, :stdio, item, opts)
end
def inspect(env, :stdio, item, opts) when is_list(opts) do
inspected = Shims.Kernel.safe_inspect(env, item, opts)
chardata =
if label_opt = opts[:label] do
[Shims.Kernel.safe_to_string(env, label_opt), ": ", inspected]
else
inspected
end
IO.puts(:stdio, chardata)
item
end
def inspect(_env, _device, _item, opts) when is_list(opts) do
error = Failure.restricted_function(IO, :inspect, 3)
throw(error)
end
end
================================================
FILE: lib/dune/shims/json.ex
================================================
if Code.ensure_loaded?(JSON) do
defmodule Dune.Shims.JSON do
@moduledoc false
alias Dune.AtomMapping
alias Dune.Shims
def protocol_encode(env, value, encoder) when is_non_struct_map(value) do
case :maps.next(:maps.iterator(value)) do
:none ->
"{}"
{key, value, iterator} ->
[
?{,
key(env, key, encoder),
?:,
encoder.(value, encoder) | next(env, iterator, encoder)
]
end
end
def protocol_encode(env, value, encoder)
when is_atom(value) and value not in [nil, true, false] do
encoder.(AtomMapping.to_string(env.atom_mapping, value), encoder)
end
def protocol_encode(_env, value, encoder) do
JSON.protocol_encode(value, encoder)
end
defp next(env, iterator, encoder) do
case :maps.next(iterator) do
:none ->
"}"
{key, value, iterator} ->
[
?,,
key(env, key, encoder),
?:,
encoder.(value, encoder) | next(env, iterator, encoder)
]
end
end
defp key(_env, key, encoder) when is_binary(key), do: encoder.(key, encoder)
defp key(env, key, encoder), do: encoder.(Shims.Kernel.safe_to_string(env, key), encoder)
def encode!(env, term) do
encode!(env, term, &protocol_encode(env, &1, &2))
end
def encode!(_env, term, encoder) do
IO.iodata_to_binary(encoder.(term, encoder))
end
def encode_to_iodata!(env, term) do
encode_to_iodata!(env, term, &protocol_encode(env, &1, &2))
end
def encode_to_iodata!(_env, term, encoder) do
encoder.(term, encoder)
end
end
end
================================================
FILE: lib/dune/shims/kernel.ex
================================================
defmodule Dune.Shims.Kernel do
@moduledoc false
alias Dune.{AtomMapping, Failure, Shims}
alias Dune.Helpers.TermChecker
defmacro safe_sigil_w(_env, _, ~c"a") do
error = Failure.restricted_function(Kernel, :sigil_w, 2)
throw(error)
end
defmacro safe_sigil_w(_env, term, modifiers) do
quote do
sigil_w(unquote(term), unquote(modifiers))
end
end
defmacro safe_sigil_W(_env, _, ~c"a") do
error = Failure.restricted_function(Kernel, :sigil_W, 2)
throw(error)
end
defmacro safe_sigil_W(_env, term, modifiers) do
quote do
sigil_W(unquote(term), unquote(modifiers))
end
end
def safe_throw(_env, value) do
throw({:safe_throw, value})
end
defmacro safe_dbg(_env) do
error = Failure.restricted_function(Kernel, :dbg, 0)
throw(error)
end
defmacro safe_dbg(_env, _term) do
# should never be called because the sanitizer handles it
raise "unexpected call safe_dbg/2"
end
defmacro safe_dbg(_env, _term, _opts) do
error = Failure.restricted_function(Kernel, :dbg, 2)
throw(error)
end
def safe_dot(_env, %{} = map, key) do
# TODO test key error
Map.fetch!(map, key)
end
def safe_dot(env, module, fun) when is_atom(module) do
safe_apply(env, module, fun, [])
end
def safe_apply(_env, fun, args) when is_function(fun, 1) do
# TODO check if there is a risk / why it is here
apply(fun, args)
end
def safe_apply(env, module, fun, args) when is_atom(module) do
arity = length(args)
case env.allowlist.fun_status(module, fun, arity) do
:restricted ->
error = Failure.restricted_function(module, fun, arity)
throw(error)
{:shimmed, shim_module, shim_fun} ->
apply(shim_module, shim_fun, [env | args])
:allowed ->
apply(module, fun, args)
:undefined_module ->
Dune.Eval.Env.apply_fake(env, module, fun, args)
:undefined_function ->
throw({:undefined_function, module, fun, arity})
other when other in [:undefined_module, :undefined_function] ->
Dune.Eval.Env.apply_fake(env, module, fun, args)
end
end
def safe_inspect(env, term, opts \\ [])
def safe_inspect(_env, term, opts)
when is_number(term) or is_binary(term) or is_boolean(term) do
inspect(term, opts)
end
def safe_inspect(env, atom, _opts) when is_atom(atom) do
AtomMapping.inspect(env.atom_mapping, atom)
end
def safe_inspect(env, term, opts) do
TermChecker.check(term)
inspected = inspect(term, opts)
AtomMapping.replace_in_string(env.atom_mapping, inspected)
end
def safe_to_string(env, atom) when is_atom(atom) do
Shims.Atom.to_string(env, atom)
end
def safe_to_string(env, atom) when is_list(atom) do
Shims.List.to_string(env, atom)
end
def safe_to_string(_env, other), do: to_string(other)
def safe_to_charlist(env, atom) when is_atom(atom) do
Shims.Atom.to_charlist(env, atom)
end
def safe_to_charlist(_env, other), do: to_charlist(other)
end
================================================
FILE: lib/dune/shims/list.ex
================================================
defmodule Dune.Shims.List do
@moduledoc false
alias Dune.Shims
def to_string(_env, list) when is_list(list) do
do_to_string(list)
end
defp do_to_string(list) when is_list(list) do
if Enum.any?(list, &is_list/1) do
# eagerly convert lists to binary to prevent OOM on
# structural sharing bombs
Enum.map(list, &do_to_string/1)
else
list
end
|> List.to_string()
end
defp do_to_string(elem), do: elem
# note: this is probably not safe so not actually used
def to_existing_atom(env, list) when is_list(list) do
string = to_string(list)
# make sure it was actually a flat charlist and not an IO-list
case to_charlist(string) do
^list -> Shims.String.to_existing_atom(env, string)
_ -> List.to_existing_atom(list)
end
end
def to_existing_atom(_env, list) do
List.to_existing_atom(list)
end
end
================================================
FILE: lib/dune/shims/string.ex
================================================
defmodule Dune.Shims.String do
@moduledoc false
alias Dune.AtomMapping
# note: this is probably not safe so not actually used
def to_existing_atom(env, string) when is_binary(string) do
AtomMapping.to_existing_atom(env, string)
end
def to_existing_atom(_env, string) do
String.to_existing_atom(string)
end
end
================================================
FILE: lib/dune/success.ex
================================================
defmodule Dune.Success do
@moduledoc """
A struct returned when `Dune` evaluation succeeds.
Fields:
- `value` (term): the value which was actually returned at runtime.
Should not be displayed to the user, might be different from what the user expects.
- `inspected` (string): safely inspected `value` to be displayed to the user
- `stdio` (string): captured standard output
`value` contains the actual value used at runtime, so atoms will be different from the ones
displayed to the user (see `Dune.eval_string/2`).
"""
@type t :: %__MODULE__{
value: term,
inspected: String.t(),
stdio: binary
}
@enforce_keys [:value, :inspected, :stdio]
defstruct @enforce_keys
end
================================================
FILE: lib/dune.ex
================================================
defmodule Dune do
@moduledoc """
A sandbox for Elixir to safely evaluate untrusted code from user input.
## Features
- only authorized modules and functions can be executed (see
`Dune.Allowlist.Default`)
- no access to environment variables, file system, network...
- code executed in an isolated process
- execution within configurable limits: timeout, maximum reductions and memory
(inspired by [Luerl](https://github.com/rvirding/luerl))
- captured standard output
- atoms, without atom leaks: parsing and runtime do not
[leak atoms](https://hexdocs.pm/elixir/String.html#to_atom/1) (i.e. does not
keep
[filling the atom table](https://learnyousomeerlang.com/starting-out-for-real#atoms)
until the VM crashes)
- modules, without actual module creation: Dune does not let users define any
actual module (would leak memory and modify the state of the VM globally), but
`defmodule` simulates the basic behavior of a module, including private and
recursive functions
The list of modules and functions authorized by default is defined by the
`Dune.Allowlist.Default` module, but this list can be extended and customized
(at your own risk!) using `Dune.Allowlist`.
If you need to keep the state between evaluations, you might consider
`Dune.Session`.
"""
alias Dune.{Success, Failure, Parser, Eval, Opts}
@doc ~S"""
Evaluates the `string` in the sandbox.
Available options are detailed in `Dune.Opts`.
Returns a `Dune.Success` struct if the execution went successfully,
a `Dune.Failure` else.
## Examples
iex> Dune.eval_string("IO.puts(\"Hello world!\")")
%Dune.Success{inspected: ":ok", stdio: "Hello world!\n", value: :ok}
iex> Dune.eval_string("File.cwd!()")
%Dune.Failure{message: "** (DuneRestrictedError) function File.cwd!/0 is restricted", type: :restricted}
iex> Dune.eval_string("List.duplicate(:spam, 100_000)")
%Dune.Failure{message: "Execution stopped - memory limit exceeded", stdio: "", type: :memory}
iex> Dune.eval_string("Foo.bar()")
%Dune.Failure{message: "** (UndefinedFunctionError) function Foo.bar/0 is undefined (module Foo is not available)", type: :exception}
iex> Dune.eval_string("][")
%Dune.Failure{message: "unexpected token: ]", type: :parsing}
Atoms used during parsing and execution might be transformed to prevent atom leaks:
iex> Dune.eval_string("some_variable = IO.inspect(:some_atom)")
%Dune.Success{inspected: ":some_atom", stdio: ":some_atom\n", value: :a__Dune_atom_2__}
The `value` field shows the actual runtime value, but `inspected` and `stdio` are safe to display to the user.
"""
@spec eval_string(String.t(), Keyword.t()) :: Success.t() | Failure.t()
def eval_string(string, opts \\ []) when is_binary(string) do
opts = Opts.validate!(opts)
string
|> Parser.parse_string(opts)
|> Eval.run(opts)
end
@doc ~S"""
Evaluates the quoted `ast` in the sandbox.
Available options are detailed in `Dune.Opts` (parsing restrictions have no effect)..
Returns a `Dune.Success` struct if the execution went successfully,
a `Dune.Failure` else.
## Examples
iex> Dune.eval_quoted(quote do: [1, 2] ++ [3, 4])
%Dune.Success{inspected: "[1, 2, 3, 4]", stdio: "", value: [1, 2, 3, 4]}
iex> Dune.eval_quoted(quote do: System.get_env())
%Dune.Failure{message: "** (DuneRestrictedError) function System.get_env/0 is restricted", type: :restricted}
iex> Dune.eval_quoted(quote do: Process.sleep(500))
%Dune.Failure{message: "Execution timeout - 50ms", type: :timeout}
"""
@spec eval_quoted(Macro.t(), Keyword.t()) :: Success.t() | Failure.t()
def eval_quoted(ast, opts \\ []) do
opts = Opts.validate!(opts)
ast
|> Parser.parse_quoted(opts)
|> Eval.run(opts)
end
@doc ~S"""
Returns the AST corresponding to the provided `string`, without leaking atoms.
Available options are detailed in `Dune.Opts` (runtime restrictions have no effect).
Returns a `Dune.Success` struct if the execution went successfully,
a `Dune.Failure` else.
## Examples
iex> Dune.string_to_quoted("1 + 2")
%Dune.Success{inspected: "{:+, [line: 1], [1, 2]}", stdio: "", value: {:+, [line: 1], [1, 2]}}
iex> Dune.string_to_quoted("[invalid")
%Dune.Failure{stdio: "", message: "missing terminator: ]", type: :parsing}
The `pretty` option can make the AST more readable by adding newlines to `inspected`:
iex> Dune.string_to_quoted("IO.puts(:hello)", pretty: true).inspected
"{{:., [line: 1], [{:__aliases__, [line: 1], [:IO]}, :puts]}, [line: 1],\n [:hello]}"
iex> Dune.string_to_quoted("IO.puts(:hello)").inspected
"{{:., [line: 1], [{:__aliases__, [line: 1], [:IO]}, :puts]}, [line: 1], [:hello]}"
Since the code isn't executed, there is no allowlist restriction:
iex> Dune.string_to_quoted("System.halt()")
%Dune.Success{
inspected: "{{:., [line: 1], [{:__aliases__, [line: 1], [:System]}, :halt]}, [line: 1], []}",
stdio: "",
value: {{:., [line: 1], [{:__aliases__, [line: 1], [:System]}, :halt]}, [line: 1], []}
}
Atoms might be transformed during parsing to prevent atom leaks:
iex> Dune.string_to_quoted("some_variable = :some_atom")
%Dune.Success{
inspected: "{:=, [line: 1], [{:some_variable, [line: 1], nil}, :some_atom]}",
stdio: "",
value: {:=, [line: 1], [{:a__Dune_atom_1__, [line: 1], nil}, :a__Dune_atom_2__]}
}
The `value` field shows the actual runtime value, but `inspected` is safe to display to the user.
"""
@spec string_to_quoted(String.t(), Keyword.t()) :: Success.t() | Failure.t()
def string_to_quoted(string, opts \\ []) when is_binary(string) do
opts = Opts.validate!(opts)
Parser.string_to_quoted(string, opts)
end
end
================================================
FILE: mix.exs
================================================
defmodule Dune.MixProject do
use Mix.Project
@version "0.3.15"
@github_url "https://github.com/functional-rewire/dune"
def project do
[
app: :dune,
version: @version,
elixir: ">= 1.14.0 and < 1.20.0",
start_permanent: Mix.env() == :prod,
deps: deps(),
dialyzer: [flags: [:missing_return, :extra_return]],
# Hex
description: "A sandbox for Elixir to safely evaluate untrusted code from user input",
package: package(),
aliases: aliases(),
docs: docs()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# CI
{:dialyxir, "~> 1.0", only: :test, runtime: false},
# DOCS
{:ex_doc, "~> 0.24", only: :docs, runtime: false}
]
end
defp package do
[
maintainers: ["functional-rewire", "sabiwara"],
licenses: ["MIT"],
links: %{"GitHub" => @github_url},
files: ~w(lib mix.exs .formatter.exs README.md LICENSE.md CHANGELOG.md)
]
end
defp aliases do
[docs: ["compile --force", "docs"]]
end
def cli do
[preferred_envs: [docs: :docs, "hex.publish": :docs, dialyzer: :test]]
end
defp docs do
[
main: "Dune",
source_ref: "v#{@version}",
source_url: @github_url,
homepage_url: @github_url,
extras: ["README.md", "CHANGELOG.md", "LICENSE.md"]
]
end
end
================================================
FILE: test/dune/allowlist/default_test.exs
================================================
defmodule Dune.Allowlist.DefaultTest do
use ExUnit.Case, async: true
doctest Dune.Allowlist.Default
alias Dune.Allowlist.Default
describe "fun_status/3" do
test "should not allow module_info/N" do
assert :restricted = Default.fun_status(Float, :module_info, 0)
assert :restricted = Default.fun_status(Float, :module_info, 1)
assert :restricted = Default.fun_status(:math, :module_info, 0)
assert :restricted = Default.fun_status(:math, :module_info, 1)
end
end
end
================================================
FILE: test/dune/allowlist_test.exs
================================================
defmodule Dune.AllowlistTest do
use ExUnit.Case, async: true
doctest Dune.Allowlist
describe "use/2" do
test "creates a new sandbox " do
defmodule CustomAllowlist do
use Dune.Allowlist
allow Kernel, only: [:+, :*, :-, :/, :div, :rem]
allow Integer, only: [:pow]
end
assert :allowed = CustomAllowlist.fun_status(Kernel, :+, 2)
assert :restricted = CustomAllowlist.fun_status(Kernel, :<>, 2)
assert :undefined_function = CustomAllowlist.fun_status(Kernel, :foo, 1)
assert :allowed = CustomAllowlist.fun_status(Integer, :pow, 2)
assert :restricted = CustomAllowlist.fun_status(Integer, :to_string, 1)
assert :undefined_function = CustomAllowlist.fun_status(Integer, :foo, 1)
assert :restricted = CustomAllowlist.fun_status(String, :upcase, 1)
assert :undefined_module = CustomAllowlist.fun_status(Foo, :foo, 1)
end
test "extends an existing sandbox " do
defmodule CustomModule do
def authorized(i), do: i + 1
def forbidden(i), do: i - 1
end
defmodule ExtendedAllowlist do
use Dune.Allowlist, extend: Dune.Allowlist.Default
allow CustomModule, only: [:authorized]
end
assert :allowed = ExtendedAllowlist.fun_status(String, :upcase, 1)
assert :restricted = ExtendedAllowlist.fun_status(String, :to_atom, 1)
assert :allowed = ExtendedAllowlist.fun_status(CustomModule, :authorized, 1)
assert :restricted = ExtendedAllowlist.fun_status(CustomModule, :forbidden, 1)
assert :undefined_module = ExtendedAllowlist.fun_status(Foo, :foo, 1)
end
end
end
================================================
FILE: test/dune/atom_mapping_test.exs
================================================
defmodule Dune.AtomMappingTest do
use ExUnit.Case, async: true
doctest Dune.AtomMapping
end
================================================
FILE: test/dune/opts_test.exs
================================================
defmodule Dune.OptsTest do
use ExUnit.Case, async: true
doctest Dune.Opts
end
================================================
FILE: test/dune/parser/atom_encoder_test.exs
================================================
defmodule Dune.Parser.AtomEncoderTest do
use ExUnit.Case, async: true
import Dune.Parser.AtomEncoder
describe "categorize_atom_binary/1" do
test "categorizes aliases cases" do
assert :alias = categorize_atom_binary("Elixir")
assert :alias = categorize_atom_binary("String")
assert :alias = categorize_atom_binary("Foo.Bar")
end
test "categorizes valid public variable names" do
assert :public_var = categorize_atom_binary("abc")
assert :public_var = categorize_atom_binary("erlang")
assert :public_var = categorize_atom_binary("あ")
end
test "categorizes valid private variable names" do
assert :private_var = categorize_atom_binary("_")
assert :private_var = categorize_atom_binary("_abc")
assert :private_var = categorize_atom_binary("_あ")
end
test "categorizes other cases" do
assert :other = categorize_atom_binary("")
assert :other = categorize_atom_binary(" ")
assert :other = categorize_atom_binary("a b")
assert :other = categorize_atom_binary("A B")
assert :other = categorize_atom_binary("Elixir. A")
assert :other = categorize_atom_binary("Foo.Bar ")
assert :other = categorize_atom_binary(" Foo.Bar")
end
end
end
================================================
FILE: test/dune/parser/string_parser_test.exs
================================================
defmodule Dune.Parser.StringParserTest do
use ExUnit.Case, async: true
alias Dune.{AtomMapping, Opts}
alias Dune.Parser.{StringParser, UnsafeAst}
describe "parse_string/2" do
test "existing atoms" do
assert %UnsafeAst{ast: nil, atom_mapping: AtomMapping.new()} ==
StringParser.parse_string("nil", %Opts{}, nil)
assert %UnsafeAst{ast: true, atom_mapping: AtomMapping.new()} ==
StringParser.parse_string("true", %Opts{}, nil)
assert %UnsafeAst{ast: :atom, atom_mapping: AtomMapping.new()} ==
StringParser.parse_string(":atom", %Opts{}, nil)
assert %UnsafeAst{ast: :Atom, atom_mapping: AtomMapping.new()} ==
StringParser.parse_string(":Atom", %Opts{}, nil)
end
test "existing modules" do
assert %UnsafeAst{
ast: {:__aliases__, [line: 1], [:Module]},
atom_mapping: AtomMapping.new()
} == StringParser.parse_string("Module", %Opts{}, nil)
assert %UnsafeAst{
ast: {:__aliases__, [line: 1], [:Date, :Range]},
atom_mapping: AtomMapping.new()
} == StringParser.parse_string("Date.Range", %Opts{}, nil)
end
test "non-existing atoms" do
assert %UnsafeAst{
ast: :a__Dune_atom_1__,
atom_mapping: %AtomMapping{
atoms: %{a__Dune_atom_1__: "my_atom"},
modules: %{},
extra_info: %{}
}
} == StringParser.parse_string(":my_atom", %Opts{}, nil)
assert %UnsafeAst{
ast: :__Dune_atom_1__,
atom_mapping: %AtomMapping{
atoms: %{__Dune_atom_1__: "_my_atom"},
modules: %{},
extra_info: %{}
}
} == StringParser.parse_string(":_my_atom", %Opts{}, nil)
assert %UnsafeAst{
ast: :Dune_Atom_1__,
atom_mapping: %AtomMapping{
atoms: %{Dune_Atom_1__: "MyAtom"},
modules: %{},
extra_info: %{}
}
} == StringParser.parse_string(":MyAtom", %Opts{}, nil)
end
test "non-existing modules" do
assert %UnsafeAst{
ast: {:__aliases__, [line: 1], [:Dune_Module_1__]},
atom_mapping: %AtomMapping{
atoms: %{Dune_Atom_1__: "MyModule"},
modules: %{Dune_Module_1__ => "MyModule"},
extra_info: %{}
}
} == StringParser.parse_string("MyModule", %Opts{}, nil)
assert %UnsafeAst{
ast: {:__aliases__, [line: 1], [:Dune_Module_1__]},
atom_mapping: %AtomMapping{
atoms: %{Dune_Atom_1__: "My", Dune_Atom_2__: "AwesomeModule"},
modules: %{Dune_Module_1__ => "My.AwesomeModule"},
extra_info: %{}
}
} == StringParser.parse_string("My.AwesomeModule", %Opts{}, nil)
assert %UnsafeAst{
ast: {:__aliases__, [line: 1], [:Dune_Module_1__]},
atom_mapping: %AtomMapping{
atoms: %{Dune_Atom_1__: "My"},
modules: %{Dune_Module_1__ => "My.Module"},
extra_info: %{}
}
} == StringParser.parse_string("My.Module", %Opts{}, nil)
end
test ~S[non-existing "wrapped" atoms (with whitespace)] do
assert %UnsafeAst{
ast: :__Dune_atom_1__,
atom_mapping: %AtomMapping{
atoms: %{__Dune_atom_1__: " "},
modules: %{},
extra_info: %{__Dune_atom_1__: :wrapped}
}
} == StringParser.parse_string(~S(:" "), %Opts{}, nil)
assert %UnsafeAst{
ast: :__Dune_atom_1__,
atom_mapping: %AtomMapping{
atoms: %{__Dune_atom_1__: "my atom"},
modules: %{},
extra_info: %{__Dune_atom_1__: :wrapped}
}
} == StringParser.parse_string(~S(:"my atom"), %Opts{}, nil)
end
end
end
================================================
FILE: test/dune/session_test.exs
================================================
defmodule Dune.SessionTest do
use ExUnit.Case
doctest Dune.Session, tags: [lts_only: true]
alias Dune.{Session, Success, Failure}
@module_code """
defmodule MySum do
def sum(xs), do: sum(xs, 0)
defp sum([], acc), do: acc
defp sum([x | xs], acc) do
sum(xs, acc + x)
end
end
"""
describe "eval_string/3" do
test "keeps variable bindings" do
session =
Session.new()
|> Session.eval_string("abcd = 5")
|> Session.eval_string("abcd")
assert %Session{
last_result: %Success{inspected: "5"},
bindings: [a__Dune_atom_1__: 5]
} = session
end
test "ignores failed steps" do
session =
Session.new()
|> Session.eval_string("abcd = 5")
|> Session.eval_string("abcd = 1 / 0")
|> Session.eval_string("abcd")
assert %Session{
last_result: %Success{inspected: "5"},
bindings: [a__Dune_atom_1__: 5]
} = session
end
test "keeps variable bindings on errors" do
session =
Session.new()
|> Session.eval_string("abcd = 5")
|> Session.eval_string("abcd / 0")
assert %Session{
last_result: %Failure{
message: "** (ArithmeticError) bad argument in arithmetic expression" <> _
},
bindings: [a__Dune_atom_1__: 5]
} = session
end
test "keeps the atom mapping" do
session =
Session.new()
|> Session.eval_string("abcd = :abcd")
|> Session.eval_string("efgh = :efgh")
|> Session.eval_string("[abcd, efgh]")
assert %Session{
last_result: %Success{inspected: "[:abcd, :efgh]"},
bindings: [
a__Dune_atom_2__: :a__Dune_atom_2__,
a__Dune_atom_1__: :a__Dune_atom_1__
]
} = session
end
test "keeps the module mapping" do
session =
Session.new()
|> Session.eval_string("acbd = [Aa]")
|> Session.eval_string("acbd = acbd ++ [Bb]")
|> Session.eval_string("acbd = acbd ++ [Aa.Bb]")
|> Session.eval_string("acbd ++ [Cc.Dd]")
assert %Session{
last_result: %Success{inspected: "[Aa, Bb, Aa.Bb, Cc.Dd]"},
bindings: [a__Dune_atom_1__: [Dune_Module_1__, Dune_Module_2__, Dune_Module_3__]]
} = session
end
test "handles modules" do
session =
Session.new()
|> Session.eval_string(@module_code)
|> Session.eval_string("MySum.sum([1, 2, 100])")
assert %Session{
last_result: %Success{inspected: "103"},
bindings: []
} = session
end
test "does not break due to Elixir single atom bug" do
session = Session.new() |> Session.eval_string(":foo")
assert %Session{
last_result: %Success{inspected: ":foo"},
bindings: []
} = session
end
end
end
================================================
FILE: test/dune/shims_test.exs
================================================
defmodule Dune.ShimsTest do
use ExUnit.Case, async: true
alias Dune.Success
alias Dune.Failure
defmacrop sigil_E(call, _expr) do
quote do
Dune.eval_string(unquote(call), timeout: 100, inspect_sort_maps: true)
end
end
describe "JSON" do
@describetag :lts_only
test "encode atoms" do
assert %Success{value: ~S("json101"), inspected: ~S("\"json101\"")} =
~E'JSON.encode!(:json101)'
assert %Success{value: ~S("json102"), inspected: ~S("\"json102\"")} =
~E'JSON.encode_to_iodata!(:json102) |> IO.iodata_to_binary()'
assert %Success{
value: ~S({"json201":["json202",123,"foo",null,true]}),
inspected: ~S("{\"json201\":[\"json202\",123,\"foo\",null,true]}")
} = ~E'JSON.encode!(%{json201: [:json202, 123, "foo", nil, true]})'
end
test "decode atoms" do
assert %Success{value: "json301", inspected: ~S("json301")} =
~E'JSON.decode!("\"json301\"")'
end
end
describe "iodata / chardata" do
test "List.to_string" do
assert %Success{value: <<1, 2, 3>>} = ~E'List.to_string([1, 2, 3])'
assert %Success{value: <<1, 2, 3>>} = ~E'List.to_string([1, [[2], 3]])'
assert %Success{value: "abc"} = ~E'List.to_string(["a", [["b"], "c"]])'
assert %Failure{message: "** (ArgumentError) cannot convert the given list" <> _} =
~E'List.to_string([1, :foo])'
end
test "IO.chardata_to_string" do
assert %Success{value: <<1, 2, 3>>} = ~E'IO.chardata_to_string([1, 2, 3])'
assert %Success{value: <<1, 2, 3>>} = ~E'IO.chardata_to_string([1, [[2], 3]])'
assert %Success{value: "abc"} = ~E'IO.chardata_to_string(["a", [["b"], "c"]])'
assert %Failure{message: "** (ArgumentError) cannot convert the given list" <> _} =
~E'IO.chardata_to_string([1, :foo])'
end
end
end
================================================
FILE: test/dune/validation_test.exs
================================================
defmodule Dune.ValidationTest do
end
================================================
FILE: test/dune_modules_test.exs
================================================
defmodule DuneModulesTest do
use ExUnit.Case, async: true
alias Dune.{Success, Failure}
defmacro sigil_E(call, _expr) do
# TODO fix memory needs
quote do
Dune.eval_string(unquote(call), max_reductions: 25_000, max_heap_size: 50_000, timeout: 100)
end
end
describe "Dune authorized" do
test "basic module" do
result = ~E'''
defmodule Hello do
def greet(value) do
IO.puts "Hello #{value}"
end
end
Hello.greet(:world!)
'''
assert %Success{value: :ok, stdio: "Hello world!\n"} = result
end
test "plain atom module" do
result = ~E'''
defmodule :hello do
def greet(value) do
IO.puts "Hello #{value}"
end
end
:hello.greet(:world!)
'''
assert %Success{value: :ok, stdio: "Hello world!\n"} = result
end
test "module without other code" do
result = ~E'''
defmodule Hello do
end
'''
assert %Success{
value: {:module, Dune_Module_1__, nil, nil},
inspected: "{:module, Hello, nil, nil}",
stdio: ""
} = result
end
test "default argument" do
result = ~E'''
defmodule My.Default do
def incr(x \\ 0), do: x + 1
end
[My.Default.incr(), My.Default.incr(100)]
'''
assert %Success{value: [1, 101]} = result
end
test "default arguments" do
result = ~E'''
defmodule My.Defaults do
def defaults(a \\ 1, b \\ 2, c) do
[a, b, c]
end
end
{My.Defaults.defaults(:c), My.Defaults.defaults(:a, :c)}
'''
assert %Success{value: {[1, 2, :c], [:a, 2, :c]}} = result
end
test "recursive functions with guards" do
result = ~E'''
defmodule My.List do
def my_sum([]), do: 0
def my_sum([h | t]) when is_number(h), do: h + my_sum(t)
end
My.List.my_sum([1, 100, 1000])
'''
assert %Success{value: 1101} = result
end
test "recursive functions in a nested block" do
result = ~E'''
defmodule My.List do
def my_sum([]), do: 0
def my_sum([h | t]) do
if is_number(h) do
h + my_sum(t)
else
:NaN
end
end
end
My.List.my_sum([1, 100, 1000])
'''
assert %Success{value: 1101} = result
end
test "public and private functions" do
assert %Success{value: "success!"} = ~E'''
defmodule My.Module do
def public, do: private()
defp private, do: "success!"
end
My.Module.public
'''
end
test "recursive private function with guards" do
result = ~E'''
defmodule My.List do
def my_sum(list) when is_list(list), do: my_sum(list, 0)
defp my_sum([], acc), do: acc
defp my_sum([h | t], acc) when is_number(h), do: my_sum(t, h + acc)
end
My.List.my_sum([1, 100, 1000])
'''
assert %Success{value: 1101} = result
end
test "captured fake module functions (external)" do
assert %Success{value: ["Joe (20)", "Jane (27)"]} = ~E'''
defmodule My.Captures do
def format(%{name: name, age: age}) do
"#{name} (#{age})"
end
end
Enum.map(
[%{name: "Joe", age: 20}, %{name: "Jane", age: 27}],
&My.Captures.format/1
)
'''
assert %Success{inspected: ":foo"} = ~E'''
defmodule My.Captures do
def const, do: :foo
end
f = &My.Captures.const/0
f.()
'''
end
test "captured fake module functions (internal)" do
assert %Success{value: ["Joe (20)", "Jane (27)"]} = ~E'''
defmodule My.Captures do
def format_many(list) do
Enum.map(list, &format_one/1)
end
def format_one(%{name: name, age: age}) do
"#{name} (#{age})"
end
end
My.Captures.format_many([%{name: "Joe", age: 20}, %{name: "Jane", age: 27}])
'''
end
test "apply fake module functions" do
assert %Success{value: "Joe (20)"} = ~E'''
defmodule My.Formatter do
def format(%{name: name, age: age}) do
"#{name} (#{age})"
end
end
apply(
My.Formatter,
:format,
[%{name: "Joe", age: 20}]
)
'''
end
test "accept docs and typespecs" do
assert %Success{value: "Joe (20)"} = ~E'''
defmodule My.Formatter do
@moduledoc "Format all the things!"
@typep name :: String.t()
@type user :: %{name: name, age: integer}
@doc "Formats a user"
@spec format(user) :: String.t()
def format(%{name: name, age: age}) do
"#{name} (#{age})"
end
end
My.Formatter.format(%{name: "Joe", age: 20})
'''
end
test "0-arity call without parenthesis" do
assert %Success{value: "success!"} = ~E'''
defmodule My.Module do
def public, do: private()
defp private, do: "success!"
end
My.Module.public
'''
end
# TODO: apply private function
end
describe "exceptions" do
test "function clause error" do
assert %Failure{
type: :exception,
message:
"** (Dune.Eval.FunctionClauseError) no function clause matching in My.Checker.check_age/1: My.Checker.check_age(:invalid)"
} = ~E'''
defmodule My.Checker do
def check_age(age) when is_integer(age) and age >= 18, do: :ok
end
My.Checker.check_age(:invalid)
'''
end
test "calling private function" do
assert %Failure{
type: :exception,
message:
"** (UndefinedFunctionError) function My.Module.private/0 is undefined or private"
} = ~E'''
defmodule My.Module do
def public, do: :public
defp private, do: :private
end
My.Module.private
'''
end
test "conflicting public and private functions" do
assert %Failure{
type: :exception,
message:
"** (Dune.Eval.CompileError) nofile:3: defp conflicting/0 already defined as def in nofile:2"
} = ~E'''
defmodule My.Module do
def conflicting, do: "public"
defp conflicting, do: "private"
end
'''
end
end
describe "Dune restricted" do
test "unsafe function body" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function System.get_env/0 is restricted"
} = ~E'''
defmodule Danger do
def danger() do
System.get_env()
end
end
Danger.danger()
'''
end
test "unsafe function default arg" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function System.get_env/0 is restricted"
} = ~E'''
defmodule Danger do
def danger(env \\ System.get_env()) do
env
end
end
Danger.danger()
'''
end
test "restrictions in the module top level" do
assert %Failure{
type: :module_restricted,
message:
"** (DuneRestrictedError) the following syntax is restricted inside defmodule:\n def no_block"
} = ~E'''
defmodule My.Module do
def no_block
end
'''
assert %Failure{
type: :module_restricted,
message:
"** (DuneRestrictedError) the following syntax is restricted inside defmodule:\n @foo 1 + 1"
} = ~E'''
defmodule My.Module do
@foo 1 + 1
end
'''
end
test "trying to redefine existing module" do
assert %Failure{
type: :module_conflict,
message:
"** (DuneRestrictedError) Following module cannot be defined/redefined: System"
} = ~E'''
defmodule System do
def foo, do: :bar
end
'''
assert %Failure{
type: :module_conflict,
message:
"** (DuneRestrictedError) Following module cannot be defined/redefined: String"
} = ~E'''
defmodule String do
def foo, do: :bar
end
'''
assert %Failure{
type: :module_conflict,
message:
"** (DuneRestrictedError) Following module cannot be defined/redefined: ExUnit"
} = ~E'''
defmodule ExUnit do
def foo, do: :bar
end
'''
assert %Failure{
type: :module_conflict,
message:
"** (DuneRestrictedError) Following module cannot be defined/redefined: Foo"
} = ~E'''
defmodule Foo do
def foo, do: :bar
end
defmodule Foo do
def foo, do: :bar
end
'''
end
test "defmodule used with different arities" do
assert %Failure{
type: :parsing,
message: "dune parsing error: failed to safe parse\n defmodule"
} = ~E'defmodule'
assert %Failure{
type: :parsing,
message: "dune parsing error: failed to safe parse\n defmodule do\n" <> _
} = ~E'''
defmodule do
def foo, do: :bar
end
'''
end
test "defmodule with invalid module names" do
assert %Failure{
type: :exception,
message: "** (Dune.Eval.CompileError) nofile:1: invalid module name: true"
} = ~E'''
defmodule true do
end
'''
assert %Failure{
type: :exception,
message: "** (Dune.Eval.CompileError) nofile:1: invalid module name: nil"
} = ~E'''
defmodule nil do
end
'''
assert %Failure{
type: :exception,
message: "** (Dune.Eval.CompileError) nofile:1: invalid module name: 1"
} = ~E'''
defmodule 1 do
end
'''
end
end
end
================================================
FILE: test/dune_oom_safety_test.exs
================================================
defmodule Dune.AssertionHelper do
alias Dune.Failure
defmacro test_execution_stops(test_name, do: expr) do
quote do
test unquote(test_name) do
# not sure if reductions or memory limit this first
assert %Failure{message: "Execution stopped - " <> _} =
Dune.eval_quoted(unquote(Macro.escape(expr)), timeout: 100)
end
end
end
end
defmodule Dune.OOMSafetyTest do
# Safety integration tests for "structural-sharing bombs" edge cases
# that would cause BIFs to hang and use enormous amounts of memory
use ExUnit.Case, async: true
import Dune.AssertionHelper
test_execution_stops "List.duplicate" do
List.duplicate(:foo, 200_000)
end
# TODO figure out why this fails since Elixir 1.18
@tag :skip
test_execution_stops "String.duplicate" do
String.duplicate("foo", 200_000)
end
describe "structural sharing bombs" do
test_execution_stops "returning value directly" do
Enum.reduce(1..100, ["foo", "bar"], fn _, acc -> [acc, acc] end)
end
test_execution_stops "inspect" do
Enum.reduce(1..100, ["foo", "bar"], fn _, acc -> [acc, acc] end)
|> inspect(limit: :infinity)
end
test_execution_stops "string interpolation" do
bomb = Enum.reduce(1..100, ["foo", "bar"], fn _, acc -> [acc, acc] end)
"#{bomb}!"
end
test_execution_stops "to_string" do
Enum.reduce(1..100, ["foo", "bar"], fn _, acc -> [acc, acc] end) |> to_string()
end
test_execution_stops "List.to_string" do
Enum.reduce(1..100, ["foo", "bar"], fn _, acc -> [acc, acc] end) |> List.to_string()
end
test_execution_stops "IO.iodata_to_binary" do
Enum.reduce(1..100, ["foo", "bar"], fn _, acc -> [acc, acc] end) |> IO.iodata_to_binary()
end
test_execution_stops "IO.iodata_length" do
Enum.reduce(1..100, ["foo", "bar"], fn _, acc -> [acc, acc] end) |> IO.iodata_length()
end
test_execution_stops "IO.chardata_to_string" do
Enum.reduce(1..100, ["foo", "bar"], fn _, acc -> [acc, acc] end) |> IO.chardata_to_string()
end
test_execution_stops "Enum.join" do
Enum.reduce(1..100, ["foo", "bar"], fn _, acc -> [acc, acc] end) |> Enum.join()
end
@tag :lts_only
test_execution_stops "JSON encode key" do
bomb = Enum.reduce(1..100, ["foo", "bar"], fn _, acc -> [acc, acc] end)
JSON.encode!(%{bomb => 123})
end
end
end
================================================
FILE: test/dune_quoted_test.exs
================================================
defmodule DuneQuotedTest do
use ExUnit.Case, async: true
import ExUnit.CaptureIO
alias Dune.{Success, Failure}
defmacrop dune(do: ast) do
escaped_ast = Macro.escape(ast)
quote do
unquote(escaped_ast) |> Dune.eval_quoted(timeout: 100)
end
end
describe "Dune authorized" do
test "simple operations" do
assert %Success{value: 5} = dune(do: 2 + 3)
assert %Success{value: 15} = dune(do: 5 * 3)
assert %Success{value: 0.5} = dune(do: 1 / 2)
assert %Success{value: [1, 2, 3, 4]} = dune(do: [1, 2] ++ [3, 4])
assert %Success{value: "abcd"} = dune(do: "ab" <> "cd")
end
test "basic Kernel functions" do
assert %Success{value: 10} = dune(do: max(5, 10))
assert %Success{value: "foo"} = dune(do: to_string(:foo))
assert %Success{value: true} = dune(do: is_atom(:foo))
end
test "Kernel guards" do
assert %Success{value: false} = dune(do: is_binary(55))
assert %Success{value: true} = dune(do: is_number(55))
end
test "basic String functions" do
assert %Success{value: "JoJo"} = dune(do: String.replace("jojo", "j", "J"))
end
test "basic Map functions" do
assert %Success{value: %{foo: 5}} = dune(do: Map.put(%{}, :foo, 5))
assert %Success{value: %{}} = dune(do: Map.new())
end
test "basic :math functions" do
assert %Success{value: 3.0} = dune(do: :math.log10(1000))
assert %Success{value: 3.141592653589793} = dune(do: :math.pi())
end
test "tuples" do
assert %Success{value: {}} = dune(do: {})
assert %Success{value: {:foo}} = dune(do: {:foo})
assert %Success{value: {"hello", ~c"world"}} = dune(do: {"hello", ~c"world"})
assert %Success{value: {1, 2, 3}} = dune(do: {1, 2, 3})
end
test "map operations" do
assert %Success{value: %{a: :foo, b: 6}} =
(dune do
map = %{a: 5, b: 6}
%{map | a: :foo}
end)
assert %Success{value: "Dio"} =
(dune do
user = %{first_name: "Dio", last_name: "Brando"}
user.first_name
end)
end
test "dynamic module names (authorized functions)" do
assert %Success{value: %{}} =
(dune do
module = Map
module.new()
end)
assert %Success{value: [%{}, %MapSet{}]} =
(dune do
Enum.map([Map, MapSet], fn module -> module.new end)
end)
assert %Success{value: [%{}, %MapSet{}]} =
(dune do
Enum.map([Map, MapSet], fn module -> module.new() end)
end)
end
test "captures" do
assert %Success{value: ["1", "2", "3"]} =
(dune do
1..3 |> Enum.map(&inspect/1)
end)
assert %Success{value: [[0], [1, 0], [2, 0], [3, 0]]} =
(dune do
0..30//10 |> Enum.map(&Integer.digits/1)
end)
assert %Success{value: 3.317550714905183e39} =
(dune do
1..100//10 |> Enum.map(&:math.exp/1) |> Enum.sum()
end)
end
test "sigils" do
assert %Success{value: %Regex{source: "(a|b)?c"}} = dune(do: ~r/(a|b)?c/)
assert %Success{value: ~U[2021-05-20 01:02:03Z]} = dune(do: ~U[2021-05-20 01:02:03Z])
assert %Success{value: [~c"foo", ~c"bar", ~c"baz"]} = dune(do: ~W[foo bar baz]c)
assert %Success{value: [~c"foo", ~c"bar", ~c"baz"]} =
dune(do: ~w[#{String.downcase("FOO")} bar baz]c)
assert %Dune.Failure{
message: "** (DuneRestrictedError) function sigil_W/2 is restricted",
type: :restricted
} = dune(do: ~W[foo bar baz]a)
end
test "block of code" do
assert %Success{value: "quick-brown-fox-jumps-over-lazy-dog"} =
(dune do
sentence = "the quick brown fox jumps over the lazy dog"
words = String.split(sentence)
filtered = Enum.reject(words, &(&1 == "the"))
Enum.join(filtered, "-")
end)
end
test "pipe operator" do
assert %Success{value: "quick-brown-fox-jumps-over-lazy-dog"} =
(dune do
"the quick brown fox jumps over the lazy dog"
|> String.split()
|> Enum.reject(&(&1 == "the"))
|> Enum.join("-")
end)
end
test "if block" do
assert %Success{value: {:foo, 6}} =
(dune do
x = 6
if x > 5 do
{:foo, x}
else
{:bar, x}
end
end)
end
test "case block" do
assert %Success{value: {:bar, 4}} =
(dune do
case 4 do
x when x > 5 -> {:foo, x}
y -> {:bar, y}
end
end)
end
test "for block" do
assert %Success{value: [:b, :c]} =
(dune do
for {x, y} <- [a: 1, b: 2, c: 3], y > 1, do: x
end)
end
end
describe "Dune restricted" do
test "System calls" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function System.get_env/0 is restricted"
} = dune(do: System.get_env())
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function System.get_env/1 is restricted"
} = dune(do: System.get_env("TEST"))
end
test "Code calls" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function Code.eval_string/1 is restricted"
} = dune(do: Code.eval_string("IO.puts(:hello)"))
end
test "String/List restricted methods" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function String.to_atom/1 is restricted"
} = dune(do: String.to_atom("foo"))
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function List.to_atom/1 is restricted"
} = dune(do: List.to_atom(~c"foo"))
assert %Failure{
type: :restricted,
message:
"** (DuneRestrictedError) function String.to_existing_atom/1 is restricted"
} = dune(do: String.to_existing_atom("foo"))
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function List.to_existing_atom/1 is restricted"
} = dune(do: List.to_existing_atom(~c"foo"))
end
test "atom interpolation" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function :erlang.binary_to_atom/2 is restricted"
} = dune(do: :"#{1 + 1} is two")
end
test "Kernel apply/3" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function Process.get/0 is restricted"
} =
(dune do
apply(Process, :get, [])
end)
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function IO.puts/2 is restricted"
} =
(dune do
apply(IO, :puts, [:stderr, "Hello"])
end)
end
test ". operator with variable modules" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function :erlang.yield/0 is restricted"
} =
(dune do
module = :erlang
module.yield
end)
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function :erlang.processes/0 is restricted"
} =
(dune do
Enum.map([:erlang], fn module -> module.processes end)
end)
end
test "capture operator" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function Code.eval_string/1 is restricted"
} = dune(do: f = &Code.eval_string/1)
end
test "Kernel 0-arity functions" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function binding/0 is restricted"
} =
(dune do
binding
end)
end
test "erlang unsafe libs" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function :erlang.processes/0 is restricted"
} = dune(do: :erlang.processes())
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function :erlang.get/0 is restricted"
} = dune(do: :erlang.get())
end
test "nested restricted code" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function Code.eval_string/1 is restricted"
} =
(dune do
f = fn -> Code.eval_string("IO.puts(:hello)") end
end)
end
end
describe "process restrictions" do
test "execution timeout" do
assert %Failure{type: :timeout, message: "Execution timeout - 100ms"} =
dune(do: Process.sleep(101))
end
test "too many reductions" do
assert %Failure{type: :reductions, message: "Execution stopped - reductions limit exceeded"} =
dune(do: Enum.any?(1..1_000_000, &(&1 < 0)))
end
test "uses to much memory" do
assert %Failure{type: :memory, message: "Execution stopped - memory limit exceeded"} =
dune(do: List.duplicate(:foo, 100_000))
end
end
describe "error handling" do
test "math error" do
assert %Failure{
type: :exception,
message: "** (ArithmeticError) bad argument in arithmetic expression\n" <> rest
} = dune(do: 42 / 0)
assert rest =~ ":erlang./(42, 0)"
end
test "throw" do
assert %Failure{type: :throw, message: "** (throw) :yo"} = dune(do: throw(:yo))
end
test "raise" do
assert %Failure{type: :exception, message: "** (ArgumentError) kaboom!"} =
(dune do
raise ArgumentError, "kaboom!"
end)
end
test "actual UndefinedFunctionError" do
assert %Failure{
type: :exception,
message: "** (UndefinedFunctionError) function Code.baz/0 is undefined or private"
} = dune(do: Code.baz())
assert %Failure{
type: :exception,
message:
"** (UndefinedFunctionError) function FooBar.baz/0 is undefined (module FooBar is not available)"
} = dune(do: FooBar.baz())
assert %Failure{
type: :exception,
message:
"** (UndefinedFunctionError) function :foo_bar.baz/0 is undefined (module :foo_bar is not available)"
} = dune(do: :foo_bar.baz())
end
@tag :lts_only
test "undefined variable" do
capture_io(:stderr, fn ->
# TODO better error messages
assert %Failure{
type: :compile_error,
message:
"** (CompileError) nofile: cannot compile file (errors have been logged)",
stdio: "error: undefined variable \"y\"\n" <> _
} = dune(do: y)
assert %Failure{
type: :compile_error,
message:
"** (CompileError) nofile: cannot compile file (errors have been logged)",
stdio: "error: undefined variable \"x\"\n" <> _
} = dune(do: if(x, do: x))
end)
end
end
end
================================================
FILE: test/dune_string_test.exs
================================================
defmodule DuneStringTest do
use ExUnit.Case, async: true
alias Dune.{Success, Failure}
defmacro sigil_E(call, _expr) do
quote do
Dune.eval_string(unquote(call), timeout: 100, inspect_sort_maps: true)
end
end
describe "Dune authorized" do
test "simple operations" do
assert %Success{value: 5, inspected: ~S'5'} = ~E'2 + 3'
assert %Success{value: 15, inspected: ~S'15'} = ~E'5 * 3'
assert %Success{value: 0.5, inspected: ~S'0.5'} = ~E'1 / 2'
assert %Success{value: [1, 2, 3, 4], inspected: ~S'[1, 2, 3, 4]'} = ~E'[1, 2] ++ [3, 4]'
assert %Success{value: "abcd", inspected: ~S'"abcd"'} = ~E'"ab" <> "cd"'
assert %Success{value: true, inspected: ~S'true'} = ~E'"abc!" =~ ~r/abc/'
end
test "basic Kernel functions" do
assert %Success{value: 10, inspected: ~S'10'} = ~E'max(5, 10)'
assert %Success{value: "foo", inspected: ~S'"foo"'} = ~E'to_string(:foo)'
assert %Success{value: true, inspected: ~S'true'} = ~E'is_atom(:foo)'
end
test "Kernel guards" do
assert %Success{value: false, inspected: ~S'false'} = ~E'is_binary(55)'
assert %Success{value: true, inspected: ~S'true'} = ~E'is_number(55)'
end
test "basic String functions" do
assert %Success{value: "JoJo", inspected: ~S'"JoJo"'} = ~E'String.replace("jojo", "j", "J")'
end
test "basic Map functions" do
assert %Success{value: %{foo: 5}, inspected: ~S'%{foo: 5}'} = ~E'Map.put(%{}, :foo, 5)'
assert %Success{value: %{}, inspected: ~S'%{}'} = ~E'Map.new()'
end
test "basic :math functions" do
assert %Success{value: 3.0, inspected: ~S'3.0'} = ~E':math.log10(1000)'
assert %Success{value: 3.141592653589793, inspected: ~S'3.141592653589793'} = ~E':math.pi()'
end
@tag :lts_only
test "tuples" do
assert %Success{value: {}, inspected: ~S'{}'} = ~E'{}'
assert %Success{value: {:foo}, inspected: ~S'{:foo}'} = ~E'{:foo}'
assert %Success{value: {"hello", ~c"world"}, inspected: ~S/{"hello", ~c"world"}/} =
~E/{"hello", 'world'}/
assert %Success{value: {1, 2, 3}, inspected: ~S'{1, 2, 3}'} = ~E'{1, 2, 3}'
end
test "map operations" do
assert %Success{value: %{a: :foo, b: 6}, inspected: ~S'%{a: :foo, b: 6}'} = ~E'
map = %{a: 5, b: 6}
%{map | a: :foo}
'
assert %Success{
value: "Dio",
inspected: ~S'"Dio"'
} = ~E'
user = %{first_name: "Dio", last_name: "Brando"}
user.first_name
'
end
test "dynamic module names (authorized functions)" do
assert %Success{value: %{}, inspected: ~S'%{}'} = ~E'module = Map; module.new()'
assert %Success{value: [%{}], inspected: ~S'[%{}]'} =
~E'Enum.map([Map], fn module -> module.new end)'
assert %Success{value: [%{}], inspected: ~S'[%{}]'} =
~E'Enum.map([Map], fn module -> module.new() end)'
assert %Success{value: 3, inspected: ~S'3'} = ~E'apply(List, :last, [[1, 2, 3]])'
end
test "captures" do
assert %Success{value: 33, inspected: ~S'33'} = ~E'f = &+/2; f.(11, 22)'
assert %Success{value: 35, inspected: ~S'35'} = ~E'f = & &1*&2; f.(5, 7)'
assert %Success{value: 1.5, inspected: ~S'1.5'} = ~E'f = & &1/&2; 3 |> f.(2)'
assert %Success{value: 20, inspected: ~S'20'} = ~E'apply(& &1 * 2, [10])'
assert %Success{value: ["1", "2", "3"], inspected: ~S'["1", "2", "3"]'} =
~E'1..3 |> Enum.map(&inspect/1)'
assert %Success{
value: [[0], [1, 0], [2, 0], [3, 0]],
inspected: ~S'[[0], [1, 0], [2, 0], [3, 0]]'
} = ~E'0..30//10 |> Enum.map(&Integer.digits/1)'
assert %Success{value: 3.317550714905183e39, inspected: ~S'3.317550714905183e39'} =
~E'1..100//10 |> Enum.map(&:math.exp/1) |> Enum.sum()'
end
test "anonymous functions" do
assert %Success{value: 0, inspected: ~S'0'} = ~E'f = fn -> _x = 0 end; f.()'
assert %Success{value: 0, inspected: ~S'0'} = ~E'(fn -> _x = 0 end).()'
assert %Success{value: 3, inspected: ~S'3'} = ~E'2 |> (fn x -> x + 1 end).()'
assert %Success{value: -4, inspected: ~S'-4'} = ~E'(&(&2 - &1)).(7, 3)'
assert %Success{value: -4, inspected: ~S'-4'} = ~E'(& &2 - &1).(7, 3)'
assert %Success{value: 3, inspected: ~S'3'} = ~E'2 |> (& &1 + 1).()'
assert %Success{value: 0.2, inspected: ~S'0.2'} = ~E'(& &2 / &1).(10, 2)'
end
@tag :lts_only
test "sigils" do
assert %Success{value: %Regex{source: "(a|b)?c"}, inspected: ~S'~r/(a|b)?c/'} =
~E'~r/(a|b)?c/'
assert %Success{value: ~U[2021-05-20 01:02:03Z], inspected: ~S'~U[2021-05-20 01:02:03Z]'} =
~E'~U[2021-05-20 01:02:03Z]'
assert %Success{
value: [~c"foo", ~c"bar", ~c"baz"],
inspected: ~S'[~c"foo", ~c"bar", ~c"baz"]'
} = ~E'~W[foo bar baz]c'
assert %Success{
value: [~c"foo", ~c"bar", ~c"baz"],
inspected: ~S'[~c"foo", ~c"bar", ~c"baz"]'
} = ~E'~w[#{String.downcase("FOO")} bar baz]c'
assert %Dune.Failure{
message: "** (DuneRestrictedError) function sigil_W/2 is restricted",
type: :restricted
} = ~E'~W[foo bar baz]a'
assert %Dune.Failure{
message: "** (DuneRestrictedError) function sigil_w/2 is restricted",
type: :restricted
} = ~E'~w[#{String.downcase("FOO")} bar baz]a'
end
@tag :lts_only
test "bitstring modifiers" do
assert %Success{
value: <<3>>,
inspected: "<<3>>"
} = ~E'<<3>>'
assert %Success{
value: <<3::4>>,
inspected: "<<3::size(4)>>"
} = ~E'<<3::4>>'
assert %Success{
value: "ߧ",
inspected: ~S'"ߧ"'
} = ~E'<<2023::utf8>>'
assert %Success{
value: 12520,
inspected: "12520"
} = ~E'<<c::utf8>> = "ヨ"; c'
assert %Success{
value: <<3::4>>,
inspected: "<<3::size(4)>>"
} = ~E'<<3::size(4)>>'
assert %Success{
value: 6_382_179,
inspected: "6382179"
} = ~E'<<c::size(24)>> = "abc"; c'
assert %Success{
value: <<2, 3>>,
inspected: "<<2, 3>>"
} = ~E'<<_::binary-size(2), rest::binary>> = <<0, 1, 2, 3>>; rest'
assert %Success{
value: <<0, 0, 0, 1>>,
inspected: "<<0, 0, 0, 1>>"
} = ~E'''
x = 1
<<x::8*4>>
'''
assert %Success{
value: <<0, 0, 0, 1>>,
inspected: "<<0, 0, 0, 1>>"
} = ~E'''
x = 1
<<x::size(8)-unit(4)>>
'''
assert %Success{
value: {"Frank", "Walrus"},
inspected: ~S'{"Frank", "Walrus"}'
} = ~E'''
name_size = 5
<<name::binary-size(^name_size), " the ", species::binary>> = <<"Frank the Walrus">>
{name, species}
{"Frank", "Walrus"}
'''
end
test "binary comprehensions" do
assert %Success{
value: [{213, 45, 132}, {64, 76, 32}],
inspected: "[{213, 45, 132}, {64, 76, 32}]"
} = ~E'''
pixels = <<213, 45, 132, 64, 76, 32>>
for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b}
'''
end
test "block of code" do
assert %Success{
value: "quick-brown-fox-jumps-over-lazy-dog",
inspected: ~S'"quick-brown-fox-jumps-over-lazy-dog"'
} = ~E'
sentence = "the quick brown fox jumps over the lazy dog"
words = String.split(sentence)
filtered = Enum.reject(words, &(&1 == "the"))
Enum.join(filtered, "-")
'
end
test "pipe operator" do
assert %Success{
value: "quick-brown-fox-jumps-over-lazy-dog",
inspected: ~S'"quick-brown-fox-jumps-over-lazy-dog"'
} = ~E'
"the quick brown fox jumps over the lazy dog"
|> String.split()
|> Enum.reject(&(&1 == "the"))
|> Enum.join("-")
'
assert %Success{value: ":foo", inspected: ~S'":foo"'} = ~E':foo |> inspect()'
end
test "atoms" do
assert %Success{value: "foo51", inspected: ~s'"foo51"'} = ~E'to_string(:foo51)'
assert %Success{value: "foo52", inspected: ~s'"foo52"'} = ~E'Atom.to_string(:foo52)'
assert %Success{value: "foo53", inspected: ~s'"foo53"'} = ~E'Enum.join([:foo53])'
assert %Success{value: "boo57", inspected: ~s'"boo57"'} =
~E':foo57 |> to_string() |> String.replace("f", "b")'
assert %Success{value: "Hello boo58", inspected: ~s'"Hello boo58"'} =
~E'"Hello #{:foo58}" |> String.replace("f", "b")'
assert %Success{value: :Dune_Atom_1__, inspected: ~s':Foo12'} = ~E':Foo12'
assert %Success{value: Dune_Module_1__, inspected: ~s'Foo13'} = ~E'Foo13'
assert %Success{value: "Foo14", inspected: ~s'"Foo14"'} = ~E'Atom.to_string(:Foo14)'
assert %Success{value: "Elixir.Foo15", inspected: ~s("Elixir.Foo15")} =
~E'Atom.to_string(Foo15)'
assert %Success{value: ":Foo16", inspected: ~s'":Foo16"'} = ~E'inspect(:Foo16)'
assert %Success{value: "Foo17", inspected: ~s'"Foo17"'} = ~E'inspect(Foo17)'
assert %Success{value: "Elixir.Foo.Bar33", inspected: ~s("Elixir.Foo.Bar33")} =
~E'Atom.to_string(Foo.Bar33)'
assert %Success{
value: [
Dune_Module_1__,
{:a__Dune_atom_2__, :Dune_Atom_1__},
[a__Dune_atom_2__: 15, Dune_Atom_1__: 6, __Dune_atom_3__: 33]
],
inspected: ~s([Foo91, {:foo91, :Foo91}, [foo91: 15, Foo91: 6, _foo92: 33]])
} = ~E'[Foo91, {:foo91, :Foo91}, [foo91: 15, Foo91: 6, _foo92: 33]]'
end
@tag :lts_only
test "atoms to charlist" do
assert %Success{value: ~c"Hello foo59", inspected: ~s'~c"Hello foo59"'} =
~E'~c"Hello #{:foo59}"'
assert %Success{value: ~c"Elixir.Foo15", inspected: ~s(~c"Elixir.Foo15")} =
~E'Atom.to_charlist(Foo15)'
end
test "atoms (prefixed by Elixir)" do
assert %Success{value: Elixir, inspected: ~s'Elixir'} = ~E'Elixir'
assert %Success{value: Dune_Module_1__, inspected: ~s'Foo13'} = ~E'Elixir.Foo13'
assert %Success{value: Dune_Module_1__, inspected: ~s'Foo13'} = ~E':"Elixir.Foo13"'
assert %Success{value: true, inspected: ~s'true'} = ~E'Elixir.Foo13 == Foo13'
assert %Success{value: true, inspected: ~s'true'} = ~E':"Elixir.Foo13" == Foo13'
assert %Success{value: true, inspected: ~s'true'} = ~E'Elixir.Foo13.Foo13 == Foo13.Foo13'
assert %Success{value: true, inspected: ~s'true'} = ~E':"Elixir.Foo13.Foo13" == Foo13.Foo13'
assert %Success{value: String, inspected: ~s'String'} = ~E':"Elixir.String"'
assert %Success{value: Dune_Module_1__, inspected: ~s'Elixir.Elixir'} = ~E'Elixir.Elixir'
assert %Success{value: Dune_Module_1__, inspected: ~s'Elixir.Elixir'} = ~E':"Elixir.Elixir"'
end
test "atoms (wrapped with quotes)" do
assert %Success{value: :__Dune_atom_1__, inspected: ~s':" "'} = ~E':" "'
assert %Success{value: :__Dune_atom_1__, inspected: ~s':"foo/bar"'} = ~E':"foo/bar"'
assert %Success{value: " ", inspected: ~s'" "'} = ~E'to_string :" "'
assert %Success{value: "foo/bar", inspected: ~s'"foo/bar"'} = ~E'to_string :"foo/bar"'
assert %Success{
value: [
__Dune_atom_1__: {:__Dune_atom_2__, :__Dune_atom_3__},
__Dune_atom_4__: %{__Dune_atom_5__: :__Dune_atom_6__, a__Dune_atom_7__: 6}
],
inspected: ~s([" ": {:"\t", :" A"}, "ab cd": %{"foo+91": :"15", abc: 6}])
} = ~E([" ": {:"\t", :" A"}, "ab cd": %{"foo+91": :'15', abc: 6}])
end
test "function and atom parameters" do
assert ":digits" = ~E':digits'.value |> inspect()
assert ":turkic" = ~E':turkic'.value |> inspect()
end
test "stdio capture" do
assert %Success{value: :ok, inspected: ~s(:ok), stdio: "yo!\n"} = ~E'IO.puts("yo!")'
assert %Success{value: :ok, inspected: ~s(:ok), stdio: "foo987\n"} = ~E'IO.puts(:foo987)'
assert %Success{value: :ok, inspected: ~s(:ok), stdio: "hello world!\n"} =
~E'io = IO; io.puts(["hello", ?\s, "world", ?!])'
assert %Success{value: :ok, inspected: ~s(:ok), stdio: "1\n2\n3\n"} =
~E'Enum.each(1..3, &IO.puts/1)'
assert %Success{value: :a__Dune_atom_1__, inspected: ~s(:foo912), stdio: ":foo912\n"} =
~E'IO.inspect(:foo912)'
assert %Success{
value: %{a__Dune_atom_1__: 581},
inspected: ~s(%{foo9101: 581}),
stdio: "bar777: %{foo9101: 581}\n"
} = ~E'%{foo9101: 581} |> IO.inspect(label: :bar777)'
assert %Success{
value: :ok,
inspected: ":ok",
stdio: ":foo9321\nfoo9321\n"
} = ~E'io = IO; io.puts(io.inspect(:foo9321))'
end
test "dbg" do
assert %Success{value: :a__Dune_atom_1__, inspected: ~s(:foo913), stdio: stdio} =
~E'dbg(:foo913)'
assert stdio == """
[nofile:1: (file)]
:foo913 #=> :foo913
"""
assert %Success{value: :a__Dune_atom_1__, inspected: ~s(:foo914), stdio: stdio} =
~E':foo914 |> dbg()'
assert stdio == """
[nofile:1: (file)]
:foo914 #=> :foo914
"""
assert %Success{value: ":foo915", inspected: ~s(":foo915"), stdio: stdio} =
~E':foo915 |> inspect() |> dbg()'
assert stdio == """
[nofile:1: (file)]
:foo915 #=> :foo915
|> inspect() #=> ":foo915"
"""
assert %Success{value: "Hello World", inspected: ~s("Hello World"), stdio: stdio} =
~E'"hello world" |> String.split() |> Enum.map_join(" ", &String.capitalize/1) |> dbg()'
assert stdio == """
[nofile:1: (file)]
"hello world" #=> "hello world"
|> String.split() #=> ["hello", "world"]
|> Enum.map_join(" ", &String.capitalize/1) #=> "Hello World"
"""
assert %Success{value: ":foo987398", inspected: ~s(":foo987398"), stdio: stdio} =
~E':foo987398 |> inspect() |> dbg() |> dbg()'
assert stdio == """
[nofile:1: (file)]
:foo987398 #=> :foo987398
|> inspect() #=> ":foo987398"
[nofile:1: (file)]
:foo987398 |> inspect() |> dbg() #=> ":foo987398"
"""
assert %Dune.Failure{
type: :restricted,
message: "** (DuneRestrictedError) function dbg/0 is restricted"
} = ~E'dbg()'
assert %Dune.Failure{
type: :restricted,
message: "** (DuneRestrictedError) function dbg/2 is restricted"
} = ~E'dbg(:abc, syntax_colors: [])'
assert %Dune.Failure{
type: :restricted,
message: "** (DuneRestrictedError) function Code.eval_string/1 is restricted"
} = ~E'Code.eval_string(":hello") |> dbg()'
assert %Dune.Failure{
type: :restricted,
message: "** (DuneRestrictedError) function Code.eval_string/1 is restricted"
} = ~E'":hello" |> Code.eval_string() |> dbg()'
end
test "pretty option" do
raw_string =
~S'{"This line is really long, maybe we should break", [%{bar: 1, baz: 2}, %{bar: 55}]}'
with_break =
~s'{"This line is really long, maybe we should break",\n [%{bar: 1, baz: 2}, %{bar: 55}]}'
assert %Success{inspected: ^raw_string} =
Dune.eval_string(raw_string, inspect_sort_maps: true)
assert %Success{inspected: ^with_break} =
Dune.eval_string(raw_string, pretty: true, inspect_sort_maps: true)
end
end
describe "Dune restricted" do
test "System calls" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function System.get_env/0 is restricted"
} = ~E'System.get_env()'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function System.get_env/1 is restricted"
} = ~E'System.get_env("TEST")'
end
test "Code calls" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function Code.eval_string/1 is restricted"
} = ~E'Code.eval_string("IO.puts(:hello)")'
end
test "String/List restricted methods" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function String.to_atom/1 is restricted"
} = ~E'String.to_atom("foo")'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function List.to_atom/1 is restricted"
} = ~E/List.to_atom('foo')/
end
test "atom interpolation" do
assert %Failure{
type: :restricted,
message:
"** (DuneRestrictedError) function :erlang.binary_to_existing_atom/2 is restricted"
} = ~E':"#{1 + 1} is two"'
end
test "Kernel apply/3" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function Process.get/0 is restricted"
} = ~E'apply(Process, :get, [])'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function IO.puts/2 is restricted"
} = ~E'apply(IO, :puts, [:stderr, "Hello"])'
end
test ". operator with variable modules" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function :erlang.yield/0 is restricted"
} = ~E'
module = :erlang
module.yield
'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function :erlang.processes/0 is restricted"
} = ~E'
Enum.map([:erlang], fn module -> module.processes end)
'
end
test ". operator as key access" do
assert %Success{value: 100, inspected: ~S'100'} =
~E'users = [john: %{age: 100}]; users[:john].age'
end
test ". operator various failures" do
assert %Failure{
type: :exception,
message:
"** (UndefinedFunctionError) function :foo.bar/0 is undefined (module :foo is not available)"
} = ~E'module = :foo; module.bar()'
assert %Failure{
type: :exception,
message: "** (UndefinedFunctionError) function List.bar/0 is undefined or private"
} = ~E'module = List; module.bar()'
assert %Failure{
type: :exception,
message: "** (KeyError) key :job not found in" <> rest_message
} = ~E'users = [john: %{age: 100}]; users[:john].job'
assert rest_message =~ "%{age: 100}"
assert %Failure{
type: :exception,
message:
"** (UndefinedFunctionError) function Foo1234.bar567/0 is undefined (module Foo1234 is not available)"
} = ~E'Foo1234.bar567.baz890'
end
test "pipe operator" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function Code.eval_string/1 is restricted"
} = ~E'":foo" |> Code.eval_string()'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function Code.eval_string/1 is restricted"
} = ~E'code = Code; "1 + 1" |> code.eval_string()'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function Code.eval_string/1 is restricted"
} = ~E'code = Code; "1 + 1" |> code.eval_string'
end
test "capture operator" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function Code.eval_string/1 is restricted"
} = ~E'f = &Code.eval_string/1'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function Code.eval_string/1 is restricted"
} = ~E'f = &Code.eval_string(&1)'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function Code.eval_string/1 is restricted"
} = ~E'(&Code.eval_string/1).(":pawned!")'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function Code.eval_string/1 is restricted"
} = ~E'(&Code.eval_string(&1)).(":pawned!")'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function Code.eval_string/1 is restricted"
} = ~E'":pawned!" |> (&Code.eval_string/1).()'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function Code.eval_string/1 is restricted"
} = ~E'":pawned!" |> (&Code.eval_string(&1)).()'
end
test "Kernel 0-arity functions" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function binding/0 is restricted"
} = ~E'binding'
end
test "erlang unsafe libs" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function :erlang.processes/0 is restricted"
} = ~E':erlang.processes()'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function :erlang.get/0 is restricted"
} = ~E':erlang.get()'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function :erlang.process_info/1 is restricted"
} = ~E':erlang.process_info(self)'
end
test "nested restricted code" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function Code.eval_string/1 is restricted"
} = ~E'f = fn -> Code.eval_string("IO.puts(:hello)") end'
end
test "partially restricted shims" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function IO.puts/2 is restricted"
} = ~E'IO.puts(:stderr, "foo")'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function IO.puts/2 is restricted"
} = ~E':stderr |> IO.puts("foo")'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function IO.inspect/3 is restricted"
} = ~E'IO.inspect(:stderr, "foo", [])'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function IO.inspect/3 is restricted"
} = ~E'io = IO; io.inspect(:stderr, "foo", [])'
end
test "forbidden atoms" do
assert %Failure{
type: :restricted,
message: "Atoms containing `Dune` are restricted for safety: Dune"
} = ~E'Dune'
assert %Failure{
type: :restricted,
message: "Atoms containing `Dune` are restricted for safety: Dune"
} = ~E'Dune.Foo'
assert %Failure{
type: :restricted,
message: "Atoms containing `Dune` are restricted for safety: Dune"
} = ~E':Dune'
assert %Failure{
type: :restricted,
message: "Atoms containing `Dune` are restricted for safety: __Dune__"
} = ~E':__Dune__'
end
test "forbidden use" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function use/1 is restricted"
} = ~E'use GenServer'
end
test "forbidden import/requires" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function import/1 is restricted"
} = ~E'import Logger'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function import/2 is restricted"
} = ~E'import Logger, only: [info: 2]'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function require/1 is restricted"
} = ~E'require Logger'
end
test "forbidden alias" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function alias/1 is restricted"
} = ~E'alias Task.Supervised'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function alias/2 is restricted"
} = ~E'alias Process, as: P; P.get'
end
test "forbidden quote/unquote" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function quote/1 is restricted"
} = ~E'quote do: 1 + 1'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function quote/1 is restricted"
} = ~E'quote do: unquote(a)'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function unquote/1 is restricted"
} = ~E'unquote(10)'
end
test "forbidden receive" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function receive/1 is restricted"
} = ~E'''
receive do
{:ok, foo} -> foo
end
'''
end
test "forbidden __ENV__" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function __ENV__/0 is restricted"
} = ~E'__ENV__'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) function __ENV__/0 is restricted"
} = ~E'__ENV__.requires'
end
test "bitstring modifiers" do
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) size modifiers above 256 are restricted"
} = ~E'<<0::123456789123456789>>'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) size modifiers above 256 are restricted"
} = ~E'<<0::size(257)>>'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) size modifiers above 256 are restricted"
} = ~E'<<0::256*2>>'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) size modifiers above 256 are restricted"
} = ~E'<<0::integer-size(257)>>'
assert %Failure{
type: :restricted,
message: "** (DuneRestrictedError) size modifiers above 256 are restricted"
} = ~E'<<0::integer-size(256)-unit(2)>>'
assert %Failure{
type: :restricted,
message:
"** (DuneRestrictedError) bitstring modifier is restricted:\n size(x)"
} = ~E'''
x = 123456789123456789
<<0::size(x)>>
'''
assert %Failure{
message:
"** (DuneRestrictedError) bitstring modifier is restricted:\n unquote(1)"
} = ~E'<<1::unquote(1)>>'
assert %Failure{
message:
"** (DuneRestrictedError) bitstring modifier is restricted:\n unquote(1)"
} = ~E'<<1::integer-unquote(1)>>'
assert %Failure{
message: "** (DuneRestrictedError) function unquote/1 is restricted"
} = ~E'<<unquote(1)>>'
end
end
describe "process restrictions" do
test "execution timeout" do
assert %Failure{type: :timeout, message: "Execution timeout - 100ms"} =
~E'Process.sleep(101)'
end
test "too many reductions" do
assert %Failure{type: :reductions, message: "Execution stopped - reductions limit exceeded"} =
~E'Enum.any?(1..1_000_000, &(&1 < 0))'
end
test "uses to much memory" do
assert %Failure{type: :memory, message: "Execution stopped - memory limit exceeded"} =
~E'List.duplicate(:foo, 100_000)'
end
end
describe "error handling" do
test "math error" do
assert %Failure{
type: :exception,
message: "** (ArithmeticError) bad argument in arithmetic expression\n" <> rest
} = ~E'42 / 0'
assert rest =~ ":erlang./(42, 0)"
end
test "throw" do
assert %Failure{type: :throw, message: "** (throw) :yo"} = ~E'throw(:yo)'
assert %Failure{type: :throw, message: "** (throw) {:undefined_function, Kernel, :+, 2}"} =
~E'throw({:undefined_function, Kernel, :+, 2})'
end
test "raise" do
assert %Failure{type: :exception, message: "** (ArgumentError) kaboom!"} =
~E'raise ArgumentError, "kaboom!"'
end
test "actual UndefinedFunctionError" do
assert %Failure{
type: :exception,
message: "** (UndefinedFunctionError) function Code.baz/0 is undefined or private"
} = ~E'Code.baz()'
assert %Failure{
type: :exception,
message:
"** (UndefinedFunctionError) function FooBar.baz/0 is undefined (module FooBar is not available)"
} = ~E'FooBar.baz()'
assert %Failure{
type: :exception,
message:
"** (UndefinedFunctionError) function :foo_bar.baz/0 is undefined (module :foo_bar is not available)"
} = ~E':foo_bar.baz()'
end
@tag :lts_only
test "syntax error" do
assert %Failure{
type: :parsing,
message: "missing terminator: )"
} = ~E'foo('
assert %Failure{
type: :parsing,
message: "missing terminator: }"
} = ~E'{'
assert %Failure{
type: :parsing,
message: "unexpected reserved word: do. In case you wanted to write " <> _
} = ~E'if true, do'
# TODO improve message
assert %Failure{type: :parsing, message: "syntax error before: "} = ~E'%'
assert %Failure{type: :parsing, message: "syntax error before: foo120987"} =
~E'<<>>foo120987'
end
test "max length" do
assert %Failure{
type: :parsing,
message: "max code length exceeded: 26 > 10"
} = Dune.eval_string("exceeeds_max_length = true", max_length: 10)
end
test "atom pool" do
assert %Failure{
type: :parsing,
message: "atom_pool_size exceeded, failed to parse atom: bar1462"
} = Dune.eval_string("{foo5345, bar1462} = {9, 10}", atom_pool_size: 4)
end
test "invalid pipe" do
assert %Failure{
type: :exception,
message: "** (ArgumentError) cannot pipe 1 into 2, can only pipe into " <> _
} = ~E'1 |> 2'
assert %Failure{
type: :exception,
m
gitextract_bo31q11i/
├── .formatter.exs
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── lib/
│ ├── dune/
│ │ ├── allowlist/
│ │ │ ├── default.ex
│ │ │ ├── docs.ex
│ │ │ └── spec.ex
│ │ ├── allowlist.ex
│ │ ├── atom_mapping.ex
│ │ ├── eval/
│ │ │ ├── env.ex
│ │ │ ├── fake_module.ex
│ │ │ ├── function_clause_error.ex
│ │ │ ├── macro_env.ex
│ │ │ └── process.ex
│ │ ├── eval.ex
│ │ ├── failure.ex
│ │ ├── helpers/
│ │ │ ├── diagnostics.ex
│ │ │ └── term_checker.ex
│ │ ├── opts.ex
│ │ ├── parser/
│ │ │ ├── atom_encoder.ex
│ │ │ ├── compile_env.ex
│ │ │ ├── debug.ex
│ │ │ ├── real_module.ex
│ │ │ ├── safe_ast.ex
│ │ │ ├── sanitizer.ex
│ │ │ ├── string_parser.ex
│ │ │ └── unsafe_ast.ex
│ │ ├── parser.ex
│ │ ├── session.ex
│ │ ├── shims/
│ │ │ ├── atom.ex
│ │ │ ├── enum.ex
│ │ │ ├── io.ex
│ │ │ ├── json.ex
│ │ │ ├── kernel.ex
│ │ │ ├── list.ex
│ │ │ └── string.ex
│ │ └── success.ex
│ └── dune.ex
├── mix.exs
└── test/
├── dune/
│ ├── allowlist/
│ │ └── default_test.exs
│ ├── allowlist_test.exs
│ ├── atom_mapping_test.exs
│ ├── opts_test.exs
│ ├── parser/
│ │ ├── atom_encoder_test.exs
│ │ └── string_parser_test.exs
│ ├── session_test.exs
│ ├── shims_test.exs
│ └── validation_test.exs
├── dune_modules_test.exs
├── dune_oom_safety_test.exs
├── dune_quoted_test.exs
├── dune_string_test.exs
├── dune_string_to_quoted_test.exs
├── dune_test.exs
└── test_helper.exs
SYMBOL INDEX (257 symbols across 49 files)
FILE: lib/dune.ex
class Dune (line 1) | defmodule Dune
method eval_quoted (line 98) | def eval_quoted(ast, opts \\ []) do
FILE: lib/dune/allowlist.ex
class Dune.Allowlist (line 1) | defmodule Dune.Allowlist
method extract_extend_opt (line 155) | defp extract_extend_opt(opts, caller) do
method __postprocess__ (line 166) | def __postprocess__(module) do
method generate_spec (line 187) | defp generate_spec(module, extend) do
method def_spec (line 200) | defp def_spec(spec) do
method def_fun_status (line 210) | defp def_fun_status(spec) do
method update_module_doc (line 235) | defp update_module_doc(module, spec) do
method alias_atoms (line 250) | defp alias_atoms(spec) do
FILE: lib/dune/allowlist/default.ex
class Dune.Allowlist.Default (line 1) | defmodule Dune.Allowlist.Default
FILE: lib/dune/allowlist/docs.ex
class Dune.Allowlist.Docs (line 1) | defmodule Dune.Allowlist.Docs
method document_allowlist (line 4) | def document_allowlist(spec) do
method do_doc_funs (line 22) | defp do_doc_funs({module, grouped_funs}) do
method format_fun (line 39) | defp format_fun(module, {fun, arity}, status, public_funs) do
method maybe_strike (line 67) | defp maybe_strike(:restricted), do: "~~"
method maybe_strike (line 68) | defp maybe_strike(_status), do: []
method format_status (line 70) | defp format_status(:allowed), do: "Allowed"
method format_status (line 71) | defp format_status(:shimmed), do: "Alernative implementation"
method format_status (line 72) | defp format_status(:restricted), do: "Restricted"
FILE: lib/dune/allowlist/spec.ex
class Dune.Allowlist.Spec (line 1) | defmodule Dune.Allowlist.Spec
method new (line 13) | def new do
method list_fun_statuses (line 18) | def list_fun_statuses(%__MODULE__{modules: modules}) do
method list_ordered_modules (line 27) | def list_ordered_modules(%__MODULE__{modules: modules}) do
method group_funs_by_status (line 35) | defp group_funs_by_status(funs) do
method extract_status_atom (line 45) | defp extract_status_atom(:restricted), do: :restricted
method extract_status_atom (line 46) | defp extract_status_atom(:allowed), do: :allowed
method extract_status_atom (line 47) | defp extract_status_atom({:shimmed, _, _}), do: :shimmed
method status_sort (line 49) | defp status_sort(:allowed), do: 1
method status_sort (line 50) | defp status_sort(:shimmed), do: 2
method status_sort (line 51) | defp status_sort(:restricted), do: 3
method classify_functions (line 70) | defp classify_functions(functions, :all) do
method classify_functions (line 82) | defp classify_functions(functions, opts) do
method do_classify (line 98) | defp do_classify(functions, set_list, member_atom, non_member_atom) do
method to_atom_set (line 111) | defp to_atom_set(list) do
method unwrap_classify (line 118) | defp unwrap_classify({_, remaining}) do
method shim_functions (line 123) | defp shim_functions(functions, shims) do
method validate_shim! (line 137) | defp validate_shim!(module, fun_name, arity) do
FILE: lib/dune/atom_mapping.ex
class Dune.AtomMapping (line 1) | defmodule Dune.AtomMapping
method new (line 19) | def new do
method add_modules (line 31) | def add_modules(mapping = %__MODULE__{}, list) do
method build_mapping (line 35) | defp build_mapping(list) do
method do_replace_in_string (line 92) | defp do_replace_in_string(mapping = %__MODULE__{}, string) do
method build_replace_map (line 101) | defp build_replace_map(map, sub_mapping, extra_info, to_string_fun) do
method replace_in_result (line 115) | def replace_in_result(mapping, result)
method replace_in_result (line 117) | def replace_in_result(mapping, %Success{} = success) do
method replace_in_result (line 125) | def replace_in_result(mapping, %Failure{} = error) do
method fetch_existing_atom (line 141) | defp fetch_existing_atom(mapping, "Elixir." <> module_name) do
method fetch_existing_atom (line 149) | defp fetch_existing_atom(mapping, atom_name) do
FILE: lib/dune/eval.ex
class Dune.Eval (line 1) | defmodule Dune.Eval
method run (line 14) | def run(parsed, opts, previous_session \\ nil)
method run (line 16) | def run(
method run (line 38) | def run(%Failure{} = failure, _opts, _bindings), do: failure
method do_run (line 40) | defp do_run(ast, atom_mapping, opts, env, bindings) do
method prepend_parser_stdio (line 52) | defp prepend_parser_stdio(result, ""), do: result
method prepend_parser_stdio (line 54) | defp prepend_parser_stdio(result, parser_stdio) do
method safe_eval (line 58) | defp safe_eval(safe_ast, env, bindings, pretty, sort_maps) do
method do_safe_eval (line 68) | defp do_safe_eval(safe_ast, env, nil, inspect_opts) do
method eval_quoted (line 92) | defp eval_quoted(safe_ast, binding) do
FILE: lib/dune/eval/env.ex
class Dune.Eval.Env (line 1) | defmodule Dune.Eval.Env
method fetch_fake_function (line 38) | defp fetch_fake_function(%{fake_modules: modules}, module, fun_name, a...
FILE: lib/dune/eval/fake_module.ex
class Dune.Eval.FakeModule (line 1) | defmodule Dune.Eval.FakeModule
FILE: lib/dune/eval/function_clause_error.ex
class Dune.Eval.FunctionClauseError (line 1) | defmodule Dune.Eval.FunctionClauseError
method message (line 6) | def message(err = %__MODULE__{function: function, args: args}) do
FILE: lib/dune/eval/macro_env.ex
class Dune.Eval.MacroEnv (line 1) | defmodule Dune.Eval.MacroEnv
method make_env (line 6) | def make_env do
FILE: lib/dune/eval/process.ex
class Dune.Eval.Process (line 1) | defmodule Dune.Eval.Process
method do_run (line 13) | defp do_run(fun, opts, string_io) do
method with_string_io (line 40) | defp with_string_io(fun) do
method spawn_trapped_process (line 51) | defp spawn_trapped_process(fun, max_heap_size, max_reductions, string_...
method catch_diagnostics (line 109) | defp catch_diagnostics(fun) do
method format_error (line 145) | defp format_error(error, stacktrace)
method format_error (line 147) | defp format_error({:nocatch, value}, _stacktrace) do
method format_error (line 155) | defp format_error(error, stacktrace) do
method format_compile_error (line 175) | defp format_compile_error(error, diagnostics, stacktrace) do
FILE: lib/dune/failure.ex
class Dune.Failure (line 1) | defmodule Dune.Failure
method restricted_function (line 28) | def restricted_function(module, fun, arity) do
method undefined_module (line 36) | def undefined_module(module, function, arity) do
method undefined_function (line 46) | def undefined_function(module, function, arity) do
method base_undefined_message (line 53) | defp base_undefined_message(module, function, arity) do
method format_function (line 62) | defp format_function(module, fun, arity) do
FILE: lib/dune/helpers/diagnostics.ex
class Dune.Helpers.Diagnostics (line 1) | defmodule Dune.Helpers.Diagnostics
method prepend_diagnostics (line 12) | def prepend_diagnostics(result, []), do: result
method prepend_diagnostics (line 14) | def prepend_diagnostics(result, diagnostics) do
method format_diagnostics (line 19) | def format_diagnostics(diagnostics) do
method format_pos (line 28) | defp format_pos({line, col}), do: [Integer.to_string(line), ?:, Intege...
FILE: lib/dune/helpers/term_checker.ex
class Dune.Helpers.TermChecker (line 1) | defmodule Dune.Helpers.TermChecker
method check (line 11) | def check(term), do: do_check(term)
method do_check (line 19) | defp do_check([left | right]) do
FILE: lib/dune/opts.ex
class Dune.Opts (line 1) | defmodule Dune.Opts
method validate! (line 134) | def validate!(opts) do
method do_validate (line 174) | defp do_validate(opts = %{allowlist: allowlist}) do
FILE: lib/dune/parser.ex
class Dune.Parser (line 1) | defmodule Dune.Parser
method do_parse_string (line 21) | defp do_parse_string(string, opts = %{max_length: max_length}, previou...
method parse_quoted (line 32) | def parse_quoted(quoted, opts = %Opts{}, previous_session \\ nil) do
method unsafe_quoted (line 40) | def unsafe_quoted(ast) do
method get_compile_env (line 44) | defp get_compile_env(opts, nil) do
method get_compile_env (line 48) | defp get_compile_env(opts, %{compile_env: compile_env}) do
method string_to_quoted (line 53) | def string_to_quoted(string, opts) do
FILE: lib/dune/parser/atom_encoder.ex
class Dune.Parser.AtomEncoder (line 1) | defmodule Dune.Parser.AtomEncoder
method load_atom_mapping (line 137) | def load_atom_mapping(nil), do: :ok
method load_atom_mapping (line 139) | def load_atom_mapping(%AtomMapping{atoms: atoms}) do
method categorize_atom_binary (line 166) | def categorize_atom_binary(atom_binary) do
method do_static_atoms_encoder (line 177) | defp do_static_atoms_encoder("Elixir." <> rest, :alias, pool_size) do
method do_static_atoms_encoder (line 183) | defp do_static_atoms_encoder(binary, atom_category, pool_size) do
method do_static_atoms_encoder (line 192) | defp do_static_atoms_encoder(binary, atom_category, process_key, pool_...
method encode_many_atoms (line 211) | defp encode_many_atoms([], _pool_size, acc) do
method encode_many_atoms (line 215) | defp encode_many_atoms([head | tail], pool_size, acc) do
method plain_atom_mapping (line 223) | def plain_atom_mapping() do
method new_atom (line 237) | defp new_atom(atom_category, pool_size) do
method do_new_atom (line 249) | defp do_new_atom(:alias, count) do
method do_new_atom (line 253) | defp do_new_atom(:public_var, count) do
method encode_modules (line 263) | def encode_modules(ast, plain_atom_mapping, existing_mapping) do
method get_module_acc (line 281) | defp get_module_acc(nil), do: %{}
method get_module_acc (line 283) | defp get_module_acc(%AtomMapping{atoms: atoms, modules: modules}) do
method remove_elixir_prefix (line 292) | defp remove_elixir_prefix(atoms = [Elixir, Elixir | _]), do: atoms
method remove_elixir_prefix (line 294) | defp remove_elixir_prefix(atoms), do: atoms
method map_modules_ast (line 296) | defp map_modules_ast(atoms, acc) do
method build_module_mapping (line 315) | defp build_module_mapping(acc, plain_atom_mapping) do
FILE: lib/dune/parser/compile_env.ex
class Dune.Parser.CompileEnv (line 1) | defmodule Dune.Parser.CompileEnv
method new (line 17) | def new(allowlist) do
method module_already_exists? (line 36) | defp module_already_exists?(module, fake_modules) do
method resolve_module (line 70) | defp resolve_module(nil, fun_name, arity) do
method resolve_module (line 78) | defp resolve_module(module, _fun_name, _arity), do: module
method resolve_fake_module (line 80) | defp resolve_fake_module(%{module: nil}, nil, _fun_name, _arity), do: ...
method resolve_fake_module (line 82) | defp resolve_fake_module(env = %{module: module}, nil, fun_name, arity...
method resolve_fake_module (line 86) | defp resolve_fake_module(env, module, fun_name, arity) do
method check_private (line 96) | defp check_private(_env, module, :def), do: {:fake, module}
method check_private (line 97) | defp check_private(%{module: module}, module, :defp), do: {:fake, module}
method check_private (line 98) | defp check_private(_env, _module, _def), do: :undefined_function
FILE: lib/dune/parser/debug.ex
class Dune.Parser.Debug (line 1) | defmodule Dune.Parser.Debug
method io_debug (line 4) | def io_debug(ast) do
method ast_to_string (line 17) | defp ast_to_string({:__block__, _, list}) do
method ast_to_string (line 21) | defp ast_to_string(ast) do
FILE: lib/dune/parser/real_module.ex
class Dune.Parser.RealModule (line 1) | defmodule Dune.Parser.RealModule
method elixir_module? (line 5) | def elixir_module?(module) do
method list_functions (line 11) | def list_functions(module)
method list_functions (line 13) | def list_functions(Kernel.SpecialForms) do
method fun_exists? (line 26) | def fun_exists?(module, fun_name, arity) do
method fun_status (line 31) | def fun_status(module, fun_name, arity)
method fun_status (line 33) | def fun_status(Kernel.SpecialForms, fun_name, arity) do
method fun_status (line 41) | def fun_status(module, fun_name, arity) do
FILE: lib/dune/parser/safe_ast.ex
class Dune.Parser.SafeAst (line 1) | defmodule Dune.Parser.SafeAst
FILE: lib/dune/parser/sanitizer.ex
class Dune.Parser.Sanitizer (line 1) | defmodule Dune.Parser.Sanitizer
method sanitize (line 10) | def sanitize(unsafe = %UnsafeAst{}, compile_env = %CompileEnv{}) do
method sanitize (line 86) | def sanitize(%Failure{} = failure, _opts), do: failure
method try_sanitize (line 95) | defp try_sanitize(ast, env) do
method do_sanitize_main (line 110) | defp do_sanitize_main({:__block__, ctx, list}, env) do
method do_sanitize_main (line 116) | defp do_sanitize_main(single, env) do
method defmodule_block? (line 157) | defp defmodule_block?({:defmodule, _, _}), do: true
method defmodule_block? (line 158) | defp defmodule_block?(_), do: false
method parse_module_definition (line 160) | defp parse_module_definition({:defmodule, ctx, [module_name, [do: do_a...
method parse_module_definition (line 166) | defp parse_module_definition(ast = {:defmodule, _, _}) do
method validate_module_name (line 175) | defp validate_module_name(module_def = {:__aliases__, _, [_module_atom...
method validate_module_name (line 179) | defp validate_module_name(module_ast, ctx) do
method do_parse_module_definition (line 183) | defp do_parse_module_definition(module_name, do_ast) do
method parse_fun_definition (line 218) | defp parse_fun_definition(unsupported_ast) do
method extract_default_args (line 224) | defp extract_default_args([], _index, arg_acc, defaults) do
method extract_default_args (line 228) | defp extract_default_args([{:\\, _, [arg, default]} | args], index, ar...
method extract_default_args (line 232) | defp extract_default_args([arg | args], index, arg_acc, defaults) do
method expand_defaults (line 236) | defp expand_defaults({name_arity, definition, _defaults = []}) do
method expand_defaults (line 240) | defp expand_defaults({name_arity = {name, arity}, definition, defaults...
method do_expand_defaults (line 245) | defp do_expand_defaults(_name, _arity, _def_or_defp, [], acc) do
method do_expand_defaults (line 249) | defp do_expand_defaults(
method check_definition_conflicts (line 274) | defp check_definition_conflicts(grouped_definitions) do
method check_definition_conflict (line 280) | defp check_definition_conflict(_name_arity, _, []), do: :ok
method check_definition_conflict (line 282) | defp check_definition_conflict(
method check_definition_conflict (line 292) | defp check_definition_conflict(name_arity, {previous_def, previous_ctx...
method parse_fun_signature (line 300) | defp parse_fun_signature({:when, _, [header, guards]}) do
method parse_fun_signature (line 304) | defp parse_fun_signature(header) do
method sanitize_module_definition (line 308) | defp sanitize_module_definition({module, fun_defs}, env) do
method sanitize_fun (line 332) | defp sanitize_fun({{fun_name, arity}, definitions}, env) do
method sanitize_fun_clause (line 365) | defp sanitize_fun_clause({_def_or_defp, ctx, args, body, guards}, env) do
method definition_to_clause (line 372) | defp definition_to_clause(ctx, args, body, _guards = nil) do
method env_variable_if_used (line 381) | defp env_variable_if_used(asts) do
method uses_variable? (line 390) | defp uses_variable?([], _variable_name), do: false
method uses_variable? (line 392) | defp uses_variable?([head | tail], variable_name) do
method uses_variable? (line 399) | defp uses_variable?({variable_name, _, nil}, variable_name), do: true
method uses_variable? (line 405) | defp uses_variable?({x, y}, variable_name) do
method uses_variable? (line 409) | defp uses_variable?(_ast, _variable_name), do: false
method do_sanitize (line 411) | defp do_sanitize(ast, env)
method do_sanitize (line 423) | defp do_sanitize({arg1, arg2}, env) do
method do_sanitize (line 441) | defp do_sanitize({:&, _, args}, env) do
method do_sanitize (line 447) | defp do_sanitize({:<<>>, meta, args}, env) do
method do_sanitize (line 481) | defp do_sanitize({:dbg, meta, [expr]}, env) do
method do_sanitize (line 499) | defp do_sanitize({:|>, _, [expr, {:dbg, meta, []}]}, env) do
method do_sanitize (line 512) | defp do_sanitize({:|>, _, _} = ast, env) do
method try_expand_once (line 523) | defp try_expand_once(ast) do
method do_sanitize_dot (line 530) | defp do_sanitize_dot(left, key, args, ctx, env) do
method extract_module_and_fun (line 603) | defp extract_module_and_fun({:., _, [{:__aliases__, _, modules}, func_...
method handle_mfa_error (line 657) | defp handle_mfa_error(:undefined_module, module, func_name, arity) do
method handle_mfa_error (line 661) | defp handle_mfa_error(:undefined_function, module, func_name, arity) do
method sanitize_capture (line 670) | defp sanitize_capture(capture_arg, env) do
method dbg_pipeline (line 708) | defp dbg_pipeline(expr, env, header) do
method check_bin_modifier (line 728) | defp check_bin_modifier(modifier) do
method check_bin_modifier_size (line 739) | defp check_bin_modifier_size({:-, _, [left, right]}, size, unit) do
method check_bin_modifier_size (line 744) | defp check_bin_modifier_size(modifier, size, unit) do
method env_variable (line 772) | defp env_variable do
method underscore_env_variable (line 776) | defp underscore_env_variable do
method authorized_var_name? (line 780) | defp authorized_var_name?(name) do
FILE: lib/dune/parser/string_parser.ex
class Dune.Parser.StringParser (line 1) | defmodule Dune.Parser.StringParser
method do_parse_string (line 21) | defp do_parse_string(
method maybe_load_atom_mapping (line 45) | defp maybe_load_atom_mapping(nil), do: :ok
method maybe_load_atom_mapping (line 47) | defp maybe_load_atom_mapping(%{atom_mapping: atom_mapping}) do
method maybe_encode_modules (line 51) | defp maybe_encode_modules(ast, previous_session, encode_modules?) do
method handle_failure (line 65) | defp handle_failure("Atoms containing" <> _ = error, token) do
method handle_failure (line 69) | defp handle_failure(error, token) do
FILE: lib/dune/parser/unsafe_ast.ex
class Dune.Parser.UnsafeAst (line 1) | defmodule Dune.Parser.UnsafeAst
FILE: lib/dune/session.ex
class Dune.Session (line 1) | defmodule Dune.Session
method new (line 50) | def new do
method eval_string (line 82) | def eval_string(session = %__MODULE__{}, string, opts \\ []) do
method add_result_to_session (line 93) | defp add_result_to_session(result = %Success{value: {value, env, bindi...
method add_result_to_session (line 100) | defp add_result_to_session(result = %Failure{}, session, _) do
FILE: lib/dune/shims/atom.ex
class Dune.Shims.Atom (line 1) | defmodule Dune.Shims.Atom
FILE: lib/dune/shims/enum.ex
class Dune.Shims.Enum (line 1) | defmodule Dune.Shims.Enum
FILE: lib/dune/shims/io.ex
class Dune.Shims.IO (line 1) | defmodule Dune.Shims.IO
method puts (line 6) | def puts(env, device \\ :stdio, item)
method puts (line 8) | def puts(env, :stdio, item) do
method puts (line 14) | def puts(_env, _device, _item) do
method inspect (line 19) | def inspect(env, item, opts \\ []) do
FILE: lib/dune/shims/kernel.ex
class Dune.Shims.Kernel (line 1) | defmodule Dune.Shims.Kernel
method safe_throw (line 29) | def safe_throw(_env, value) do
method safe_dot (line 48) | def safe_dot(_env, %{} = map, key) do
method safe_inspect (line 87) | def safe_inspect(env, term, opts \\ [])
method safe_inspect (line 98) | def safe_inspect(env, term, opts) do
method safe_to_string (line 112) | def safe_to_string(_env, other), do: to_string(other)
method safe_to_charlist (line 118) | def safe_to_charlist(_env, other), do: to_charlist(other)
FILE: lib/dune/shims/list.ex
class Dune.Shims.List (line 1) | defmodule Dune.Shims.List
method do_to_string (line 21) | defp do_to_string(elem), do: elem
method to_existing_atom (line 33) | def to_existing_atom(_env, list) do
FILE: lib/dune/shims/string.ex
class Dune.Shims.String (line 1) | defmodule Dune.Shims.String
method to_existing_atom (line 12) | def to_existing_atom(_env, string) do
FILE: lib/dune/success.ex
class Dune.Success (line 1) | defmodule Dune.Success
FILE: mix.exs
class Dune.MixProject (line 1) | defmodule Dune.MixProject
method project (line 7) | def project do
method application (line 25) | def application do
method deps (line 32) | defp deps do
method package (line 41) | defp package do
method aliases (line 50) | defp aliases do
method cli (line 54) | def cli do
method docs (line 58) | defp docs do
FILE: test/dune/allowlist/default_test.exs
class Dune.Allowlist.DefaultTest (line 1) | defmodule Dune.Allowlist.DefaultTest
FILE: test/dune/allowlist_test.exs
class Dune.AllowlistTest (line 1) | defmodule Dune.AllowlistTest
FILE: test/dune/atom_mapping_test.exs
class Dune.AtomMappingTest (line 1) | defmodule Dune.AtomMappingTest
FILE: test/dune/opts_test.exs
class Dune.OptsTest (line 1) | defmodule Dune.OptsTest
FILE: test/dune/parser/atom_encoder_test.exs
class Dune.Parser.AtomEncoderTest (line 1) | defmodule Dune.Parser.AtomEncoderTest
FILE: test/dune/parser/string_parser_test.exs
class Dune.Parser.StringParserTest (line 1) | defmodule Dune.Parser.StringParserTest
FILE: test/dune/session_test.exs
class Dune.SessionTest (line 1) | defmodule Dune.SessionTest
FILE: test/dune/shims_test.exs
class Dune.ShimsTest (line 1) | defmodule Dune.ShimsTest
FILE: test/dune/validation_test.exs
class Dune.ValidationTest (line 1) | defmodule Dune.ValidationTest
FILE: test/dune_modules_test.exs
class DuneModulesTest (line 1) | defmodule DuneModulesTest
FILE: test/dune_oom_safety_test.exs
class Dune.AssertionHelper (line 1) | defmodule Dune.AssertionHelper
class Dune.OOMSafetyTest (line 15) | defmodule Dune.OOMSafetyTest
FILE: test/dune_quoted_test.exs
class DuneQuotedTest (line 1) | defmodule DuneQuotedTest
FILE: test/dune_string_test.exs
class DuneStringTest (line 1) | defmodule DuneStringTest
FILE: test/dune_string_to_quoted_test.exs
class DuneStringToQuotedTest (line 1) | defmodule DuneStringToQuotedTest
FILE: test/dune_test.exs
class DuneTest (line 1) | defmodule DuneTest
Condensed preview — 57 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (208K chars).
[
{
"path": ".formatter.exs",
"chars": 239,
"preview": "# Used by \"mix format\"\nlocals_without_parens = [allow: 2]\n\n[\n inputs: [\"{mix,.formatter}.exs\", \"{config,lib,test}/**/*."
},
{
"path": ".github/workflows/ci.yml",
"chars": 1349,
"preview": "name: CI\non: [push, pull_request]\n\njobs:\n test:\n runs-on: ubuntu-latest\n name: OTP ${{matrix.otp}} / Elixir ${{ma"
},
{
"path": ".gitignore",
"chars": 626,
"preview": "# The directory Mix will write compiled artifacts to.\n/_build/\n\n# If you run \"mix test --cover\", coverage assets end up "
},
{
"path": "CHANGELOG.md",
"chars": 3796,
"preview": "# Changelog\n\n## Dev\n\n## v0.3.15 (2025-10-19)\n\n- Support Elixir 1.19\n\n## v0.3.14 (2025-07-29)\n\n### Security fixes\n\n- Use "
},
{
"path": "LICENSE.md",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) 2021 Sabiwara\n\nPermission is hereby granted, free of charge, to any person obtaining a copy o"
},
{
"path": "README.md",
"chars": 6006,
"preview": "# Dune\n\n[](https://hex.pm/packages/dune)\n[ do\n spec\n |> Dune.Allowlist.Sp"
},
{
"path": "lib/dune/allowlist/spec.ex",
"chars": 4339,
"preview": "defmodule Dune.Allowlist.Spec do\n @moduledoc false\n\n alias Dune.Allowlist\n alias Dune.Parser.RealModule\n\n @type t ::"
},
{
"path": "lib/dune/allowlist.ex",
"chars": 7500,
"preview": "defmodule Dune.Allowlist do\n @moduledoc \"\"\"\n Behaviour to customize the modules and functions that are allowed or rest"
},
{
"path": "lib/dune/atom_mapping.ex",
"chars": 4788,
"preview": "defmodule Dune.AtomMapping do\n @moduledoc false\n\n alias Dune.{Success, Failure}\n\n @type substitute_atom :: atom\n @ty"
},
{
"path": "lib/dune/eval/env.ex",
"chars": 1568,
"preview": "defmodule Dune.Eval.Env do\n @moduledoc false\n\n alias Dune.AtomMapping\n alias Dune.Eval.FakeModule\n\n @type t :: %__MO"
},
{
"path": "lib/dune/eval/fake_module.ex",
"chars": 459,
"preview": "defmodule Dune.Eval.FakeModule do\n @moduledoc false\n\n @type t :: %__MODULE__{\n public_funs: %{optional(atom) "
},
{
"path": "lib/dune/eval/function_clause_error.ex",
"chars": 395,
"preview": "defmodule Dune.Eval.FunctionClauseError do\n @moduledoc false\n\n defexception [:module, :function, :args]\n\n def message"
},
{
"path": "lib/dune/eval/macro_env.ex",
"chars": 344,
"preview": "defmodule Dune.Eval.MacroEnv do\n @moduledoc false\n\n # Recommended way to generate a Macro.Env struct\n # https://hexdo"
},
{
"path": "lib/dune/eval/process.ex",
"chars": 5076,
"preview": "defmodule Dune.Eval.Process do\n @moduledoc false\n\n alias Dune.Failure\n alias Dune.Helpers.Diagnostics\n\n def run(fun,"
},
{
"path": "lib/dune/eval.ex",
"chars": 2893,
"preview": "defmodule Dune.Eval do\n @moduledoc false\n\n alias Dune.{AtomMapping, Success, Failure, Opts}\n alias Dune.Eval.Env\n al"
},
{
"path": "lib/dune/failure.ex",
"chars": 1917,
"preview": "defmodule Dune.Failure do\n @moduledoc \"\"\"\n A struct returned when `Dune` parsing or evaluation fails.\n\n Fields:\n - `"
},
{
"path": "lib/dune/helpers/diagnostics.ex",
"chars": 1344,
"preview": "defmodule Dune.Helpers.Diagnostics do\n @moduledoc false\n\n # used for formatting errors and warnings consistently\n\n @t"
},
{
"path": "lib/dune/helpers/term_checker.ex",
"chars": 835,
"preview": "defmodule Dune.Helpers.TermChecker do\n @moduledoc false\n\n defguardp is_simple_term(term)\n when is_atom(term"
},
{
"path": "lib/dune/opts.ex",
"chars": 6377,
"preview": "defmodule Dune.Opts do\n @moduledoc \"\"\"\n Defines and validates the options for `Dune`.\n\n The available options are exp"
},
{
"path": "lib/dune/parser/atom_encoder.ex",
"chars": 9900,
"preview": "defmodule Dune.Parser.AtomEncoder do\n @moduledoc false\n\n alias Dune.AtomMapping\n\n @type atom_category :: :alias | :pr"
},
{
"path": "lib/dune/parser/compile_env.ex",
"chars": 2917,
"preview": "defmodule Dune.Parser.CompileEnv do\n @moduledoc false\n\n @type name_arity :: {atom, non_neg_integer}\n @type maybe_fake"
},
{
"path": "lib/dune/parser/debug.ex",
"chars": 431,
"preview": "defmodule Dune.Parser.Debug do\n @moduledoc false\n\n def io_debug(ast) do\n debug(ast) |> IO.puts()\n ast\n end\n\n d"
},
{
"path": "lib/dune/parser/real_module.ex",
"chars": 1305,
"preview": "defmodule Dune.Parser.RealModule do\n @moduledoc false\n\n @spec elixir_module?(module) :: boolean\n def elixir_module?(m"
},
{
"path": "lib/dune/parser/safe_ast.ex",
"chars": 337,
"preview": "defmodule Dune.Parser.SafeAst do\n @moduledoc false\n\n @type t :: %__MODULE__{\n ast: Macro.t(),\n atom_"
},
{
"path": "lib/dune/parser/sanitizer.ex",
"chars": 22740,
"preview": "defmodule Dune.Parser.Sanitizer do\n @moduledoc false\n\n alias Dune.{Failure, AtomMapping, Opts}\n alias Dune.Parser.{Co"
},
{
"path": "lib/dune/parser/string_parser.ex",
"chars": 2760,
"preview": "defmodule Dune.Parser.StringParser do\n @moduledoc false\n\n alias Dune.{AtomMapping, Failure, Opts}\n alias Dune.Helpers"
},
{
"path": "lib/dune/parser/unsafe_ast.ex",
"chars": 274,
"preview": "defmodule Dune.Parser.UnsafeAst do\n @moduledoc false\n\n @type t :: %__MODULE__{\n ast: Macro.t(),\n ato"
},
{
"path": "lib/dune/parser.ex",
"chars": 2066,
"preview": "defmodule Dune.Parser do\n @moduledoc false\n\n alias Dune.{AtomMapping, Success, Failure, Opts}\n alias Dune.Parser.{Com"
},
{
"path": "lib/dune/session.ex",
"chars": 4075,
"preview": "defmodule Dune.Session do\n @moduledoc \"\"\"\n Sessions provide a way to evaluate code and keep state (bindings, modules.."
},
{
"path": "lib/dune/shims/atom.ex",
"chars": 302,
"preview": "defmodule Dune.Shims.Atom do\n @moduledoc false\n\n alias Dune.AtomMapping\n\n def to_string(env, atom) when is_atom(atom)"
},
{
"path": "lib/dune/shims/enum.ex",
"chars": 504,
"preview": "defmodule Dune.Shims.Enum do\n @moduledoc false\n\n def join(env, enumerable, joiner \\\\ \"\") when is_binary(joiner) do\n "
},
{
"path": "lib/dune/shims/io.ex",
"chars": 919,
"preview": "defmodule Dune.Shims.IO do\n @moduledoc false\n\n alias Dune.{Failure, Shims}\n\n def puts(env, device \\\\ :stdio, item)\n\n "
},
{
"path": "lib/dune/shims/json.ex",
"chars": 1705,
"preview": "if Code.ensure_loaded?(JSON) do\n defmodule Dune.Shims.JSON do\n @moduledoc false\n\n alias Dune.AtomMapping\n alia"
},
{
"path": "lib/dune/shims/kernel.ex",
"chars": 3029,
"preview": "defmodule Dune.Shims.Kernel do\n @moduledoc false\n\n alias Dune.{AtomMapping, Failure, Shims}\n alias Dune.Helpers.TermC"
},
{
"path": "lib/dune/shims/list.ex",
"chars": 893,
"preview": "defmodule Dune.Shims.List do\n @moduledoc false\n\n alias Dune.Shims\n\n def to_string(_env, list) when is_list(list) do\n "
},
{
"path": "lib/dune/shims/string.ex",
"chars": 336,
"preview": "defmodule Dune.Shims.String do\n @moduledoc false\n\n alias Dune.AtomMapping\n\n # note: this is probably not safe so not "
},
{
"path": "lib/dune/success.ex",
"chars": 736,
"preview": "defmodule Dune.Success do\n @moduledoc \"\"\"\n A struct returned when `Dune` evaluation succeeds.\n\n Fields:\n - `value` ("
},
{
"path": "lib/dune.ex",
"chars": 5884,
"preview": "defmodule Dune do\n @moduledoc \"\"\"\n A sandbox for Elixir to safely evaluate untrusted code from user input.\n\n ## Featu"
},
{
"path": "mix.exs",
"chars": 1514,
"preview": "defmodule Dune.MixProject do\n use Mix.Project\n\n @version \"0.3.15\"\n @github_url \"https://github.com/functional-rewire/"
},
{
"path": "test/dune/allowlist/default_test.exs",
"chars": 508,
"preview": "defmodule Dune.Allowlist.DefaultTest do\n use ExUnit.Case, async: true\n doctest Dune.Allowlist.Default\n alias Dune.All"
},
{
"path": "test/dune/allowlist_test.exs",
"chars": 1647,
"preview": "defmodule Dune.AllowlistTest do\n use ExUnit.Case, async: true\n\n doctest Dune.Allowlist\n\n describe \"use/2\" do\n test"
},
{
"path": "test/dune/atom_mapping_test.exs",
"chars": 96,
"preview": "defmodule Dune.AtomMappingTest do\n use ExUnit.Case, async: true\n doctest Dune.AtomMapping\nend\n"
},
{
"path": "test/dune/opts_test.exs",
"chars": 82,
"preview": "defmodule Dune.OptsTest do\n use ExUnit.Case, async: true\n doctest Dune.Opts\nend\n"
},
{
"path": "test/dune/parser/atom_encoder_test.exs",
"chars": 1264,
"preview": "defmodule Dune.Parser.AtomEncoderTest do\n use ExUnit.Case, async: true\n import Dune.Parser.AtomEncoder\n\n describe \"ca"
},
{
"path": "test/dune/parser/string_parser_test.exs",
"chars": 4132,
"preview": "defmodule Dune.Parser.StringParserTest do\n use ExUnit.Case, async: true\n\n alias Dune.{AtomMapping, Opts}\n alias Dune."
},
{
"path": "test/dune/session_test.exs",
"chars": 3040,
"preview": "defmodule Dune.SessionTest do\n use ExUnit.Case\n\n doctest Dune.Session, tags: [lts_only: true]\n\n alias Dune.{Session, "
},
{
"path": "test/dune/shims_test.exs",
"chars": 1901,
"preview": "defmodule Dune.ShimsTest do\n use ExUnit.Case, async: true\n\n alias Dune.Success\n alias Dune.Failure\n\n defmacrop sigil"
},
{
"path": "test/dune/validation_test.exs",
"chars": 37,
"preview": "defmodule Dune.ValidationTest do\nend\n"
},
{
"path": "test/dune_modules_test.exs",
"chars": 11170,
"preview": "defmodule DuneModulesTest do\n use ExUnit.Case, async: true\n\n alias Dune.{Success, Failure}\n\n defmacro sigil_E(call, _"
},
{
"path": "test/dune_oom_safety_test.exs",
"chars": 2421,
"preview": "defmodule Dune.AssertionHelper do\n alias Dune.Failure\n\n defmacro test_execution_stops(test_name, do: expr) do\n quot"
},
{
"path": "test/dune_quoted_test.exs",
"chars": 12212,
"preview": "defmodule DuneQuotedTest do\n use ExUnit.Case, async: true\n\n import ExUnit.CaptureIO\n\n alias Dune.{Success, Failure}\n\n"
},
{
"path": "test/dune_string_test.exs",
"chars": 34698,
"preview": "defmodule DuneStringTest do\n use ExUnit.Case, async: true\n\n alias Dune.{Success, Failure}\n\n defmacro sigil_E(call, _e"
},
{
"path": "test/dune_string_to_quoted_test.exs",
"chars": 777,
"preview": "defmodule DuneStringToQuotedTest do\n use ExUnit.Case, async: true\n\n alias Dune.Success\n\n describe \"Dune.string_to_quo"
},
{
"path": "test/dune_test.exs",
"chars": 144,
"preview": "defmodule DuneTest do\n use ExUnit.Case, async: true\n\n # TODO remove then dropping support for 1.15\n doctest Dune, tag"
},
{
"path": "test/test_helper.exs",
"chars": 15,
"preview": "ExUnit.start()\n"
}
]
About this extraction
This page contains the full source code of the functional-rewire/dune GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 57 files (191.6 KB), approximately 54.4k tokens, and a symbol index with 257 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.