-- lib/supple/host.lua
--
-- Sandbox (for) Untrusted Procedure Partitioning (in) Lua Engine
--
-- Management of the host side of Supple
--
-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
--
-- For licence terms, see COPYING
--

--- Running the host-side of sandboxed code
--
-- When running code in Supple sandboxes, the starting point is this module.
-- The host application should require supple and then begin from here...
--
--     local supple = require 'supple'
--     print(supple.host.run("return ...", "example code", "arg1", "arg2"))
--     -- Expect: 'arg1    arg2' as output
--


local luxio = require 'luxio'
local subprocess = require 'luxio.subprocess'

local comms = require 'supple.comms'
local objects = require 'supple.objects'
local capi = require 'supple.capi'
local track = require 'supple.track'

local hostname = "host"
local counter = 0
local limits, globals

local function simplify(t, memo)
   if not memo then memo = {} end
   if memo[t] then return memo[t] end
   local ret = {}
   memo[t] = ret
   local kk, vv
   for k, v in capi.pairs(t) do
      kk, vv = k, v
      if capi.type(k) == "table" then
	 kk = simplify(k, memo)
      end
      if capi.type(v) == "table" then
	 vv = simplify(v, memo)
      end
      if capi.rawtype(kk) ~= "userdata" and
	 capi.rawtype(vv) ~= "userdata" then
	 -- We've not got a proxy left over anywhere, so copy it
	 ret[kk] = vv
      end
   end
   return ret
end

local function run_wrapper()
   local wrapperpath = capi.wrapper_path
   -- START_TEST_SUPPLE
   wrapperpath = os.getenv "SUPPLE_TEST_WRAPPER"
   -- END_TEST_SUPPLE
   local fds = {}
   local ret, errno = luxio.socketpair(luxio.AF_UNIX, luxio.SOCK_STREAM,
				       luxio.PF_UNIX, fds)
   if ret ~= 0 then
      error("Unable to launch subprocess, could not prepare socketpair():"
	    .. luxio.strerror(errno))
   end
   local proc, msg = subprocess.spawn {
      "supple-sandbox",
      exe = wrapperpath,
      stdin = fds[1],
      stdout = -1,
      stderr = -1,
      close_in_child = { fds[1], fds[2] },
   }
   if not proc then
      error(msg)
   end
   luxio.close(fds[1])
   return proc, fds[2]
end

--- Run some code in a Supple sandbox
--
-- Call this routine to run some code in a Supple sandbox.  The code is
-- compiled and run entirely within the sandbox process.  Any kind of value
-- can be passed to the code as an argument.  Intrinsics are transferred
-- immediately (numbers, booleans, nils, strings) but complex values (tables,
-- userdata, and functions) are sent over as a proxy and actions on those
-- values are proxied back to the host to be run.  Users of Supple should be
-- aware that those routines are therefore not sandboxed.
--
-- @tparam string codestr The code to run in the sandbox, as a string.
-- @tparam string codename The name to give to the code running in the sandbox.
-- @param[opt] ... Arguments to pass to the code running in the sandbox.
-- @treturn[1] boolean A value which has 'truth'
-- @return[1] ... The results returned from running the code in the sandbox.
-- @treturn[2] boolean A value which has 'falsehood'
-- @treturn[2] string The basic Lua error message associated with the failure
-- @treturn[2] string The Supple traceback (very large, not useful for users)
-- @function run

local function run_sandbox(codestr, codename, ...)
   -- Prepare and start a sandbox,
   -- compiling the codestr and running it
   -- with the given args
   local child, commsfd = run_wrapper()

   counter = counter + 1
   objects.set_name(("%s[%d,%%d]"):format(hostname,counter))
   comms._set_fd(commsfd)
   objects.set_proc_call(comms.call)

   track.start()
   
   local func, env = comms.call("supple:loadstring", "__call", codestr, codename)
   if not func then
      error(env)
   end

   local limitsok, limitserr = true
   if limits then
      limitsok, limitserr = comms.call("supple:set_limits", "__call", limits)
   end

   -- In a protected manner, capture the output of the call
   local args, ret = {...}
   local function capture()
      ret = {func(unpack(args))}
   end

   local ok, err
   if limitsok then
      if globals then
	 -- Hand over the globals
	 for k, v in pairs(globals) do
	    env[k] = v
	 end
      end
      ok, err = pcall(capture)
   else
      ok, err = limitsok, limitserr
   end
   -- Convert any complex objects returned to us, so we can clean up...
   if ok then
      ret = simplify(ret)
   end
   -- We need to clean up, so dump all the objects
   func = nil
   env = nil
   -- And ask the supple API to clear down too
   objects.clean_down(true)

   comms._set_fd(-1)
   luxio.close(commsfd)
   child:wait()
   if ok then
      return ok, unpack(ret)
   else
      return ok, err, track.stop()
   end
end

---
-- Load a string into the sandbox and return it as a wrappered function.
--
-- This loads the given string (with given name) into the sandbox interpreter
-- and then wrappers the function and returns it to the caller.  This can then
-- be used to call code inside the sandbox.  This is the entry point to load
-- further code into the sandbox and should only be called during running
-- sandboxed code (i.e. `supple.host.run` is running)
--
-- @tparam string codestr The code to be loaded in the sandboxed interpreter
-- @tparam string codename The name to be given to the code when it is loaded
-- @treturn function The wrappered function which can then be called or handed
--                   back to the sandbox as needed.
-- @treturn table    The function environment for the loaded sandboxed code.
-- @function loadstring

local function sandboxed_loadstring(codestr, codename)
   return comms.call("supple:loadstring", "__call", codestr, codename)
end

---
-- Set the name by which the host refers to itself in traces.
--
-- Calling this resets the host sandbox counter and sets the name of the host
-- to the given new name.  This mostly has an effect on object names and trace
-- data which typically only appear if the user of Supple chooses to trace the
-- activity of the sandbox, or if an error occurs.
--
-- @tparam string newname The new host name to set
-- @function set_name

local function set_hostname(newname)
   hostname = newname
   counter = 0
end

---
-- Set a new limits table for use in a new sandbox instance.
--
-- When a sandbox is run (from `supple.host.run`) it has a number of soft
-- limits sent to it to be honoured during the runtime of the sandbox.
-- Normally the sandbox limits itself only by hard limits, but this call allows
-- tighter soft limits to be set if so-desired.
--
-- The table provided takes the form:
--
--     { [count = maxopcodestorun], [memory = maxbytestouse] }
--
-- If you call this function then at least one, but optionally both of the
-- count and memory table entries need to be present.
--
-- @tparam table newlimits The new soft-limits for any new sandboxes
-- @function set_limits

local function set_limits(newlimits)
   limits = newlimits
end

---
-- Set a new globals table to be used by new sandbox runs.
--
-- When a sandboxed function is run, it is run within a given globals table.
-- The globals in question are provided by calling this function.  Remember
-- that the table will be passed *shallowly* and new globals created by the
-- sandboxed code will not be copied back to the host.
--
-- @tparam table newglobals The set of globals to be passed to the sandbox
-- @function set_globals

local function set_globals(newglobals)
   globals = newglobals
end

return {
   run = run_sandbox,
   loadstring = sandboxed_loadstring,
   set_name = set_hostname,
   set_limits = set_limits,
   set_globals = set_globals,
}
