2014-09-23 18:06:30 +03:00
|
|
|
-- Buildat: extension/urho3d
|
2014-09-22 21:42:00 +03:00
|
|
|
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
-- Copyright 2014 Perttu Ahola <celeron55@gmail.com>
|
2014-09-23 18:06:30 +03:00
|
|
|
local log = buildat.Logger("extension/urho3d")
|
|
|
|
local dump = buildat.dump
|
2014-09-26 19:06:07 +03:00
|
|
|
local magic_sandbox = require("buildat/extension/magic_sandbox")
|
2014-09-27 07:46:30 +03:00
|
|
|
local safe_globals = dofile(buildat.extension_path("urho3d").."/safe_globals.lua")
|
2014-09-27 08:21:12 +03:00
|
|
|
local safe_events = dofile(buildat.extension_path("urho3d").."/safe_events.lua")
|
2014-09-27 08:38:10 +03:00
|
|
|
local safe_classes = dofile(buildat.extension_path("urho3d").."/safe_classes.lua")
|
2014-09-27 14:26:26 +03:00
|
|
|
|
|
|
|
local Safe = {}
|
|
|
|
local Unsafe = {}
|
2014-09-22 21:42:00 +03:00
|
|
|
|
2014-09-30 12:37:23 +03:00
|
|
|
--[[
|
2014-09-26 08:55:10 +03:00
|
|
|
--
|
2014-09-26 19:06:07 +03:00
|
|
|
-- ResourceCache support code
|
2014-09-26 08:55:10 +03:00
|
|
|
--
|
|
|
|
|
2014-09-24 00:00:24 +03:00
|
|
|
-- Checks that this is not an absolute file path or anything funny
|
2014-09-27 14:26:26 +03:00
|
|
|
local allowed_name_pattern = '^[a-zA-Z0-9_][a-zA-Z0-9/._ ]*$'
|
|
|
|
function Unsafe.check_safe_resource_name(name)
|
2014-09-24 00:00:24 +03:00
|
|
|
if type(name) ~= "string" then
|
2014-09-24 10:58:46 +03:00
|
|
|
error("Unsafe resource name: "..dump(name).." (not string)")
|
2014-09-24 00:00:24 +03:00
|
|
|
end
|
|
|
|
if string.match(name, '^/.*$') then
|
2014-09-24 10:58:46 +03:00
|
|
|
error("Unsafe resource name: "..dump(name).." (absolute path)")
|
2014-09-24 00:00:24 +03:00
|
|
|
end
|
|
|
|
if not string.match(name, allowed_name_pattern) then
|
2014-09-24 10:58:46 +03:00
|
|
|
error("Unsafe resource name: "..dump(name).." (unneeded chars)")
|
2014-09-24 00:00:24 +03:00
|
|
|
end
|
|
|
|
if string.match(name, '[.][.]') then
|
2014-09-24 10:58:46 +03:00
|
|
|
error("Unsafe resource name: "..dump(name).." (contains ..)")
|
2014-09-24 00:00:24 +03:00
|
|
|
end
|
2014-09-27 14:26:26 +03:00
|
|
|
--log:verbose("Safe resource name: "..name)
|
2014-09-24 00:00:24 +03:00
|
|
|
return name
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Basic tests
|
|
|
|
assert(pcall(function()
|
2014-09-27 14:26:26 +03:00
|
|
|
Unsafe.check_safe_resource_name("/etc/passwd")
|
2014-09-24 00:00:24 +03:00
|
|
|
end) == false)
|
|
|
|
assert(pcall(function()
|
2014-09-27 14:26:26 +03:00
|
|
|
Unsafe.check_safe_resource_name(" /etc/passwd")
|
2014-09-24 00:00:24 +03:00
|
|
|
end) == false)
|
|
|
|
assert(pcall(function()
|
2014-09-27 14:26:26 +03:00
|
|
|
Unsafe.check_safe_resource_name("\t /etc/passwd")
|
2014-09-24 00:00:24 +03:00
|
|
|
end) == false)
|
|
|
|
assert(pcall(function()
|
2014-09-27 14:26:26 +03:00
|
|
|
Unsafe.check_safe_resource_name("Safeodels/Box.mdl")
|
2014-09-24 00:00:24 +03:00
|
|
|
end) == true)
|
|
|
|
assert(pcall(function()
|
2014-09-27 14:26:26 +03:00
|
|
|
Unsafe.check_safe_resource_name("Fonts/Anonymous Pro.ttf")
|
2014-09-24 00:00:24 +03:00
|
|
|
end) == true)
|
|
|
|
assert(pcall(function()
|
2014-09-27 14:26:26 +03:00
|
|
|
Unsafe.check_safe_resource_name("test1/pink_texture.png")
|
2014-09-24 00:00:24 +03:00
|
|
|
end) == true)
|
|
|
|
assert(pcall(function()
|
2014-09-27 14:26:26 +03:00
|
|
|
Unsafe.check_safe_resource_name(" Box.mdl ")
|
2014-09-24 00:00:24 +03:00
|
|
|
end) == false)
|
|
|
|
assert(pcall(function()
|
2014-09-27 14:26:26 +03:00
|
|
|
Unsafe.check_safe_resource_name("../../foo")
|
2014-09-24 00:00:24 +03:00
|
|
|
end) == false)
|
|
|
|
assert(pcall(function()
|
2014-09-27 14:26:26 +03:00
|
|
|
Unsafe.check_safe_resource_name("abc$de")
|
2014-09-24 00:00:24 +03:00
|
|
|
end) == false)
|
|
|
|
|
2014-09-24 10:58:46 +03:00
|
|
|
local hack_resaved_files = {} -- name -> temporary target file
|
2014-09-24 00:00:24 +03:00
|
|
|
|
|
|
|
-- Create temporary file with wanted file name to make Urho3D load it correctly
|
2014-09-27 14:26:26 +03:00
|
|
|
function Unsafe.resave_file(resource_name)
|
|
|
|
Unsafe.check_safe_resource_name(resource_name)
|
2014-09-24 10:58:46 +03:00
|
|
|
local path2 = hack_resaved_files[resource_name]
|
2014-09-24 00:00:24 +03:00
|
|
|
if path2 == nil then
|
2014-09-24 10:58:46 +03:00
|
|
|
local path = __buildat_get_file_path(resource_name)
|
|
|
|
if path == nil then
|
2014-09-28 00:55:37 +03:00
|
|
|
-- Not found in data received by server.
|
|
|
|
-- Could be missing, or could be a local file.
|
2014-09-24 10:58:46 +03:00
|
|
|
return nil
|
|
|
|
end
|
2014-09-24 00:00:24 +03:00
|
|
|
path2 = __buildat_get_path("tmp").."/"..resource_name
|
|
|
|
dir2 = string.match(path2, '^(.*)/.+$')
|
|
|
|
if dir2 then
|
|
|
|
if not __buildat_mkdir(dir2) then
|
|
|
|
error("Failed to create directory: \""..dir2.."\"")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
log:info("Temporary path: "..path2)
|
|
|
|
local src = io.open(path, "rb")
|
|
|
|
local dst = io.open(path2, "wb")
|
|
|
|
while true do
|
|
|
|
local buf = src:read(100000)
|
|
|
|
if buf == nil then break end
|
|
|
|
dst:write(buf)
|
|
|
|
end
|
|
|
|
src:close()
|
|
|
|
dst:close()
|
2014-09-24 10:58:46 +03:00
|
|
|
hack_resaved_files[resource_name] = path2
|
2014-09-24 00:00:24 +03:00
|
|
|
end
|
|
|
|
return path2
|
|
|
|
end
|
|
|
|
|
2014-09-24 10:58:46 +03:00
|
|
|
-- Callback from core
|
|
|
|
function __buildat_file_updated_in_cache(name, hash, cached_path)
|
|
|
|
log:debug("__buildat_file_updated_in_cache(): name="..dump(name)..
|
|
|
|
", cached_path="..dump(cached_path))
|
|
|
|
if hack_resaved_files[name] then
|
|
|
|
log:verbose("__buildat_file_updated_in_cache(): Re-saving: "..dump(name))
|
|
|
|
hack_resaved_files[name] = nil -- Force re-copy
|
2014-09-27 14:26:26 +03:00
|
|
|
Unsafe.resave_file(name)
|
2014-09-24 10:58:46 +03:00
|
|
|
end
|
|
|
|
end
|
2014-09-30 12:37:23 +03:00
|
|
|
--]]
|
2014-09-24 10:58:46 +03:00
|
|
|
|
2014-09-26 19:06:07 +03:00
|
|
|
--
|
|
|
|
-- Safe interface
|
|
|
|
--
|
|
|
|
|
|
|
|
local function wc(name, def)
|
2014-09-27 14:26:26 +03:00
|
|
|
Safe[name] = magic_sandbox.wrap_class(name, def)
|
2014-09-26 19:06:07 +03:00
|
|
|
end
|
|
|
|
|
|
|
|
local function wrap_instance(name, instance)
|
2014-09-27 14:26:26 +03:00
|
|
|
local class = Safe[name]
|
2014-09-26 19:06:07 +03:00
|
|
|
local class_meta = getmetatable(class)
|
|
|
|
if not class_meta then error(dump(name).." is not a whitelisted class") end
|
|
|
|
return class_meta.wrap(instance)
|
|
|
|
end
|
|
|
|
|
|
|
|
-- (return_types, param_types, f) or (param_types, f)
|
|
|
|
local function wrap_function(return_types, param_types, f)
|
|
|
|
if type(param_types) == 'function' and f == nil then
|
|
|
|
f = param_types
|
|
|
|
param_types = return_types
|
|
|
|
return_types = {"__safe"}
|
|
|
|
end
|
|
|
|
return function(...)
|
2014-10-04 19:12:12 +03:00
|
|
|
local arg = {...}
|
2014-09-26 19:06:07 +03:00
|
|
|
local checked_arg = {}
|
|
|
|
for i = 1, #param_types do
|
|
|
|
checked_arg[i] = magic_sandbox.safe_to_unsafe(arg[i], param_types[i])
|
|
|
|
end
|
|
|
|
local wrapped_ret = {}
|
|
|
|
local ret = {f(unpack(checked_arg, 1, table.maxn(checked_arg)))}
|
|
|
|
for i = 1, #return_types do
|
|
|
|
wrapped_ret[i] = magic_sandbox.unsafe_to_safe(ret[i], return_types[i])
|
|
|
|
end
|
|
|
|
return unpack(wrapped_ret, 1, #return_types)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function self_function(function_name, return_types, param_types)
|
|
|
|
return function(...)
|
|
|
|
if #param_types < 1 then
|
|
|
|
error("At least one argument required (self)")
|
|
|
|
end
|
2014-10-04 19:12:12 +03:00
|
|
|
local arg = {...}
|
2014-09-26 19:06:07 +03:00
|
|
|
local checked_arg = {}
|
|
|
|
for i = 1, #param_types do
|
|
|
|
checked_arg[i] = magic_sandbox.safe_to_unsafe(arg[i], param_types[i])
|
|
|
|
end
|
|
|
|
local wrapped_ret = {}
|
|
|
|
local self = checked_arg[1]
|
|
|
|
local f = self[function_name]
|
|
|
|
if type(f) ~= 'function' then
|
|
|
|
error(dump(function_name).." not found in instance")
|
|
|
|
end
|
|
|
|
local ret = {f(unpack(checked_arg, 1, table.maxn(checked_arg)))}
|
|
|
|
for i = 1, #return_types do
|
|
|
|
wrapped_ret[i] = magic_sandbox.unsafe_to_safe(ret[i], return_types[i])
|
|
|
|
end
|
|
|
|
return unpack(wrapped_ret, 1, #return_types)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function simple_property(valid_types)
|
|
|
|
return {
|
|
|
|
get = function(current_value)
|
|
|
|
return magic_sandbox.unsafe_to_safe(current_value, valid_types)
|
|
|
|
end,
|
|
|
|
set = function(new_value)
|
|
|
|
return magic_sandbox.safe_to_unsafe(new_value, valid_types)
|
|
|
|
end,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
for _, name in ipairs(safe_globals) do
|
|
|
|
local v = _G[name]
|
|
|
|
if type(v) ~= 'number' and type(v) ~= 'string' then
|
|
|
|
error("Invalid safe global "..dump(name).." type: "..dump(type(v)))
|
|
|
|
end
|
2014-09-27 14:26:26 +03:00
|
|
|
Safe[name] = v
|
2014-09-26 19:06:07 +03:00
|
|
|
end
|
|
|
|
|
2014-09-27 14:26:26 +03:00
|
|
|
safe_classes.define(Safe, {
|
2014-09-27 08:38:10 +03:00
|
|
|
wc = wc,
|
|
|
|
wrap_instance = wrap_instance,
|
|
|
|
wrap_function = wrap_function,
|
|
|
|
self_function = self_function,
|
|
|
|
simple_property = simple_property,
|
2014-09-27 14:26:26 +03:00
|
|
|
check_safe_resource_name = Unsafe.check_safe_resource_name,
|
2014-09-30 12:37:23 +03:00
|
|
|
--resave_file = Unsafe.resave_file,
|
2014-09-26 19:06:07 +03:00
|
|
|
})
|
|
|
|
|
2014-09-27 14:26:26 +03:00
|
|
|
setmetatable(Safe, {
|
2014-09-26 19:06:07 +03:00
|
|
|
__index = function(t, k)
|
|
|
|
local v = rawget(t, k)
|
|
|
|
if v ~= nil then return v end
|
2014-09-27 14:26:26 +03:00
|
|
|
error("extension/urho3d: Class "..dump(k).." is not whitelisted")
|
2014-09-26 19:06:07 +03:00
|
|
|
end,
|
|
|
|
})
|
|
|
|
|
2014-09-24 00:00:24 +03:00
|
|
|
-- SubscribeToEvent
|
|
|
|
|
2014-09-26 08:55:10 +03:00
|
|
|
local sandbox_callback_to_global_function_name = {}
|
|
|
|
local next_sandbox_global_function_i = 1
|
2014-10-05 10:54:04 +03:00
|
|
|
|
2014-09-27 14:26:26 +03:00
|
|
|
function Safe.SubscribeToEvent(x, y, z)
|
2014-10-04 19:48:31 +03:00
|
|
|
log:debug("Safe.SubscribeToEvent("..dump(x)..", "..dump(y)..", "..dump(z)..")")
|
2014-09-26 11:47:16 +03:00
|
|
|
local object = x
|
2014-09-27 08:21:12 +03:00
|
|
|
local sub_event_type = y
|
2014-09-26 11:47:16 +03:00
|
|
|
local callback = z
|
2014-10-03 13:16:16 +03:00
|
|
|
if z == nil then
|
2014-09-26 11:47:16 +03:00
|
|
|
object = nil
|
2014-09-27 08:21:12 +03:00
|
|
|
sub_event_type = x
|
2014-09-26 11:47:16 +03:00
|
|
|
callback = y
|
|
|
|
end
|
2014-09-27 14:26:26 +03:00
|
|
|
if object then
|
|
|
|
if not getmetatable(object) or not getmetatable(object).unsafe then
|
|
|
|
error("SubscribeToEvent(): Object must be sandboxed")
|
|
|
|
end
|
|
|
|
end
|
2014-09-27 08:21:12 +03:00
|
|
|
if not safe_events[sub_event_type] then
|
|
|
|
error("Event type is not whitelisted: "..dump(sub_event_type))
|
|
|
|
end
|
2014-09-26 08:55:10 +03:00
|
|
|
if type(callback) == 'string' then
|
|
|
|
-- Allow supplying callback function name like Urho3D does by default
|
|
|
|
local caller_environment = getfenv(2)
|
|
|
|
callback = caller_environment[callback]
|
|
|
|
if type(callback) ~= 'function' then
|
|
|
|
error("SubscribeToEvent(): '"..callback..
|
|
|
|
"' is not a global function in current sandbox environment")
|
|
|
|
end
|
|
|
|
else
|
|
|
|
-- Allow directly supplying callback function
|
2014-09-22 21:42:00 +03:00
|
|
|
end
|
2014-09-26 08:55:10 +03:00
|
|
|
local global_function_i = next_sandbox_global_function_i
|
|
|
|
next_sandbox_global_function_i = next_sandbox_global_function_i + 1
|
2014-09-26 14:46:11 +03:00
|
|
|
local global_callback_name = "__buildat_sandbox_callback_"..global_function_i
|
|
|
|
sandbox_callback_to_global_function_name[callback] = global_callback_name
|
2014-09-27 08:21:12 +03:00
|
|
|
_G[global_callback_name] = function(event_type_thing, unsafe_event_data)
|
2014-09-22 21:42:00 +03:00
|
|
|
local f = function()
|
2014-09-27 08:44:24 +03:00
|
|
|
-- How the hell does one get a string out of event_type_thing?
|
|
|
|
-- It is not a Variant, and none of the Lua examples try to do anything
|
|
|
|
-- with it.
|
|
|
|
-- Let's just assume it's the correct one...
|
|
|
|
local got_event_type = sub_event_type
|
|
|
|
-- Filter event_data (Urho3D::VariantMap)
|
|
|
|
local safe_fields = safe_events[got_event_type]
|
|
|
|
if not safe_fields then
|
|
|
|
log:warning("Received unsafe event: "..dump(got_event_type))
|
|
|
|
end
|
2014-09-27 14:26:26 +03:00
|
|
|
local safe_event_data = Safe.VariantMap()
|
2014-09-27 08:44:24 +03:00
|
|
|
for field_name, field_def in pairs(safe_fields) do
|
|
|
|
local variant_type = field_def.variant
|
|
|
|
local safe_type = field_def.safe
|
2014-10-03 01:43:04 +03:00
|
|
|
local safe_value = nil
|
|
|
|
if variant_type == "Ptr" then
|
|
|
|
local unsafe_value = unsafe_event_data:GetPtr(
|
|
|
|
safe_type, field_name)
|
|
|
|
safe_value = wrap_instance(safe_type, unsafe_value)
|
|
|
|
else
|
|
|
|
local unsafe_value = unsafe_event_data["Get"..variant_type](
|
|
|
|
unsafe_event_data, field_name)
|
|
|
|
safe_value = magic_sandbox.unsafe_to_safe(unsafe_value, safe_type)
|
|
|
|
end
|
2014-09-27 08:44:24 +03:00
|
|
|
safe_event_data["Set"..variant_type](
|
|
|
|
safe_event_data, field_name, safe_value)
|
|
|
|
end
|
|
|
|
-- Call callback
|
2014-09-26 11:47:16 +03:00
|
|
|
if object then
|
2014-09-27 08:21:12 +03:00
|
|
|
callback(object, got_event_type, safe_event_data)
|
2014-09-26 11:47:16 +03:00
|
|
|
else
|
2014-09-27 08:21:12 +03:00
|
|
|
callback(got_event_type, safe_event_data)
|
2014-09-26 11:47:16 +03:00
|
|
|
end
|
2014-09-22 21:42:00 +03:00
|
|
|
end
|
|
|
|
__buildat_run_function_in_sandbox(f)
|
|
|
|
end
|
2014-09-26 11:47:16 +03:00
|
|
|
if object then
|
2014-09-27 14:26:26 +03:00
|
|
|
local unsafe_object = getmetatable(object).unsafe
|
|
|
|
SubscribeToEvent(unsafe_object, sub_event_type, global_callback_name)
|
2014-09-26 11:47:16 +03:00
|
|
|
else
|
2014-09-27 08:21:12 +03:00
|
|
|
SubscribeToEvent(sub_event_type, global_callback_name)
|
2014-09-26 11:47:16 +03:00
|
|
|
end
|
2014-10-04 19:48:31 +03:00
|
|
|
log:debug("-> global_callback_name="..dump(global_callback_name))
|
2014-09-26 14:46:11 +03:00
|
|
|
return global_callback_name
|
2014-09-26 08:55:10 +03:00
|
|
|
end
|
|
|
|
|
2014-10-03 13:16:16 +03:00
|
|
|
function Safe.UnsubscribeFromEvent(sub_event_type, cb_name)
|
2014-10-04 19:48:31 +03:00
|
|
|
log:debug("Safe.UnsubscribeFromEvent("..dump(sub_event_type)..", "..dump(cb_name)..")")
|
2014-10-03 13:16:16 +03:00
|
|
|
UnsubscribeFromEvent(sub_event_type, cb_name)
|
2014-09-27 14:26:26 +03:00
|
|
|
-- TODO: Delete the generated global callback
|
|
|
|
end
|
|
|
|
|
2014-09-26 08:55:10 +03:00
|
|
|
--
|
|
|
|
-- Unsafe interface
|
|
|
|
--
|
|
|
|
|
|
|
|
-- Just wrap everything to the global environment as we don't have a full list
|
|
|
|
-- of Urho3D's API available.
|
|
|
|
|
2014-09-27 14:26:26 +03:00
|
|
|
setmetatable(Unsafe, {
|
2014-09-26 08:55:10 +03:00
|
|
|
__index = function(t, k)
|
|
|
|
local v = rawget(t, k)
|
|
|
|
if v ~= nil then return v end
|
|
|
|
return _G[k]
|
|
|
|
end,
|
|
|
|
})
|
|
|
|
|
|
|
|
-- Unsafe SubscribeToEvent with function support
|
|
|
|
|
|
|
|
local unsafe_callback_to_global_function_name = {}
|
|
|
|
local next_unsafe_global_function_i = 1
|
|
|
|
|
2014-09-27 14:26:26 +03:00
|
|
|
function Unsafe.SubscribeToEvent(x, y, z)
|
2014-09-26 11:47:16 +03:00
|
|
|
local object = x
|
|
|
|
local event_name = y
|
|
|
|
local callback = z
|
|
|
|
if callback == nil then
|
|
|
|
object = nil
|
|
|
|
event_name = x
|
|
|
|
callback = y
|
|
|
|
end
|
2014-09-26 08:55:10 +03:00
|
|
|
if type(callback) == 'string' then
|
|
|
|
-- Allow supplying callback function name like Urho3D does by default
|
|
|
|
local caller_environment = getfenv(2)
|
|
|
|
callback = caller_environment[callback]
|
|
|
|
if type(callback) ~= 'function' then
|
|
|
|
error("SubscribeToEvent(): '"..callback..
|
|
|
|
"' is not a global function in current unsafe environment")
|
|
|
|
end
|
|
|
|
else
|
|
|
|
-- Allow directly supplying callback function
|
|
|
|
end
|
|
|
|
local global_function_i = next_unsafe_global_function_i
|
|
|
|
next_unsafe_global_function_i = next_unsafe_global_function_i + 1
|
2014-09-26 14:46:11 +03:00
|
|
|
local global_callback_name = "__buildat_unsafe_callback_"..global_function_i
|
|
|
|
unsafe_callback_to_global_function_name[callback] = global_callback_name
|
2014-09-27 08:21:12 +03:00
|
|
|
_G[global_callback_name] = function(event_type, event_data)
|
2014-09-26 16:03:38 +03:00
|
|
|
local f = function()
|
|
|
|
if object then
|
2014-09-27 08:21:12 +03:00
|
|
|
callback(object, event_type, event_data)
|
2014-09-26 16:03:38 +03:00
|
|
|
else
|
2014-09-27 08:21:12 +03:00
|
|
|
callback(event_type, event_data)
|
2014-09-26 16:03:38 +03:00
|
|
|
end
|
|
|
|
end
|
|
|
|
local ok, err = __buildat_pcall(f)
|
|
|
|
if not ok then
|
|
|
|
__buildat_fatal_error("Error calling callback: "..err)
|
2014-09-26 11:47:16 +03:00
|
|
|
end
|
|
|
|
end
|
|
|
|
if object then
|
2014-09-26 14:46:11 +03:00
|
|
|
SubscribeToEvent(object, event_name, global_callback_name)
|
2014-09-26 11:47:16 +03:00
|
|
|
else
|
2014-09-26 14:46:11 +03:00
|
|
|
SubscribeToEvent(event_name, global_callback_name)
|
2014-09-26 08:55:10 +03:00
|
|
|
end
|
2014-09-26 14:46:11 +03:00
|
|
|
return global_callback_name
|
2014-09-22 21:42:00 +03:00
|
|
|
end
|
2014-09-23 18:06:30 +03:00
|
|
|
|
2014-09-27 14:26:26 +03:00
|
|
|
--
|
|
|
|
-- Create the final interface
|
|
|
|
--
|
|
|
|
|
2014-09-28 12:56:14 +03:00
|
|
|
local M = {}
|
|
|
|
M.safe = Safe
|
2014-09-27 14:26:26 +03:00
|
|
|
M.unsafe = Unsafe
|
|
|
|
|
2014-09-23 18:06:30 +03:00
|
|
|
return M
|
2014-09-24 15:13:49 +03:00
|
|
|
-- vim: set noet ts=4 sw=4:
|