# `Rho.Untrusted`
[🔗](https://github.com/GenericJam/rho/blob/v0.1.0/lib/rho/untrusted.ex#L1)

Text and data from code rho doesn't trust, the Dyn layer's (`Rho.Dyn`),
for the processes it does: the loop, the Keeper, the caller of a compile
or a selftest.

Turning a term into text can run code the term chose:
`Exception.message/1` calls the exception's module, and `inspect/2`,
interpolation, `to_string/1` and `Jason.encode/1` dispatch protocols,
which Dyn code may implement for its own structs. Run in a trusted
process, that code could loop forever, raise, or send messages as that
process. So:

  * `format/3` turns a caught raise, throw or exit into text **in the
    untrusted process that caught it** (a tool's task, a selftest, a
    compile), in a throwaway helper with a time and heap bound that
    never ends in a crash report and dies with its caller; if that
    fails or runs over, it falls back to describing the term;
  * `crash/3` does that and exits with `{:rho_crash, text}`, the exit
    reason a host's own untrusted processes can use so a watcher (the
    Keeper, say) reports the text as it is;
  * `describe/1` and `exit_text/1` describe any term without running
    anything of it: no protocol of its structs, no exception message
    callback (an exception's `:message` field is read as data). That is
    what a trusted process uses for a term it still receives, a `:DOWN`
    reason say. Both are total;
  * `plain/1` turns a Dyn tool's answer into plain data (no structs, no
    funs) before it leaves the task.

Every text is valid UTF-8 and at most `max` bytes (4000 by default).

Crash reports are a separate path: OTP logs a crashed process's report
and Elixir's logger translator formats it, calling the exception's
`message/1`. Left alone, a Dyn exception whose `message/1` throws or
exits there makes OTP remove the translator VM-wide (later crash logs
come out untranslated). `Rho.Dyn.LogGuard`, installed by the Keeper,
replaces Dyn terms in log events before the translator sees them, which
keeps that call out of the translator; it lets through an event too big
to check (more than 10,000 terms before the Dyn one), and a Logger
restart removes the filter.

This is about crashes and mistakes, not hostile code: it keeps a Dyn
term the Core receives from running Dyn code there, which lowers the
chance that a built tool's crash takes a trusted process down. Dyn code
that sets out to reach a trusted process can still do so by other routes
(see `Rho.Dyn.Check`).

# `crash`

```elixir
@spec crash(:error | :exit | :throw, term(), Exception.stacktrace()) :: no_return()
```

Formats the caught `kind, reason` (`format/4`) and exits with it as the
reason (`{:rho_crash, text}`), so whoever watches this process gets the
text, not the term: `exit_text/1` reads it back. An orderly exit
(`:normal`, `:shutdown`, `{:shutdown, _}`) stays as it was.

# `describe`

```elixir
@spec describe(term(), pos_integer()) :: String.t()
```

Describes `term` without running any of its code: `inspect/2` with
`structs: false` (a struct prints as the map it is, so no `Inspect`
implementation runs), integers past 1024 bits as a placeholder (their
decimal text costs seconds), cut to `max` bytes.

# `exit_text`

```elixir
@spec exit_text(term(), pos_integer()) :: String.t()
```

Text for an exit reason (a `:DOWN` or `:EXIT` message's), without running
any of its code: an exception with its stacktrace reads as
`** (Module) message` and the stack (the `:message` field when it is a
string, else the exception's fields), a reason a process formatted
itself (`crash/3`) as that text, anything else as `describe/1` does.
Total: any term gives a text (improper lists, odd stack entries and
deep or huge terms are described, cut to `max`).

# `format`

```elixir
@spec format(:error | :exit | :throw, term(), Exception.stacktrace(), keyword()) ::
  String.t()
```

A caught `kind, reason, stack` as text, as `Exception.format/3` writes
it (banner and stacktrace), formatted in a throwaway helper process so
the caller's own mailbox and state stay out of reach. The helper has a
heap limit, never ends in a crash report and dies with its caller; if
it fails or runs over `:timeout_ms` (1000), the term is
described instead (as `exit_text/2` and `describe/2` do). Call it in the
untrusted process that caught the error, never in a trusted one.
Options: `:timeout_ms`, `:max` (bytes).

# `plain`

```elixir
@spec plain(term()) :: term()
```

`term` as plain data: every struct a map of its fields (without
`__struct__` and `__exception__`), every fun its `describe/1` text,
through lists, tuples and map keys and values. What a Dyn tool answers
goes through it in its own task, before anything trusted touches it.

# `untrusted?`

```elixir
@spec untrusted?(module()) :: boolean()
```

Is `module` untrusted code: a Dyn generation's module or a protocol
implementation for one (`Rho.Dyn.dyn_module?/1`)?

---

*Consult [api-reference.md](api-reference.md) for complete listing*
