mirror of
https://github.com/vxcontrol/soldr-lua-extras.git
synced 2026-07-01 10:05:29 -04:00
initial commit
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+302
@@ -0,0 +1,302 @@
|
||||
local openssl = require'openssl'
|
||||
local io = require'io'
|
||||
|
||||
local M = {}
|
||||
|
||||
M["_VERSION"] = "LuaCrypto 0.3.1"
|
||||
M["_COPYRIGHT"] = "Copyright (C) 2014 GeorgeZhao";
|
||||
M["_DESCRIPTION"] = "crypto is a high level Lua wrapper for OpenSSL based on lua-openssl";
|
||||
|
||||
-----------crypto------------------
|
||||
function M.hex(s)
|
||||
local s = openssl.hex(s)
|
||||
return string.lower(s)
|
||||
end
|
||||
M.digest = openssl.digest
|
||||
local dm = {}
|
||||
dm.__call = function(self,alg,msg,raw)
|
||||
raw = raw or true
|
||||
return M.digest.digest(alg,msg)
|
||||
end
|
||||
setmetatable(M.digest,dm)
|
||||
|
||||
M.hmac = openssl.hmac
|
||||
-----------crypto encrypt/decrypt compat----------
|
||||
local cipher = openssl.cipher
|
||||
local C = {}
|
||||
|
||||
C.__index = {
|
||||
new = function(alg,key,iv)
|
||||
local c = cipher.encrypt_new(alg,key,iv)
|
||||
if c then
|
||||
local I = c:info()
|
||||
if (iv and #iv>I.iv_length) then
|
||||
error('invalid iv')
|
||||
end
|
||||
if(#key>I.key_length) then
|
||||
error('invalid key')
|
||||
end
|
||||
local t = {}
|
||||
t.ctx = c
|
||||
setmetatable(t,C)
|
||||
return t
|
||||
end
|
||||
end,
|
||||
update = function(self,input)
|
||||
return self.ctx:update(input)
|
||||
end,
|
||||
final = function(self)
|
||||
return self.ctx:final()
|
||||
end
|
||||
}
|
||||
|
||||
C.__call = function(self,alg,input,key,iv)
|
||||
local c = cipher.get(alg)
|
||||
local I = c:info()
|
||||
if (iv and #iv>I.iv_length) then
|
||||
error 'invalid iv'
|
||||
end
|
||||
if(#key>I.key_length) then
|
||||
error 'invalid key'
|
||||
end
|
||||
local ret, msg = cipher.encrypt(alg,input,key,iv)
|
||||
return ret,msg
|
||||
end
|
||||
|
||||
setmetatable(C,C)
|
||||
M.encrypt = C
|
||||
|
||||
local D = {}
|
||||
|
||||
D.__index = {
|
||||
new = function(alg,key,iv)
|
||||
local c = cipher.decrypt_new(alg,key,iv)
|
||||
if c then
|
||||
local I = c:info()
|
||||
if (iv and #iv>I.iv_length) then
|
||||
error('invalid iv')
|
||||
end
|
||||
if(#key>I.key_length) then
|
||||
error('invalid key')
|
||||
end
|
||||
local t = {}
|
||||
t.ctx = c
|
||||
setmetatable(t,D)
|
||||
return t
|
||||
end
|
||||
end,
|
||||
update = function(self,input)
|
||||
return self.ctx:update(input)
|
||||
end,
|
||||
final = function(self)
|
||||
return self.ctx:final()
|
||||
end
|
||||
}
|
||||
|
||||
D.__call = function(self,alg,input,key,iv)
|
||||
local c = cipher.get(alg)
|
||||
local I = c:info()
|
||||
if (iv and #iv>I.iv_length) then
|
||||
error('invalid iv')
|
||||
end
|
||||
if(#key>I.key_length) then
|
||||
error('invalid key')
|
||||
end
|
||||
local r,s = cipher.decrypt(alg,input,key,iv)
|
||||
return r,s
|
||||
end
|
||||
|
||||
setmetatable(D,D)
|
||||
M.decrypt = D
|
||||
|
||||
-----------crypto random compat------------------
|
||||
local R = {}
|
||||
function R.load(file)
|
||||
return openssl.rand_load(file)
|
||||
end
|
||||
|
||||
function R.write(file)
|
||||
return openssl.rand_write(file)
|
||||
end
|
||||
|
||||
function R.cleanup()
|
||||
return openssl.rand_cleanup()
|
||||
end
|
||||
|
||||
function R.status()
|
||||
return openssl.rand_status()
|
||||
end
|
||||
|
||||
function R.pseudo_bytes(len)
|
||||
return openssl.random(len,false)
|
||||
end
|
||||
|
||||
function R.bytes(len)
|
||||
return openssl.random(len,true)
|
||||
end
|
||||
|
||||
M.rand = R
|
||||
|
||||
-----------crypto pkey compat------------------
|
||||
local P = {}
|
||||
local pkey = openssl.pkey
|
||||
|
||||
local PKEY_M = {}
|
||||
PKEY_M.__index = {}
|
||||
function PKEY_M.__index.to_pem(self,ispriv, israw)
|
||||
israw = israw or false
|
||||
if (ispriv) then
|
||||
return self.evp_pkey:export(true, israw)
|
||||
else
|
||||
return self.evp_pkey:is_private()
|
||||
and self.evp_pkey:get_public():export(true, israw)
|
||||
or self.evp_pkey:export(true, israw)
|
||||
end
|
||||
end
|
||||
|
||||
function PKEY_M.__index.write(self,pubfile,prifile)
|
||||
local PUB,PRI = self:to_pem(false), self:to_pem(true)
|
||||
local f = io.open(pubfile,'w+')
|
||||
if f then
|
||||
f:write(PUB)
|
||||
f:close()
|
||||
end
|
||||
local f = io.open(prifile,'w+')
|
||||
if f then
|
||||
f:write(PRI)
|
||||
f:close()
|
||||
end
|
||||
end
|
||||
|
||||
function P.read(file,ispriv)
|
||||
local f = io.open(file,'r')
|
||||
if f then
|
||||
local pem = f:read("*all")
|
||||
f:close()
|
||||
return P.from_pem(pem,ispriv)
|
||||
end
|
||||
end
|
||||
|
||||
function P.from_pem(pem, ispriv)
|
||||
local k = pkey.read (pem, ispriv,'pem')
|
||||
if k then
|
||||
local key = {}
|
||||
key.evp_pkey = k
|
||||
setmetatable(key,PKEY_M)
|
||||
return key
|
||||
end
|
||||
end
|
||||
|
||||
function P.generate(alg,bits)
|
||||
local k = pkey.new (alg,bits)
|
||||
if k then
|
||||
local key = {}
|
||||
key.evp_pkey = k
|
||||
setmetatable(key,PKEY_M)
|
||||
return key
|
||||
end
|
||||
end
|
||||
|
||||
M.pkey = P
|
||||
|
||||
------------------------------------------
|
||||
function M.sign(alg,input,prikey)
|
||||
local pk = prikey.evp_pkey
|
||||
return pkey.sign(pk,input,alg)
|
||||
end
|
||||
|
||||
function M.verify(alg,input,sig,pubkey)
|
||||
local pk = pubkey.evp_pkey
|
||||
return pkey.verify(pk,input,sig,alg)
|
||||
end
|
||||
|
||||
|
||||
-----------------crypto seal/open compat
|
||||
local S = {}
|
||||
S.__index = {
|
||||
new = function(alg,pubkey)
|
||||
local c,key,iv = pkey.seal_init(pubkey.evp_pkey,alg)
|
||||
if c then
|
||||
local t = {}
|
||||
t.ctx = c
|
||||
t.key = key
|
||||
t.iv = iv
|
||||
setmetatable(t,S)
|
||||
return t
|
||||
end
|
||||
end,
|
||||
update = function(self,data)
|
||||
return pkey.seal_update(self.ctx,data)
|
||||
end,
|
||||
final = function(self)
|
||||
local s = pkey.seal_final(self.ctx)
|
||||
return s,self.key,self.iv
|
||||
end,
|
||||
}
|
||||
S.__call = function(self,alg,input,pubkey)
|
||||
local msg, key,iv = pkey.seal(pubkey.evp_pkey, input, alg)
|
||||
return msg,key,iv
|
||||
end
|
||||
|
||||
setmetatable(S,S)
|
||||
|
||||
M.seal = S
|
||||
|
||||
local O = {}
|
||||
O.__index = {
|
||||
new = function(alg,privkey, ekey, iv)
|
||||
local c,key,iv = pkey.open_init(privkey.evp_pkey,ekey,iv,alg)
|
||||
if c then
|
||||
local t = {}
|
||||
t.ctx = c
|
||||
t.key = key
|
||||
t.iv = iv
|
||||
setmetatable(t,O)
|
||||
return t
|
||||
end
|
||||
end,
|
||||
update = function(self,data)
|
||||
return pkey.open_update(self.ctx,data)
|
||||
end,
|
||||
final = function(self)
|
||||
local s = pkey.open_final(self.ctx)
|
||||
return s
|
||||
end,
|
||||
}
|
||||
O.__call = function(self,alg,input,prikey,ek,iv)
|
||||
return pkey.open(prikey.evp_pkey,input,ek,iv,alg)
|
||||
end
|
||||
|
||||
setmetatable(O,O)
|
||||
|
||||
M.open = O
|
||||
|
||||
----------------crypto pki compat------------
|
||||
local X = {}
|
||||
X.__index = {
|
||||
add_pem = function(self,pem)
|
||||
local ret,x = pcall(openssl.x509.read,pem)
|
||||
if ret then
|
||||
self[#self+1] = x
|
||||
return x
|
||||
end
|
||||
return nil
|
||||
end,
|
||||
verify_pem = function(self,pem)
|
||||
local ret,x = pcall(openssl.x509.read,pem)
|
||||
if ret then
|
||||
local store = openssl.x509.store.new(self)
|
||||
return x:check(store)
|
||||
end
|
||||
return false
|
||||
end
|
||||
}
|
||||
|
||||
function M.x509_ca()
|
||||
local t = {}
|
||||
setmetatable(t,X)
|
||||
return t
|
||||
end
|
||||
|
||||
----------------------------------------------
|
||||
return M
|
||||
@@ -0,0 +1,143 @@
|
||||
----------------------------------------------------------------------------
|
||||
-- LuaSec 0.7alpha
|
||||
-- Copyright (C) 2009-2017 PUC-Rio
|
||||
--
|
||||
-- Author: Pablo Musa
|
||||
-- Author: Tomas Guisasola
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
local socket = require("socket")
|
||||
local ssl = require("ssl")
|
||||
local ltn12 = require("ltn12")
|
||||
local http = require("socket.http")
|
||||
local url = require("socket.url")
|
||||
|
||||
local try = socket.try
|
||||
|
||||
--
|
||||
-- Module
|
||||
--
|
||||
local _M = {
|
||||
_VERSION = "0.7",
|
||||
_COPYRIGHT = "LuaSec 0.7alpha - Copyright (C) 2009-2017 PUC-Rio",
|
||||
PORT = 443,
|
||||
}
|
||||
|
||||
-- TLS configuration
|
||||
local cfg = {
|
||||
protocol = "any",
|
||||
options = {"all", "no_sslv2", "no_sslv3"},
|
||||
verify = "none",
|
||||
}
|
||||
|
||||
--------------------------------------------------------------------
|
||||
-- Auxiliar Functions
|
||||
--------------------------------------------------------------------
|
||||
|
||||
-- Insert default HTTPS port.
|
||||
local function default_https_port(u)
|
||||
return url.build(url.parse(u, {port = _M.PORT}))
|
||||
end
|
||||
|
||||
-- Convert an URL to a table according to Luasocket needs.
|
||||
local function urlstring_totable(url, body, result_table)
|
||||
url = {
|
||||
url = default_https_port(url),
|
||||
method = body and "POST" or "GET",
|
||||
sink = ltn12.sink.table(result_table)
|
||||
}
|
||||
if body then
|
||||
url.source = ltn12.source.string(body)
|
||||
url.headers = {
|
||||
["content-length"] = #body,
|
||||
["content-type"] = "application/x-www-form-urlencoded",
|
||||
}
|
||||
end
|
||||
return url
|
||||
end
|
||||
|
||||
-- Forward calls to the real connection object.
|
||||
local function reg(conn)
|
||||
local mt = getmetatable(conn.sock).__index
|
||||
for name, method in pairs(mt) do
|
||||
if type(method) == "function" then
|
||||
conn[name] = function (self, ...)
|
||||
return method(self.sock, ...)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Return a function which performs the SSL/TLS connection.
|
||||
local function tcp(params)
|
||||
params = params or {}
|
||||
-- Default settings
|
||||
for k, v in pairs(cfg) do
|
||||
params[k] = params[k] or v
|
||||
end
|
||||
-- Force client mode
|
||||
params.mode = "client"
|
||||
-- 'create' function for LuaSocket
|
||||
return function ()
|
||||
local conn = {}
|
||||
conn.sock = try(socket.tcp())
|
||||
local st = getmetatable(conn.sock).__index.settimeout
|
||||
function conn:settimeout(...)
|
||||
return st(self.sock, ...)
|
||||
end
|
||||
-- Replace TCP's connection function
|
||||
function conn:connect(host, port)
|
||||
try(self.sock:connect(host, port))
|
||||
self.sock = try(ssl.wrap(self.sock, params))
|
||||
self.sock:sni(host)
|
||||
try(self.sock:dohandshake())
|
||||
reg(self, getmetatable(self.sock))
|
||||
return 1
|
||||
end
|
||||
return conn
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------
|
||||
-- Main Function
|
||||
--------------------------------------------------------------------
|
||||
|
||||
-- Make a HTTP request over secure connection. This function receives
|
||||
-- the same parameters of LuaSocket's HTTP module (except 'proxy' and
|
||||
-- 'redirect') plus LuaSec parameters.
|
||||
--
|
||||
-- @param url mandatory (string or table)
|
||||
-- @param body optional (string)
|
||||
-- @return (string if url == string or 1), code, headers, status
|
||||
--
|
||||
local function request(url, body)
|
||||
local result_table = {}
|
||||
local stringrequest = type(url) == "string"
|
||||
if stringrequest then
|
||||
url = urlstring_totable(url, body, result_table)
|
||||
else
|
||||
url.url = default_https_port(url.url)
|
||||
end
|
||||
if http.PROXY or url.proxy then
|
||||
return nil, "proxy not supported"
|
||||
elseif url.redirect then
|
||||
return nil, "redirect not supported"
|
||||
elseif url.create then
|
||||
return nil, "create function not permitted"
|
||||
end
|
||||
-- New 'create' function to establish a secure connection
|
||||
url.create = tcp(url)
|
||||
local res, code, headers, status = http.request(url)
|
||||
if res and stringrequest then
|
||||
return table.concat(result_table), code, headers, status
|
||||
end
|
||||
return res, code, headers, status
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Export module
|
||||
--
|
||||
|
||||
_M.request = request
|
||||
|
||||
return _M
|
||||
Executable
+812
@@ -0,0 +1,812 @@
|
||||
local store = require 'jsonschema.store'
|
||||
local tostring = tostring
|
||||
local pairs = pairs
|
||||
local ipairs = ipairs
|
||||
local unpack = unpack or table.unpack
|
||||
local sformat = string.format
|
||||
local mmax, mmodf = math.max, math.modf
|
||||
local tconcat = table.concat
|
||||
local coro_wrap = coroutine.wrap
|
||||
local coro_yield = coroutine.yield
|
||||
local DEBUG = os and os.getenv and os.getenv('DEBUG') == '1'
|
||||
|
||||
-- default null token
|
||||
local default_null = nil
|
||||
do
|
||||
local ok, cjson = pcall(require, 'cjson')
|
||||
if ok then default_null = cjson.null end
|
||||
end
|
||||
|
||||
--
|
||||
-- Code generation
|
||||
--
|
||||
|
||||
local generate_validator -- forward declaration
|
||||
|
||||
local codectx_mt = {}
|
||||
codectx_mt.__index = codectx_mt
|
||||
|
||||
function codectx_mt:libfunc(globalname)
|
||||
local root = self._root
|
||||
local localname = root._globals[globalname]
|
||||
if not localname then
|
||||
localname = globalname:gsub('%.', '_')
|
||||
root._globals[globalname] = localname
|
||||
root:preface(sformat('local %s = %s', localname, globalname))
|
||||
end
|
||||
return localname
|
||||
end
|
||||
|
||||
function codectx_mt:localvar(init, nres)
|
||||
local names = {}
|
||||
local nloc = self._nloc
|
||||
nres = nres or 1
|
||||
for i=1, nres do
|
||||
names[i] = sformat('var_%d_%d', self._idx, nloc+i)
|
||||
end
|
||||
|
||||
self:stmt(sformat('local %s = ', tconcat(names, ', ')), init or 'nil')
|
||||
self._nloc = nloc + nres
|
||||
return unpack(names)
|
||||
end
|
||||
|
||||
function codectx_mt:param(n)
|
||||
self._nparams = mmax(n, self._nparams)
|
||||
return 'p_' .. n
|
||||
end
|
||||
|
||||
function codectx_mt:label()
|
||||
local nlabel = self._nlabels + 1
|
||||
self._nlabels = nlabel
|
||||
return 'label_' .. nlabel
|
||||
end
|
||||
|
||||
-- Returns an expression that will result in passed value.
|
||||
-- Currently user vlaues are stored in an array to avoid consuming a lot of local
|
||||
-- and upvalue slots. Array accesses are still decently fast.
|
||||
function codectx_mt:uservalue(val)
|
||||
local slot = #self._root._uservalues + 1
|
||||
self._root._uservalues[slot] = val
|
||||
return sformat('uservalues[%d]', slot)
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
local function q(s) return sformat('%q', s) end
|
||||
|
||||
function codectx_mt:validator(path, schema)
|
||||
local ref = self._schema:child(path)
|
||||
local resolved = ref:resolve()
|
||||
local root = self._root
|
||||
local var = root._validators[resolved]
|
||||
if not var then
|
||||
var = root:localvar('nil')
|
||||
root._validators[resolved] = var
|
||||
root:stmt(sformat('%s = ', var), generate_validator(root:child(ref), resolved))
|
||||
end
|
||||
return var
|
||||
end
|
||||
|
||||
function codectx_mt:preface(...)
|
||||
assert(self._preface, 'preface is only available for root contexts')
|
||||
local n = #self._preface
|
||||
for i=1, select('#', ...) do
|
||||
self._preface[n+i] = select(i, ...)
|
||||
end
|
||||
self._preface[#self._preface+1] = '\n'
|
||||
end
|
||||
|
||||
function codectx_mt:stmt(...)
|
||||
local n = #self._body
|
||||
for i=1, select('#', ...) do
|
||||
self._body[n+i] = select(i, ...)
|
||||
end
|
||||
self._body[#self._body+1] = '\n'
|
||||
end
|
||||
|
||||
-- load doesn't like at all empty string, but sometimes it is easier to add
|
||||
-- some in the chunk buffer
|
||||
local function yield_chunk(chunk)
|
||||
if chunk and chunk ~= '' then
|
||||
coro_yield(chunk)
|
||||
end
|
||||
end
|
||||
|
||||
function codectx_mt:_generate()
|
||||
local indent = ''
|
||||
if self._root == self then
|
||||
for _, stmt in ipairs(self._preface) do
|
||||
yield_chunk(indent)
|
||||
if getmetatable(stmt) == codectx_mt then
|
||||
stmt:_generate()
|
||||
else
|
||||
yield_chunk(stmt)
|
||||
end
|
||||
end
|
||||
else
|
||||
coro_yield('function(')
|
||||
for i=1, self._nparams do
|
||||
yield_chunk('p_' .. i)
|
||||
if i ~= self._nparams then yield_chunk(', ') end
|
||||
end
|
||||
yield_chunk(')\n')
|
||||
indent = string.rep(' ', self._idx)
|
||||
end
|
||||
|
||||
for _, stmt in ipairs(self._body) do
|
||||
yield_chunk(indent)
|
||||
if getmetatable(stmt) == codectx_mt then
|
||||
stmt:_generate()
|
||||
else
|
||||
yield_chunk(stmt)
|
||||
end
|
||||
end
|
||||
|
||||
if self._root ~= self then
|
||||
yield_chunk('end')
|
||||
end
|
||||
end
|
||||
|
||||
function codectx_mt:_get_loader()
|
||||
return coro_wrap(function()
|
||||
self:_generate()
|
||||
end)
|
||||
end
|
||||
|
||||
function codectx_mt:as_string()
|
||||
local buf, n = {}, 0
|
||||
for chunk in self:_get_loader() do
|
||||
n = n+1
|
||||
buf[n] = chunk
|
||||
end
|
||||
return table.concat(buf)
|
||||
end
|
||||
|
||||
function codectx_mt:as_func(name, ...)
|
||||
local loader, err = load(self:_get_loader(), 'jsonschema:' .. (name or 'anonymous'))
|
||||
if loader then
|
||||
local validator
|
||||
validator, err = loader(self._uservalues, ...)
|
||||
if validator then return validator end
|
||||
end
|
||||
|
||||
-- something went really wrong
|
||||
if DEBUG then
|
||||
local line=1
|
||||
print('------------------------------')
|
||||
print('FAILED to generate validator: ', err)
|
||||
print('generated code:')
|
||||
print('0001: ' .. self:as_string():gsub('\n', function()
|
||||
line = line + 1
|
||||
return sformat('\n%04d: ', line)
|
||||
end))
|
||||
print('------------------------------')
|
||||
end
|
||||
error(err)
|
||||
end
|
||||
|
||||
-- returns a child code context with the current context as parent
|
||||
function codectx_mt:child(ref)
|
||||
return setmetatable({
|
||||
_schema = ref,
|
||||
_idx = self._idx+1,
|
||||
_nloc = 0,
|
||||
_nlabels = 0,
|
||||
_body = {},
|
||||
_root = self._root,
|
||||
_nparams = 0,
|
||||
}, codectx_mt)
|
||||
end
|
||||
|
||||
-- returns a root code context. A root code context holds the library function
|
||||
-- cache (as upvalues for the child contexts), a preface, and no named params
|
||||
local function codectx(schema, options)
|
||||
local self = setmetatable({
|
||||
_schema = store.new(schema, options.external_resolver),
|
||||
_id = schema.id,
|
||||
_path = '',
|
||||
_idx = 0,
|
||||
-- code generation
|
||||
_nloc = 0,
|
||||
_nlabels = 0,
|
||||
_preface = {},
|
||||
_body = {},
|
||||
_globals = {},
|
||||
_uservalues = {},
|
||||
-- schema management
|
||||
_validators = {}, -- maps paths to local variable validators
|
||||
_external_resolver = options.external_resolver,
|
||||
}, codectx_mt)
|
||||
self._root = self
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
--
|
||||
-- Validator util functions (available in the validator context
|
||||
--
|
||||
local validatorlib = {}
|
||||
|
||||
-- TODO: this function is critical for performance, optimize it
|
||||
-- Returns:
|
||||
-- 0 for objects
|
||||
-- 1 for empty object/table (these two are indistinguishable in Lua)
|
||||
-- 2 for arrays
|
||||
function validatorlib.tablekind(t)
|
||||
local length = #t
|
||||
if length == 0 then
|
||||
if next(t) == nil then
|
||||
return 1 -- empty table
|
||||
else
|
||||
return 0 -- pure hash
|
||||
end
|
||||
end
|
||||
|
||||
-- not empty, check if the number of items is the same as the length
|
||||
local items = 0
|
||||
for k, v in pairs(t) do items = items + 1 end
|
||||
if items == #t then
|
||||
return 2 -- array
|
||||
else
|
||||
return 0 -- mixed array/object
|
||||
end
|
||||
end
|
||||
|
||||
-- used for unique items in arrays (not fast at all)
|
||||
-- from: http://stackoverflow.com/questions/25922437
|
||||
-- If we consider only the JSON case, this function could be simplified:
|
||||
-- no loops, keys are only strings. But this library might also be used in
|
||||
-- other cases.
|
||||
local function deepeq(table1, table2)
|
||||
local avoid_loops = {}
|
||||
local function recurse(t1, t2)
|
||||
-- compare value types
|
||||
if type(t1) ~= type(t2) then return false end
|
||||
-- Base case: compare simple values
|
||||
if type(t1) ~= "table" then return t1 == t2 end
|
||||
-- Now, on to tables.
|
||||
-- First, let's avoid looping forever.
|
||||
if avoid_loops[t1] then return avoid_loops[t1] == t2 end
|
||||
avoid_loops[t1] = t2
|
||||
-- Copy keys from t2
|
||||
local t2keys = {}
|
||||
local t2tablekeys = {}
|
||||
for k, _ in pairs(t2) do
|
||||
if type(k) == "table" then table.insert(t2tablekeys, k) end
|
||||
t2keys[k] = true
|
||||
end
|
||||
-- Let's iterate keys from t1
|
||||
for k1, v1 in pairs(t1) do
|
||||
local v2 = t2[k1]
|
||||
if type(k1) == "table" then
|
||||
-- if key is a table, we need to find an equivalent one.
|
||||
local ok = false
|
||||
for i, tk in ipairs(t2tablekeys) do
|
||||
if deepeq(k1, tk) and recurse(v1, t2[tk]) then
|
||||
table.remove(t2tablekeys, i)
|
||||
t2keys[tk] = nil
|
||||
ok = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not ok then return false end
|
||||
else
|
||||
-- t1 has a key which t2 doesn't have, fail.
|
||||
if v2 == nil then return false end
|
||||
t2keys[k1] = nil
|
||||
if not recurse(v1, v2) then return false end
|
||||
end
|
||||
end
|
||||
-- if t2 has a key which t1 doesn't have, fail.
|
||||
if next(t2keys) then return false end
|
||||
return true
|
||||
end
|
||||
return recurse(table1, table2)
|
||||
end
|
||||
validatorlib.deepeq = deepeq
|
||||
|
||||
|
||||
--
|
||||
-- Validation generator
|
||||
--
|
||||
|
||||
-- generate an expression to check a JSON type
|
||||
local function typeexpr(ctx, jsontype, datatype, tablekind)
|
||||
-- TODO: optimize the type check for arays/objects (using NaN as kind?)
|
||||
if jsontype == 'object' then
|
||||
return sformat(' %s == "table" and %s <= 1 ', datatype, tablekind)
|
||||
elseif jsontype == 'array' then
|
||||
return sformat(' %s == "table" and %s >= 1 ', datatype, tablekind)
|
||||
elseif jsontype == 'table' then
|
||||
return sformat(' %s == "table" ', datatype)
|
||||
elseif jsontype == 'integer' then
|
||||
return sformat(' (%s == "number" and %s(%s, 1.0) == 0.0) ',
|
||||
datatype, ctx:libfunc('math.fmod'), ctx:param(1))
|
||||
elseif jsontype == 'string' or jsontype == 'boolean' or jsontype == 'number' then
|
||||
return sformat('%s == %q', datatype, jsontype)
|
||||
elseif jsontype == 'null' then
|
||||
return sformat('%s == %s', ctx:param(1), ctx:libfunc('custom.null'))
|
||||
elseif jsontype == 'function' then
|
||||
return sformat(' %s == "function" ', datatype)
|
||||
else
|
||||
error('invalid JSON type: ' .. jsontype)
|
||||
end
|
||||
end
|
||||
|
||||
generate_validator = function(ctx, schema)
|
||||
-- get type informations as they will be necessary anyway
|
||||
local datatype = ctx:localvar(sformat('%s(%s)',
|
||||
ctx:libfunc('type'), ctx:param(1)))
|
||||
local datakind = ctx:localvar(sformat('%s == "table" and %s(%s)',
|
||||
datatype, ctx:libfunc('lib.tablekind'), ctx:param(1)))
|
||||
|
||||
-- type check
|
||||
local tt = type(schema.type)
|
||||
if tt == 'string' then
|
||||
-- only one type allowed
|
||||
ctx:stmt('if not (', typeexpr(ctx, schema.type, datatype, datakind), ') then')
|
||||
ctx:stmt(sformat(' return false, "wrong type: expected %s, got " .. %s', schema.type, datatype))
|
||||
ctx:stmt('end')
|
||||
elseif tt == 'table' then
|
||||
-- multiple types allowed
|
||||
ctx:stmt('if not (')
|
||||
for _, t in ipairs(schema.type) do
|
||||
ctx:stmt(' ', typeexpr(ctx, t, datatype, datakind), ' or')
|
||||
end
|
||||
ctx:stmt('false) then') -- close the last "or" statement
|
||||
ctx:stmt(sformat(' return false, "wrong type: expected one of %s, got " .. %s', table.concat(schema.type, ', '), datatype))
|
||||
ctx:stmt('end')
|
||||
elseif tt ~= 'nil' then error('invalid "type" type: got ' .. tt) end
|
||||
|
||||
-- properties check
|
||||
if schema.properties or
|
||||
schema.additionalProperties or
|
||||
schema.patternProperties or
|
||||
schema.minProperties or
|
||||
schema.maxProperties or
|
||||
schema.dependencies
|
||||
then
|
||||
-- check properties, this differs from the spec as empty arrays are
|
||||
-- considered as object
|
||||
ctx:stmt(sformat('if %s == "table" and %s <= 1 then', datatype, datakind))
|
||||
|
||||
-- switch the required keys list to a set
|
||||
local required = {}
|
||||
local dependencies = schema.dependencies or {}
|
||||
local properties = schema.properties or {}
|
||||
if schema.required then
|
||||
for _, k in ipairs(schema.required) do required[k] = true end
|
||||
end
|
||||
|
||||
-- opportunistically count keys if we walk the table
|
||||
local needcount = schema.minProperties or schema.maxProperties
|
||||
if needcount then
|
||||
ctx:stmt( ' local propcount = 0')
|
||||
end
|
||||
|
||||
for prop, subschema in pairs(properties) do
|
||||
-- generate validator
|
||||
local propvalidator = ctx:validator({ 'properties', prop }, subschema)
|
||||
ctx:stmt( ' do')
|
||||
ctx:stmt(sformat( ' local propvalue = %s[%q]', ctx:param(1), prop))
|
||||
ctx:stmt( ' if propvalue ~= nil then')
|
||||
ctx:stmt(sformat( ' local ok, err = %s(propvalue)', propvalidator))
|
||||
ctx:stmt( ' if not ok then')
|
||||
ctx:stmt(sformat( " return false, 'property %q validation failed: ' .. err", prop))
|
||||
ctx:stmt( ' end')
|
||||
|
||||
if dependencies[prop] then
|
||||
local d = dependencies[prop]
|
||||
if #d > 0 then
|
||||
-- dependency is a list of properties
|
||||
for _, depprop in ipairs(d) do
|
||||
ctx:stmt(sformat(' if %s[%q] == nil then', ctx:param(1), depprop))
|
||||
ctx:stmt(sformat(" return false, 'property %q is required when %q is set'", depprop, prop))
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
else
|
||||
-- dependency is a schema
|
||||
local depvalidator = ctx:validator({ 'dependencies', prop }, d)
|
||||
-- ok and err are already defined in this block
|
||||
ctx:stmt(sformat(' ok, err = %s(%s)', depvalidator, ctx:param(1)))
|
||||
ctx:stmt( ' if not ok then')
|
||||
ctx:stmt(sformat(" return false, 'failed to validate dependent schema for %q: ' .. err", prop))
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
end
|
||||
|
||||
if required[prop] then
|
||||
ctx:stmt( ' else')
|
||||
ctx:stmt(sformat(" return false, 'property %q is required'", prop))
|
||||
required[prop] = nil
|
||||
end
|
||||
ctx:stmt( ' end') -- if prop
|
||||
ctx:stmt( ' end') -- do
|
||||
end
|
||||
|
||||
-- check the rest of required fields
|
||||
for prop, _ in pairs(required) do
|
||||
ctx:stmt(sformat(' if %s[%q] == nil then', ctx:param(1), prop))
|
||||
ctx:stmt(sformat(" return false, 'property %q is required'", prop))
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
|
||||
-- check the rest of dependencies
|
||||
for prop, d in pairs(dependencies) do
|
||||
if not properties[prop] then
|
||||
if #d > 0 then
|
||||
-- dependencies are a list of properties
|
||||
for _, depprop in ipairs(d) do
|
||||
ctx:stmt(sformat(' if %s[%q] ~= nil and %s[%q] == nil then', ctx:param(1), prop, ctx:param(1), depprop))
|
||||
ctx:stmt(sformat(" return false, 'property %q is required when %q is set'", depprop, prop))
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
else
|
||||
-- dependency is a schema
|
||||
local depvalidator = ctx:validator({ 'dependencies', prop }, d)
|
||||
ctx:stmt(sformat(' if %s[%q] ~= nil then', ctx:param(1), prop))
|
||||
ctx:stmt(sformat(' local ok, err = %s(%s)', depvalidator, ctx:param(1)))
|
||||
ctx:stmt( ' if not ok then')
|
||||
ctx:stmt(sformat(" return false, 'failed to validate dependent schema for %q: ' .. err", prop))
|
||||
ctx:stmt( ' end')
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- patternProperties and additionalProperties
|
||||
local propset, addprop_validator -- all properties defined in the object
|
||||
if schema.additionalProperties ~= nil and schema.additionalProperties ~= true then
|
||||
-- TODO: can be optimized with a static table expression
|
||||
propset = ctx._root:localvar('{}')
|
||||
if schema.properties then
|
||||
for prop, _ in pairs(schema.properties) do
|
||||
ctx._root:stmt(sformat('%s[%q] = true', propset, prop))
|
||||
end
|
||||
end
|
||||
|
||||
if type(schema.additionalProperties) == 'table' then
|
||||
addprop_validator = ctx:validator({ 'additionalProperties' }, schema.additionalProperties)
|
||||
end
|
||||
end
|
||||
|
||||
-- patternProperties and additionalProperties are matched together whenever
|
||||
-- possible in order to walk the table only once
|
||||
if schema.patternProperties then
|
||||
local patterns = {}
|
||||
for patt, patt_schema in pairs(schema.patternProperties) do
|
||||
patterns[patt] = ctx:validator({ 'patternProperties', patt }, patt_schema )
|
||||
end
|
||||
|
||||
ctx:stmt(sformat( ' for prop, value in %s(%s) do', ctx:libfunc('pairs'), ctx:param(1)))
|
||||
if propset then
|
||||
ctx:stmt( ' local matched = false')
|
||||
for patt, validator in pairs(patterns) do
|
||||
ctx:stmt(sformat(' if %s(prop, %q) then', ctx:libfunc('custom.match_pattern'), patt))
|
||||
ctx:stmt(sformat(' local ok, err = %s(value)', validator))
|
||||
ctx:stmt( ' if not ok then')
|
||||
ctx:stmt(sformat(" return false, 'failed to validate '..prop..' (matching %q): '..err", patt))
|
||||
ctx:stmt( ' end')
|
||||
ctx:stmt( ' matched = true')
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
-- additional properties check
|
||||
ctx:stmt(sformat( ' if not (%s[prop] or matched) then', propset))
|
||||
if addprop_validator then
|
||||
-- the additional properties must match a schema
|
||||
ctx:stmt(sformat(' local ok, err = %s(value)', addprop_validator))
|
||||
ctx:stmt( ' if not ok then')
|
||||
ctx:stmt( " return false, 'failed to validate additional property '..prop..': '..err")
|
||||
ctx:stmt( ' end')
|
||||
else
|
||||
-- additional properties are forbidden
|
||||
ctx:stmt( ' return false, "additional properties forbidden, found " .. prop')
|
||||
end
|
||||
ctx:stmt( ' end') -- if not (%s[prop] or matched)
|
||||
else
|
||||
for patt, validator in pairs(patterns) do
|
||||
ctx:stmt(sformat(' if %s(prop, %q) then', ctx:libfunc('custom.match_pattern'), patt))
|
||||
ctx:stmt(sformat(' local ok, err = %s(value)', validator))
|
||||
ctx:stmt( ' if not ok then')
|
||||
ctx:stmt(sformat(" return false, 'failed to validate '..prop..' (matching %q): '..err", patt))
|
||||
ctx:stmt( ' end')
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
end
|
||||
if needcount then
|
||||
ctx:stmt( ' propcount = propcount + 1')
|
||||
end
|
||||
ctx:stmt( ' end') -- for
|
||||
elseif propset then
|
||||
-- additionalProperties alone
|
||||
ctx:stmt(sformat( ' for prop, value in %s(%s) do', ctx:libfunc('pairs'), ctx:param(1)))
|
||||
ctx:stmt(sformat( ' if not %s[prop] then', propset))
|
||||
if addprop_validator then
|
||||
-- the additional properties must match a schema
|
||||
ctx:stmt(sformat(' local ok, err = %s(value)', addprop_validator))
|
||||
ctx:stmt( ' if not ok then')
|
||||
ctx:stmt( " return false, 'failed to validate additional property '..prop..': '..err")
|
||||
ctx:stmt( ' end')
|
||||
else
|
||||
-- additional properties are forbidden
|
||||
ctx:stmt( ' return false, "additional properties forbidden, found " .. prop')
|
||||
end
|
||||
ctx:stmt( ' end') -- if not %s[prop]
|
||||
if needcount then
|
||||
ctx:stmt( ' propcount = propcount + 1')
|
||||
end
|
||||
ctx:stmt( ' end') -- for prop
|
||||
elseif needcount then
|
||||
-- we might still need to walk the table to get the number of properties
|
||||
ctx:stmt(sformat( ' for _, _ in %s(%s) do', ctx:libfunc('pairs'), ctx:param(1)))
|
||||
ctx:stmt( ' propcount = propcount + 1')
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
|
||||
if schema.minProperties then
|
||||
ctx:stmt(sformat(' if propcount < %d then', schema.minProperties))
|
||||
ctx:stmt(sformat(' return false, "expect object to have at least %s properties"', schema.minProperties))
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
if schema.maxProperties then
|
||||
ctx:stmt(sformat(' if propcount > %d then', schema.maxProperties))
|
||||
ctx:stmt(sformat(' return false, "expect object to have at most %s properties"', schema.maxProperties))
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
|
||||
ctx:stmt('end') -- if object
|
||||
end
|
||||
|
||||
-- array checks
|
||||
if schema.items or schema.minItems or schema.maxItems or schema.uniqueItems then
|
||||
ctx:stmt(sformat('if %s == "table" and %s >= 1 then', datatype, datakind))
|
||||
|
||||
-- this check is rather cheap so do it before validating the items
|
||||
-- NOTE: getting the size could be avoided in the list validation case, but
|
||||
-- this would mean validating items beforehand
|
||||
if schema.minItems or schema.maxItems then
|
||||
ctx:stmt(sformat( ' local itemcount = #%s', ctx:param(1)))
|
||||
if schema.minItems then
|
||||
ctx:stmt(sformat(' if itemcount < %d then', schema.minItems))
|
||||
ctx:stmt(sformat(' return false, "expect array to have at least %s items"', schema.minItems))
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
if schema.maxItems then
|
||||
ctx:stmt(sformat(' if itemcount > %d then', schema.maxItems))
|
||||
ctx:stmt(sformat(' return false, "expect array to have at least %s items"', schema.maxItems))
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
end
|
||||
|
||||
if schema.items and #schema.items > 0 then
|
||||
-- each item has a specific schema applied (tuple validation)
|
||||
|
||||
-- From the section 5.1.3.2, missing an array with missing items is
|
||||
-- still valid, because... Well because! So we have to jump after
|
||||
-- validations whenever we meet a nil value
|
||||
local after = ctx:label()
|
||||
for i, ischema in ipairs(schema.items) do
|
||||
-- JSON arrays are zero-indexed: remove 1 for URI path
|
||||
local ivalidator = ctx:validator({ 'items', tostring(i-1) }, ischema)
|
||||
ctx:stmt( ' do')
|
||||
ctx:stmt(sformat(' local item = %s[%d]', ctx:param(1), i))
|
||||
ctx:stmt(sformat(' if item == nil then goto %s end', after))
|
||||
ctx:stmt(sformat(' local ok, err = %s(item)', ivalidator))
|
||||
ctx:stmt(sformat(' if not ok then'))
|
||||
ctx:stmt(sformat(' return false, "failed to validate item %d: " .. err', i))
|
||||
ctx:stmt( ' end')
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
|
||||
-- additional items check
|
||||
if schema.additionalItems == false then
|
||||
ctx:stmt(sformat(' if %s[%d] ~= nil then', ctx:param(1), #schema.items+1))
|
||||
ctx:stmt( ' return false, "found unexpected extra items in array"')
|
||||
ctx:stmt( ' end')
|
||||
elseif type(schema.additionalItems) == 'table' then
|
||||
local validator = ctx:validator({ 'additionalItems' }, schema.additionalItems)
|
||||
ctx:stmt(sformat(' for i=%d, #%s do', #schema.items+1, ctx:param(1)))
|
||||
ctx:stmt(sformat(' local ok, err = %s(%s[i])', validator, ctx:param(1)))
|
||||
ctx:stmt(sformat(' if not ok then'))
|
||||
ctx:stmt(sformat(' return false, %s("failed to validate additional item %%d: %%s", i, err)', ctx:libfunc('string.format')))
|
||||
ctx:stmt( ' end')
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
|
||||
ctx:stmt(sformat( '::%s::', after))
|
||||
elseif schema.items then
|
||||
-- all of the items has to match the same schema (list validation)
|
||||
local validator = ctx:validator({ 'items' }, schema.items)
|
||||
ctx:stmt(sformat(' for i, item in %s(%s) do', ctx:libfunc('ipairs'), ctx:param(1)))
|
||||
ctx:stmt(sformat(' local ok, err = %s(item)', validator))
|
||||
ctx:stmt(sformat(' if not ok then'))
|
||||
ctx:stmt(sformat(' return false, %s("failed to validate item %%d: %%s", i, err)', ctx:libfunc('string.format')))
|
||||
ctx:stmt( ' end')
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
|
||||
-- TODO: this is slow as hell, could be optimized by storing value items
|
||||
-- in a spearate set, and calling deepeq only for references.
|
||||
if schema.uniqueItems then
|
||||
ctx:stmt(sformat(' for i=2, #%s do', ctx:param(1)))
|
||||
ctx:stmt( ' for j=1, i-1 do')
|
||||
ctx:stmt(sformat(' if %s(%s[i], %s[j]) then', ctx:libfunc('lib.deepeq'), ctx:param(1), ctx:param(1)))
|
||||
ctx:stmt(sformat(' return false, %s("expected unique items but items %%d and %%d are equal", i, j)', ctx:libfunc('string.format')))
|
||||
ctx:stmt( ' end')
|
||||
ctx:stmt( ' end')
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
ctx:stmt('end') -- if array
|
||||
end
|
||||
|
||||
if schema.minLength or schema.maxLength or schema.pattern then
|
||||
ctx:stmt(sformat('if %s == "string" then', datatype))
|
||||
if schema.minLength then
|
||||
ctx:stmt(sformat(' if #%s < %d then', ctx:param(1), schema.minLength))
|
||||
ctx:stmt(sformat(' return false, %s("string too short, expected at least %d, got %%d", #%s)',
|
||||
ctx:libfunc('string.format'), schema.minLength, ctx:param(1)))
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
if schema.maxLength then
|
||||
ctx:stmt(sformat(' if #%s > %d then', ctx:param(1), schema.maxLength))
|
||||
ctx:stmt(sformat(' return false, %s("string too long, expected at most %d, got %%d", #%s)',
|
||||
ctx:libfunc('string.format'), schema.maxLength, ctx:param(1)))
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
if schema.pattern then
|
||||
ctx:stmt(sformat(' if not %s(%s, %q) then', ctx:libfunc('custom.match_pattern'), ctx:param(1), schema.pattern))
|
||||
ctx:stmt(sformat(' return false, %s([[failed to match pattern %q with %%q]], %s)', ctx:libfunc('string.format'), schema.pattern, ctx:param(1)))
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
ctx:stmt('end') -- if string
|
||||
end
|
||||
|
||||
if schema.minimum or schema.maximum or schema.multipleOf then
|
||||
ctx:stmt(sformat('if %s == "number" then', datatype))
|
||||
|
||||
if schema.minimum then
|
||||
local op = schema.exclusiveMinimum and '<=' or '<'
|
||||
local msg = schema.exclusiveMinimum and 'sctrictly greater' or 'greater'
|
||||
ctx:stmt(sformat(' if %s %s %s then', ctx:param(1), op, schema.minimum))
|
||||
ctx:stmt(sformat(' return false, %s("expected %%s to be %s than %s", %s)',
|
||||
ctx:libfunc('string.format'), msg, schema.minimum, ctx:param(1)))
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
|
||||
if schema.maximum then
|
||||
local op = schema.exclusiveMaximum and '>=' or '>'
|
||||
local msg = schema.exclusiveMaximum and 'sctrictly smaller' or 'smaller'
|
||||
ctx:stmt(sformat(' if %s %s %s then', ctx:param(1), op, schema.maximum))
|
||||
ctx:stmt(sformat(' return false, %s("expected %%s to be %s than %s", %s)',
|
||||
ctx:libfunc('string.format'), msg, schema.maximum, ctx:param(1)))
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
|
||||
local mof = schema.multipleOf
|
||||
if mof then
|
||||
-- TODO: optimize integer case
|
||||
if mmodf(mof) == mof then
|
||||
-- integer multipleOf: modulo is enough
|
||||
ctx:stmt(sformat(' if %s %% %d ~= 0 then', ctx:param(1), mof))
|
||||
else
|
||||
-- float multipleOf: it's a bit more hacky and slow
|
||||
ctx:stmt(sformat(' local quotient = %s / %s', ctx:param(1), mof))
|
||||
ctx:stmt(sformat(' if %s(quotient) ~= quotient then', ctx:libfunc('math.modf')))
|
||||
end
|
||||
ctx:stmt(sformat( ' return false, %s("expected %%s to be a multiple of %s", %s)',
|
||||
ctx:libfunc('string.format'), mof, ctx:param(1)))
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
ctx:stmt('end') -- if number
|
||||
end
|
||||
|
||||
-- enum values
|
||||
-- TODO: for big sets of hashable values (> 16 or so), it might be intersing to create a
|
||||
-- table beforehand
|
||||
if schema.enum then
|
||||
ctx:stmt('if not (')
|
||||
local lasti = #schema.enum
|
||||
for i, val in ipairs(schema.enum) do
|
||||
local tval = type(val)
|
||||
local op = i == lasti and '' or ' or'
|
||||
|
||||
if tval == 'number' or tval == 'boolean' then
|
||||
ctx:stmt(sformat(' %s == %s', ctx:param(1), val), op)
|
||||
elseif tval == 'string' then
|
||||
ctx:stmt(sformat(' %s == %q', ctx:param(1), val), op)
|
||||
elseif tval == 'table' then
|
||||
ctx:stmt(sformat(' %s(%s, %s)', ctx:libfunc('lib.deepeq'), ctx:param(1), ctx:uservalue(val)), op)
|
||||
else
|
||||
error('unsupported enum type: ' .. tval) -- TODO: null
|
||||
end
|
||||
end
|
||||
ctx:stmt(') then')
|
||||
ctx:stmt(' return false, "matches non of the enum values"')
|
||||
ctx:stmt('end')
|
||||
end
|
||||
|
||||
-- compound schemas
|
||||
-- (very naive implementation for now, can be optimized a lot)
|
||||
if schema.allOf then
|
||||
for i, subschema in ipairs(schema.allOf) do
|
||||
local validator = ctx:validator({ 'allOf', tostring(i-1) }, subschema)
|
||||
ctx:stmt( 'do')
|
||||
ctx:stmt(sformat(' local ok, err = %s(%s)', validator, ctx:param(1)))
|
||||
ctx:stmt(sformat(' if not ok then'))
|
||||
ctx:stmt(sformat(' return false, "allOf %d failed: " .. err', i))
|
||||
ctx:stmt( ' end')
|
||||
ctx:stmt( 'end')
|
||||
end
|
||||
end
|
||||
|
||||
if schema.anyOf then
|
||||
local lasti = #schema.anyOf
|
||||
ctx:stmt('if not (')
|
||||
for i, subschema in ipairs(schema.anyOf) do
|
||||
local op = i == lasti and '' or ' or'
|
||||
local validator = ctx:validator({ 'anyOf', tostring(i-1) }, subschema)
|
||||
ctx:stmt(sformat(' %s(%s)', validator, ctx:param(1)), op)
|
||||
end
|
||||
ctx:stmt(') then')
|
||||
ctx:stmt(' return false, "object matches none of the alternatives"')
|
||||
ctx:stmt('end')
|
||||
end
|
||||
|
||||
if schema.oneOf then
|
||||
ctx:stmt('do')
|
||||
ctx:stmt(' local matched')
|
||||
for i, subschema in ipairs(schema.oneOf) do
|
||||
local validator = ctx:validator({ 'oneOf', tostring(i-1) }, subschema)
|
||||
ctx:stmt(sformat(' if %s(%s) then', validator, ctx:param(1)))
|
||||
ctx:stmt( ' if matched then')
|
||||
ctx:stmt(sformat(' return false, %s("value sould match only one schema, but matches both schemas %%d and %%d", matched, %d)',
|
||||
ctx:libfunc('string.format'), i))
|
||||
ctx:stmt( ' end')
|
||||
ctx:stmt( ' matched = ', tostring(i))
|
||||
ctx:stmt( ' end')
|
||||
end
|
||||
ctx:stmt(' if not matched then')
|
||||
ctx:stmt(' return false, "value sould match only one schema, but matches none"')
|
||||
ctx:stmt(' end')
|
||||
ctx:stmt('end')
|
||||
end
|
||||
|
||||
if schema['not'] then
|
||||
local validator = ctx:validator({ 'not' }, schema['not'])
|
||||
ctx:stmt(sformat('if %s(%s) then', validator, ctx:param(1)))
|
||||
ctx:stmt( ' return false, "value wasn\'t supposed to match schema"')
|
||||
ctx:stmt( 'end')
|
||||
end
|
||||
|
||||
ctx:stmt('return true')
|
||||
return ctx
|
||||
end
|
||||
|
||||
local function generate_main_validator_ctx(schema, options)
|
||||
local ctx = codectx(schema, options or {})
|
||||
-- the root function takes two parameters:
|
||||
-- * the validation library (auxiliary function used during validation)
|
||||
-- * the custom callbacks (used to customize various aspects of validation
|
||||
-- or for dependency injection)
|
||||
ctx:preface('local uservalues, lib, custom = ...')
|
||||
ctx:stmt('return ', ctx:validator(nil, schema))
|
||||
return ctx
|
||||
end
|
||||
|
||||
return {
|
||||
generate_validator = function(schema, custom)
|
||||
local customlib = {
|
||||
null = custom and custom.null or default_null,
|
||||
match_pattern = custom and custom.match_pattern or string.find
|
||||
}
|
||||
local name = custom and custom.name
|
||||
return generate_main_validator_ctx(schema, custom):as_func(name, validatorlib, customlib)
|
||||
end,
|
||||
-- debug only
|
||||
generate_validator_code = function(schema, custom)
|
||||
return generate_main_validator_ctx(schema, custom):as_string()
|
||||
end,
|
||||
}
|
||||
Executable
+235
@@ -0,0 +1,235 @@
|
||||
-- This module is a store for all schemas unsed in a code context.
|
||||
-- It is meant to deal with the id and $ref madness that JSON schema authors
|
||||
-- managed to put together. Resolving JSON references involves full URI
|
||||
-- parsing, absolute/relative URLs, scope management, id aliases, multipass
|
||||
-- parsing (as you have to walk the document a first time to discover all ids)
|
||||
-- and other niceties.
|
||||
--
|
||||
-- Don't try to find any logic in this code, there isn't: this is just an
|
||||
-- implementation of [1] which is foreign to the concept of *logic*.
|
||||
--
|
||||
-- [1] http://json-schema.org/latest/json-schema-core.html#rfc.section.8
|
||||
|
||||
-- I gave up (for now) on doing a stripped down URI parser only for JSON schema
|
||||
-- needs
|
||||
local url = require 'neturl'
|
||||
local schar = string.char
|
||||
|
||||
-- the net.url is kinda weird when some uri parts are missing (sometimes it is
|
||||
-- nil, sometimes it is an empty string)
|
||||
local function noe(s) return s == nil or s == '' end
|
||||
|
||||
-- fetching and parsing external schemas requires a lot of dependencies, and
|
||||
-- depends a lot on the application ecosystem (e.g. piping curl, LuaSocket,
|
||||
-- cqueues, ...). Moreover, most sane schemas are self contained, so it is not
|
||||
-- even useful.
|
||||
-- So it is up to the user to provide a resolver if it's really needed
|
||||
local function default_resolver(uri)
|
||||
error('an external resolver is required to fetch ' .. uri)
|
||||
end
|
||||
|
||||
local function percent_unescape(x)
|
||||
return schar(tonumber(x, 16))
|
||||
end
|
||||
local tilde_unescape = { ['~0']='~', ['~1']='/' }
|
||||
local function urlunescape(fragment)
|
||||
return fragment:gsub('%%(%x%x)', percent_unescape):gsub('~[01]', tilde_unescape)
|
||||
end
|
||||
|
||||
-- attempt to translate a URI fragemnt part to a valid table index:
|
||||
-- * if the part can be converted to number, that number+1 is returned to
|
||||
-- compensate with Lua 1-based indices
|
||||
-- * otherwise, the part is returned URL-escaped
|
||||
local function decodepart(part)
|
||||
local n = tonumber(part)
|
||||
return n and (n+1) or urlunescape(part)
|
||||
end
|
||||
|
||||
|
||||
-- a reference points to a particular node of a particular schema in the store
|
||||
local ref_mt = {}
|
||||
ref_mt.__index = ref_mt
|
||||
|
||||
function ref_mt:child(items)
|
||||
if not (items and items[1]) then return self end
|
||||
local schema = self:resolve()
|
||||
for _, node in ipairs(items) do
|
||||
schema = assert(schema[decodepart(node)])
|
||||
end
|
||||
return setmetatable({ store=self.store, schema=schema }, ref_mt)
|
||||
end
|
||||
|
||||
function ref_mt:resolve()
|
||||
local schema = self.schema
|
||||
|
||||
-- resolve references
|
||||
while schema['$ref'] do
|
||||
-- ok, this is a ref, but what kind of ref?!?
|
||||
local ctx = self.store:ctx(schema)
|
||||
local ref = url.parse(ctx.base.id):resolve(schema['$ref'])
|
||||
local fragment = ref.fragment
|
||||
|
||||
-- get the target schema
|
||||
ref.fragment = nil
|
||||
schema = self.store:fetch(tostring(ref:normalize()))
|
||||
|
||||
-- no fragment? just retrun the root
|
||||
if not fragment then
|
||||
return schema
|
||||
end
|
||||
|
||||
-- maybe the fragment is a id alias
|
||||
local by_id = self.store:ctx(ctx.base).map[fragment]
|
||||
if by_id then
|
||||
schema = by_id
|
||||
else
|
||||
-- maybe not after all, walk the schema
|
||||
-- TODO: notrmalize path (if there is people mean enough to put '.' or
|
||||
-- '..' components)
|
||||
for part in fragment:gmatch('[^/]+') do
|
||||
part = decodepart(part)
|
||||
local new = schema[part]
|
||||
if not new then
|
||||
error(string.format('reference not found: %s#%s (at %q)',
|
||||
ref, fragment, part))
|
||||
end
|
||||
schema = new
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return schema
|
||||
end
|
||||
|
||||
|
||||
-- a store manage all currently required schemas
|
||||
-- it is not exposed directly
|
||||
local store_mt = {}
|
||||
store_mt.__index = store_mt
|
||||
|
||||
function store_mt:ref(schema)
|
||||
return setmetatable({
|
||||
store = self,
|
||||
schema = schema,
|
||||
}, ref_mt)
|
||||
end
|
||||
|
||||
-- store of additional metadata by schema table part, this is to avoid
|
||||
-- modifying schema tables themselves. For now, we have
|
||||
--
|
||||
-- * `base`: refers to the base schema (e.g. for a nested subschema to find
|
||||
-- its parent schema
|
||||
-- * `map`: only for "root" schemas, maps indetifiers to subschemas
|
||||
function store_mt:ctx(t)
|
||||
local c = self.ctx_store[t]
|
||||
if not c then
|
||||
c = {}
|
||||
self.ctx_store[t] = c
|
||||
end
|
||||
return c
|
||||
end
|
||||
|
||||
function store_mt:fetch(uri)
|
||||
local schema = self.schemas[uri]
|
||||
if schema then return schema end
|
||||
|
||||
-- schema not yet known
|
||||
schema = self.resolver(uri)
|
||||
if not schema then
|
||||
error('faild to fetch schema for: ' .. uri)
|
||||
end
|
||||
if not schema.id then
|
||||
schema.id = uri
|
||||
end
|
||||
self:insert(schema)
|
||||
return schema
|
||||
end
|
||||
|
||||
|
||||
-- functions used to walk a schema
|
||||
local function is_schema(path)
|
||||
local n = #path
|
||||
local parent, grandparent = path[n], path[n-1]
|
||||
|
||||
return n == 0 or -- root node
|
||||
parent == 'additionalItems' or
|
||||
parent == 'additionalProperties' or
|
||||
parent == 'items' or
|
||||
parent == 'not' or
|
||||
(type(parent) == 'number' and (
|
||||
grandparent == 'items' or
|
||||
grandparent == 'allOf' or
|
||||
grandparent == 'anyOf' or
|
||||
grandparent == 'oneOf'
|
||||
)) or
|
||||
grandparent == 'properties' or
|
||||
grandparent == 'patternProperties' or
|
||||
grandparent == 'definitions' or
|
||||
grandparent == 'dependencies'
|
||||
end
|
||||
|
||||
function store_mt:insert(schema)
|
||||
local id = url.parse(assert(schema.id, 'id is required'))
|
||||
assert(noe(id.fragment), 'schema ids should not have fragments')
|
||||
schema.id = tostring(id:normalize())
|
||||
self.schemas[schema.id] = schema
|
||||
local base_id = id
|
||||
|
||||
-- walk the schema to collect the ids and populate the base field in context
|
||||
local map = {}
|
||||
|
||||
local function walk(s, p)
|
||||
local id = s.id
|
||||
if id and s ~= schema and is_schema(p) then
|
||||
-- there is an id, but it is not over: we have 2 different cases (!)
|
||||
-- 1. the id is a fragment: it is some kind of an internal alias
|
||||
-- 2. the id is an url (relative or absolute): resolve it using the
|
||||
-- current base and use that as a new base.
|
||||
if id:sub(1,1) == '#' then
|
||||
-- fragment (case 1)
|
||||
map[id.fragment] = self:ref(s)
|
||||
else
|
||||
-- relative url (case 2)
|
||||
-- FIXME: I'm sure it's broken bacasue resolution scopes could be
|
||||
-- nested... but at the same time, who the hell would do this and it
|
||||
-- passes the tests so ¯\_(ツ)_/¯
|
||||
local resolved = base_id:resolve(id)
|
||||
assert(noe(resolved.fragment), 'fragment in relative id')
|
||||
s.id = tostring(resolved:normalize())
|
||||
return self:insert(s)
|
||||
end
|
||||
end
|
||||
|
||||
self:ctx(s).base = schema
|
||||
for k, v in pairs(s) do
|
||||
if type(v) == 'table' and
|
||||
(type(k) == 'number' or (
|
||||
k ~= 'enum' and
|
||||
k:sub(1,1) ~= '_'
|
||||
))
|
||||
then
|
||||
table.insert(p, k)
|
||||
walk(v, p)
|
||||
table.remove(p)
|
||||
end
|
||||
end
|
||||
end
|
||||
walk(schema, {})
|
||||
self:ctx(schema).map = map
|
||||
return self:ref(schema)
|
||||
end
|
||||
|
||||
local function new(schema, resolver)
|
||||
local self = setmetatable({
|
||||
ctx_store = {}, -- used to store metadata aobut schema parts
|
||||
schemas = {},
|
||||
resolver = resolver or default_resolver,
|
||||
}, store_mt)
|
||||
|
||||
schema.id = schema.id or 'root:'
|
||||
return self:insert(schema)
|
||||
end
|
||||
|
||||
return {
|
||||
new = new,
|
||||
}
|
||||
+1369
File diff suppressed because it is too large
Load Diff
+258
@@ -0,0 +1,258 @@
|
||||
local uv = require 'luv'
|
||||
local openssl = require 'openssl'
|
||||
local ssl, bio, x509, pkey, csr, version = openssl.ssl, openssl.bio, openssl.x509, openssl.pkey, openssl.csr, openssl.version
|
||||
|
||||
local bit = require 'bit'
|
||||
local print = print
|
||||
-- support
|
||||
local M = {}
|
||||
|
||||
local function load (path)
|
||||
local f = io.open(path, 'rb')
|
||||
if f then
|
||||
local c = f:read '*a'
|
||||
f:close()
|
||||
return c
|
||||
end
|
||||
end
|
||||
|
||||
function M.new_ctx (params)
|
||||
params = params or {}
|
||||
local protocol = params.protocol or 'SSLv3_client'
|
||||
local ctx = ssl.ctx_new(protocol, params.ciphers)
|
||||
|
||||
local xkey, xcert = nil, nil
|
||||
local keydata = params.keydata or params.key and load(params.key)
|
||||
local certdata = params.certdata or params.certificate and load(params.certificate)
|
||||
|
||||
if certdata then
|
||||
xcert = assert(x509.read(certdata))
|
||||
end
|
||||
|
||||
if keydata then
|
||||
if type(params.password) == 'nil' then
|
||||
xkey = assert(pkey.read(keydata, true, 'pem'))
|
||||
elseif type(params.password) == 'string' then
|
||||
xkey = assert(pkey.read(keydata, true, 'pem', params.password))
|
||||
elseif type(params.password) == 'function' then
|
||||
local p = assert(params.password())
|
||||
xkey = assert(pkey.read(keydata, true, 'pem', p))
|
||||
end
|
||||
assert(ctx:use(xkey, xcert))
|
||||
end
|
||||
|
||||
if params.cafile or params.capath then
|
||||
ctx:verify_locations(params.cafile, params.capath)
|
||||
end
|
||||
|
||||
if params.cadata then
|
||||
local certstore = nil
|
||||
local _, _, opensslv = version(true)
|
||||
if opensslv > 0x10002000 then
|
||||
certstore = assert(x509.store:new())
|
||||
local cert = assert(x509.read(params.cadata))
|
||||
assert(certstore:add(cert))
|
||||
end
|
||||
ctx:cert_store(certstore)
|
||||
end
|
||||
|
||||
local unpack = unpack or table.unpack
|
||||
if params.verify then
|
||||
ctx:verify_mode(params.verify)
|
||||
end
|
||||
if params.options and #params.options > 0 then
|
||||
local args = {}
|
||||
for i = 1, #params.options do
|
||||
table.insert(arg, params.options[i])
|
||||
end
|
||||
ctx:options(ssl.none)
|
||||
end
|
||||
|
||||
if params.verifyext then
|
||||
ctx:set_cert_verify(params.verifyext)
|
||||
end
|
||||
if params.dhparam then
|
||||
ctx:set_tmp('dh', params.dhparam)
|
||||
end
|
||||
if params.curve then
|
||||
ctx:set_tmp('ecdh', params.curve)
|
||||
end
|
||||
return ctx
|
||||
end
|
||||
|
||||
local S = {}
|
||||
S.__index = {
|
||||
handshake = function (self, connected_cb)
|
||||
if not self.connecting then
|
||||
uv.read_start(self.socket, function (err, chunk)
|
||||
if err then
|
||||
print('ERR', err)
|
||||
self:onerror(err)
|
||||
end
|
||||
if chunk then
|
||||
self.inp:write(chunk)
|
||||
self:handshake(connected_cb)
|
||||
else
|
||||
self:close()
|
||||
end
|
||||
end)
|
||||
|
||||
self.connecting = true
|
||||
end
|
||||
if not self.connected then
|
||||
local ret, err = self.ssl:handshake()
|
||||
if ret == nil then
|
||||
if self.onerror then
|
||||
self:onerror()
|
||||
elseif self.onclose then
|
||||
self:onclose()
|
||||
else
|
||||
self:close()
|
||||
end
|
||||
else
|
||||
local i, o = self.out:pending()
|
||||
if i > 0 then
|
||||
--client handshake
|
||||
uv.write(self.socket, self.out:read(), function ()
|
||||
self:handshake(connected_cb)
|
||||
end)
|
||||
return
|
||||
end
|
||||
if ret == false then
|
||||
return
|
||||
end
|
||||
|
||||
self.connected = true
|
||||
uv.read_stop(self.socket)
|
||||
uv.read_start(self.socket, function (err, chunk)
|
||||
if err then
|
||||
print('ERR', err)
|
||||
self:onerror()
|
||||
end
|
||||
if chunk then
|
||||
local ret, err = self.inp:write(chunk)
|
||||
if ret == nil then
|
||||
if self.onerror then
|
||||
self.onerror(self)
|
||||
elseif self.onend then
|
||||
self.onend(self)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
while self.connected and self.inp:pending()>0 do
|
||||
if o > 0 then
|
||||
assert(false, 'never here')
|
||||
end
|
||||
local ret, msg = self.ssl:read()
|
||||
if ret then
|
||||
self:ondata(ret)
|
||||
end
|
||||
end
|
||||
else
|
||||
self:close()
|
||||
end
|
||||
end)
|
||||
connected_cb(self)
|
||||
end
|
||||
|
||||
return self.connected
|
||||
end
|
||||
end,
|
||||
shutdown = function (self, callback)
|
||||
if not self.shutdown then
|
||||
self.ssl:shutdown()
|
||||
self.socket:shutdown()
|
||||
if callback then
|
||||
callback(self)
|
||||
end
|
||||
self.shutdown = true
|
||||
end
|
||||
end,
|
||||
close = function (self)
|
||||
if self.connected then
|
||||
if self.onclose then
|
||||
self.onclose(self)
|
||||
end
|
||||
self:shutdown()
|
||||
if self.ssl then
|
||||
self.ssl:shutdown()
|
||||
end
|
||||
self.ssl = nil
|
||||
if self.inp then
|
||||
self.inp:close()
|
||||
end
|
||||
if self.out then
|
||||
self.out:close()
|
||||
end
|
||||
|
||||
self.out, self.inp = nil, nil
|
||||
uv.close(self.socket)
|
||||
self.connected = nil
|
||||
self.socket = nil
|
||||
end
|
||||
end,
|
||||
write = function (self, data, cb)
|
||||
if not self.ssl then
|
||||
return
|
||||
end
|
||||
local ret, err = self.ssl:write(data)
|
||||
if ret == nil then
|
||||
if self.onerror then
|
||||
self.onerror(self)
|
||||
elseif self.onend then
|
||||
self.onend(self)
|
||||
end
|
||||
return
|
||||
end
|
||||
local i, o = self.out:pending()
|
||||
if i > 0 then
|
||||
uv.write(self.socket, self.out:read(), cb)
|
||||
end
|
||||
if o > 0 then
|
||||
assert(false, 'never here')
|
||||
end
|
||||
end,
|
||||
|
||||
}
|
||||
|
||||
function M.new_ssl (ctx, socket, server)
|
||||
local s = {}
|
||||
s.inp, s.out = bio.mem(8192), bio.mem(8192)
|
||||
s.socket = socket
|
||||
s.mode = server and server or false
|
||||
s.ssl = ctx:ssl(s.inp, s.out, s.mode)
|
||||
uv.tcp_nodelay(socket, true)
|
||||
|
||||
setmetatable(s, S)
|
||||
return s
|
||||
end
|
||||
|
||||
function M.connect (host, port, ctx, connected_cb)
|
||||
if type(ctx) == 'table' then
|
||||
ctx = ssl.new_ctx(ctx)
|
||||
end
|
||||
local socket = uv.new_tcp()
|
||||
local scli = M.new_ssl(ctx, socket)
|
||||
|
||||
uv.tcp_connect(socket, host, port, function (self, err)
|
||||
if err then
|
||||
print('ERROR', err)
|
||||
else
|
||||
print('SCLI', scli)
|
||||
scli:handshake(function (self)
|
||||
if connected_cb then
|
||||
connected_cb(self)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
return scli
|
||||
end
|
||||
|
||||
function M.error ()
|
||||
return openssl.error(true)
|
||||
end
|
||||
|
||||
return M
|
||||
Executable
+451
@@ -0,0 +1,451 @@
|
||||
-- neturl.lua - a robust url parser and builder
|
||||
--
|
||||
-- Bertrand Mansion, 2011-2013; License MIT
|
||||
-- @module neturl
|
||||
-- @alias M
|
||||
|
||||
local M = {}
|
||||
M.version = "0.9.0"
|
||||
|
||||
--- url options
|
||||
-- separator is set to `&` by default but could be anything like `&amp;` or `;`
|
||||
-- @todo Add an option to limit the size of the argument table
|
||||
M.options = {
|
||||
separator = '&'
|
||||
}
|
||||
|
||||
--- list of known and common scheme ports
|
||||
-- as documented in <a href="http://www.iana.org/assignments/uri-schemes.html">IANA URI scheme list</a>
|
||||
M.services = {
|
||||
acap = 674,
|
||||
cap = 1026,
|
||||
dict = 2628,
|
||||
ftp = 21,
|
||||
gopher = 70,
|
||||
http = 80,
|
||||
https = 443,
|
||||
iax = 4569,
|
||||
icap = 1344,
|
||||
imap = 143,
|
||||
ipp = 631,
|
||||
ldap = 389,
|
||||
mtqp = 1038,
|
||||
mupdate = 3905,
|
||||
news = 2009,
|
||||
nfs = 2049,
|
||||
nntp = 119,
|
||||
rtsp = 554,
|
||||
sip = 5060,
|
||||
snmp = 161,
|
||||
telnet = 23,
|
||||
tftp = 69,
|
||||
vemmi = 575,
|
||||
afs = 1483,
|
||||
jms = 5673,
|
||||
rsync = 873,
|
||||
prospero = 191,
|
||||
videotex = 516
|
||||
}
|
||||
|
||||
local legal = {
|
||||
["-"] = true, ["_"] = true, ["."] = true, ["!"] = true,
|
||||
["~"] = true, ["*"] = true, ["'"] = true, ["("] = true,
|
||||
[")"] = true, [":"] = true, ["@"] = true, ["&"] = true,
|
||||
["="] = true, ["+"] = true, ["$"] = true, [","] = true,
|
||||
[";"] = true -- can be used for parameters in path
|
||||
}
|
||||
|
||||
local function decode(str, path)
|
||||
local str = str
|
||||
if not path then
|
||||
str = str:gsub('+', ' ')
|
||||
end
|
||||
return (str:gsub("%%(%x%x)", function(c)
|
||||
return string.char(tonumber(c, 16))
|
||||
end))
|
||||
end
|
||||
|
||||
local function encode(str)
|
||||
return (str:gsub("([^A-Za-z0-9%_%.%-%~])", function(v)
|
||||
return string.upper(string.format("%%%02x", string.byte(v)))
|
||||
end))
|
||||
end
|
||||
|
||||
-- for query values, prefer + instead of %20 for spaces
|
||||
local function encodeValue(str)
|
||||
local str = encode(str)
|
||||
return str:gsub('%%20', '+')
|
||||
end
|
||||
|
||||
local function encodeSegment(s)
|
||||
local legalEncode = function(c)
|
||||
if legal[c] then
|
||||
return c
|
||||
end
|
||||
return encode(c)
|
||||
end
|
||||
return s:gsub('([^a-zA-Z0-9])', legalEncode)
|
||||
end
|
||||
|
||||
local function concat(s, u)
|
||||
return s .. u:build()
|
||||
end
|
||||
|
||||
--- builds the url
|
||||
-- @return a string representing the built url
|
||||
function M:build()
|
||||
local url = ''
|
||||
if self.path then
|
||||
local path = self.path
|
||||
path:gsub("([^/]+)", function (s) return encodeSegment(s) end)
|
||||
url = url .. tostring(path)
|
||||
end
|
||||
if self.query then
|
||||
local qstring = tostring(self.query)
|
||||
if qstring ~= "" then
|
||||
url = url .. '?' .. qstring
|
||||
end
|
||||
end
|
||||
if self.host then
|
||||
local authority = self.host
|
||||
if self.port and self.scheme and M.services[self.scheme] ~= self.port then
|
||||
authority = authority .. ':' .. self.port
|
||||
end
|
||||
local userinfo
|
||||
if self.user and self.user ~= "" then
|
||||
userinfo = self.user
|
||||
if self.password then
|
||||
userinfo = userinfo .. ':' .. self.password
|
||||
end
|
||||
end
|
||||
if userinfo and userinfo ~= "" then
|
||||
authority = userinfo .. '@' .. authority
|
||||
end
|
||||
if authority then
|
||||
if url ~= "" then
|
||||
url = '//' .. authority .. '/' .. url:gsub('^/+', '')
|
||||
else
|
||||
url = '//' .. authority
|
||||
end
|
||||
end
|
||||
end
|
||||
if self.scheme then
|
||||
url = self.scheme .. ':' .. url
|
||||
end
|
||||
if self.fragment then
|
||||
url = url .. '#' .. self.fragment
|
||||
end
|
||||
return url
|
||||
end
|
||||
|
||||
--- builds the querystring
|
||||
-- @param tab The key/value parameters
|
||||
-- @param sep The separator to use (optional)
|
||||
-- @param key The parent key if the value is multi-dimensional (optional)
|
||||
-- @return a string representing the built querystring
|
||||
function M.buildQuery(tab, sep, key)
|
||||
local query = {}
|
||||
if not sep then
|
||||
sep = M.options.separator or '&'
|
||||
end
|
||||
local keys = {}
|
||||
for k in pairs(tab) do
|
||||
keys[#keys+1] = k
|
||||
end
|
||||
table.sort(keys)
|
||||
for _,name in ipairs(keys) do
|
||||
local value = tab[name]
|
||||
name = encode(tostring(name))
|
||||
if key then
|
||||
name = string.format('%s[%s]', tostring(key), tostring(name))
|
||||
end
|
||||
if type(value) == 'table' then
|
||||
query[#query+1] = M.buildQuery(value, sep, name)
|
||||
else
|
||||
local value = encodeValue(tostring(value))
|
||||
if value ~= "" then
|
||||
query[#query+1] = string.format('%s=%s', name, value)
|
||||
else
|
||||
query[#query+1] = name
|
||||
end
|
||||
end
|
||||
end
|
||||
return table.concat(query, sep)
|
||||
end
|
||||
|
||||
--- Parses the querystring to a table
|
||||
-- This function can parse multidimensional pairs and is mostly compatible
|
||||
-- with PHP usage of brackets in key names like ?param[key]=value
|
||||
-- @param str The querystring to parse
|
||||
-- @param sep The separator between key/value pairs, defaults to `&`
|
||||
-- @todo limit the max number of parameters with M.options.max_parameters
|
||||
-- @return a table representing the query key/value pairs
|
||||
function M.parseQuery(str, sep)
|
||||
if not sep then
|
||||
sep = M.options.separator or '&'
|
||||
end
|
||||
|
||||
local values = {}
|
||||
for key,val in str:gmatch(string.format('([^%q=]+)(=*[^%q=]*)', sep, sep)) do
|
||||
local key = decode(key)
|
||||
local keys = {}
|
||||
key = key:gsub('%[([^%]]*)%]', function(v)
|
||||
-- extract keys between balanced brackets
|
||||
if string.find(v, "^-?%d+$") then
|
||||
v = tonumber(v)
|
||||
else
|
||||
v = decode(v)
|
||||
end
|
||||
table.insert(keys, v)
|
||||
return "="
|
||||
end)
|
||||
key = key:gsub('=+.*$', "")
|
||||
key = key:gsub('%s', "_") -- remove spaces in parameter name
|
||||
val = val:gsub('^=+', "")
|
||||
|
||||
if not values[key] then
|
||||
values[key] = {}
|
||||
end
|
||||
if #keys > 0 and type(values[key]) ~= 'table' then
|
||||
values[key] = {}
|
||||
elseif #keys == 0 and type(values[key]) == 'table' then
|
||||
values[key] = decode(val)
|
||||
end
|
||||
|
||||
local t = values[key]
|
||||
for i,k in ipairs(keys) do
|
||||
if type(t) ~= 'table' then
|
||||
t = {}
|
||||
end
|
||||
if k == "" then
|
||||
k = #t+1
|
||||
end
|
||||
if not t[k] then
|
||||
t[k] = {}
|
||||
end
|
||||
if i == #keys then
|
||||
t[k] = decode(val)
|
||||
end
|
||||
t = t[k]
|
||||
end
|
||||
end
|
||||
setmetatable(values, { __tostring = M.buildQuery })
|
||||
return values
|
||||
end
|
||||
|
||||
--- set the url query
|
||||
-- @param query Can be a string to parse or a table of key/value pairs
|
||||
-- @return a table representing the query key/value pairs
|
||||
function M:setQuery(query)
|
||||
local query = query
|
||||
if type(query) == 'table' then
|
||||
query = M.buildQuery(query)
|
||||
end
|
||||
self.query = M.parseQuery(query)
|
||||
return query
|
||||
end
|
||||
|
||||
--- set the authority part of the url
|
||||
-- The authority is parsed to find the user, password, port and host if available.
|
||||
-- @param authority The string representing the authority
|
||||
-- @return a string with what remains after the authority was parsed
|
||||
function M:setAuthority(authority)
|
||||
self.authority = authority
|
||||
self.port = nil
|
||||
self.host = nil
|
||||
self.userinfo = nil
|
||||
self.user = nil
|
||||
self.password = nil
|
||||
|
||||
authority = authority:gsub('^([^@]*)@', function(v)
|
||||
self.userinfo = v
|
||||
return ''
|
||||
end)
|
||||
authority = authority:gsub("^%[[^%]]+%]", function(v)
|
||||
-- ipv6
|
||||
self.host = v
|
||||
return ''
|
||||
end)
|
||||
authority = authority:gsub(':([^:]*)$', function(v)
|
||||
self.port = tonumber(v)
|
||||
return ''
|
||||
end)
|
||||
if authority ~= '' and not self.host then
|
||||
self.host = authority:lower()
|
||||
end
|
||||
if self.userinfo then
|
||||
local userinfo = self.userinfo
|
||||
userinfo = userinfo:gsub(':([^:]*)$', function(v)
|
||||
self.password = v
|
||||
return ''
|
||||
end)
|
||||
self.user = userinfo
|
||||
end
|
||||
return authority
|
||||
end
|
||||
|
||||
--- Parse the url into the designated parts.
|
||||
-- Depending on the url, the following parts can be available:
|
||||
-- scheme, userinfo, user, password, authority, host, port, path,
|
||||
-- query, fragment
|
||||
-- @param url Url string
|
||||
-- @return a table with the different parts and a few other functions
|
||||
function M.parse(url)
|
||||
local comp = {}
|
||||
M.setAuthority(comp, "")
|
||||
M.setQuery(comp, "")
|
||||
|
||||
local url = tostring(url or '')
|
||||
url = url:gsub('#(.*)$', function(v)
|
||||
comp.fragment = v
|
||||
return ''
|
||||
end)
|
||||
url =url:gsub('^([%w][%w%+%-%.]*)%:', function(v)
|
||||
comp.scheme = v:lower()
|
||||
return ''
|
||||
end)
|
||||
url = url:gsub('%?(.*)', function(v)
|
||||
M.setQuery(comp, v)
|
||||
return ''
|
||||
end)
|
||||
url = url:gsub('^//([^/]*)', function(v)
|
||||
M.setAuthority(comp, v)
|
||||
return ''
|
||||
end)
|
||||
comp.path = decode(url, true)
|
||||
|
||||
setmetatable(comp, {
|
||||
__index = M,
|
||||
__concat = concat,
|
||||
__tostring = M.build}
|
||||
)
|
||||
return comp
|
||||
end
|
||||
|
||||
--- removes dots and slashes in urls when possible
|
||||
-- This function will also remove multiple slashes
|
||||
-- @param path The string representing the path to clean
|
||||
-- @return a string of the path without unnecessary dots and segments
|
||||
function M.removeDotSegments(path)
|
||||
local fields = {}
|
||||
if string.len(path) == 0 then
|
||||
return ""
|
||||
end
|
||||
local startslash = false
|
||||
local endslash = false
|
||||
if string.sub(path, 1, 1) == "/" then
|
||||
startslash = true
|
||||
end
|
||||
if (string.len(path) > 1 or startslash == false) and string.sub(path, -1) == "/" then
|
||||
endslash = true
|
||||
end
|
||||
|
||||
path:gsub('[^/]+', function(c) table.insert(fields, c) end)
|
||||
|
||||
local new = {}
|
||||
local j = 0
|
||||
|
||||
for i,c in ipairs(fields) do
|
||||
if c == '..' then
|
||||
if j > 0 then
|
||||
j = j - 1
|
||||
end
|
||||
elseif c ~= "." then
|
||||
j = j + 1
|
||||
new[j] = c
|
||||
end
|
||||
end
|
||||
local ret = ""
|
||||
if #new > 0 and j > 0 then
|
||||
ret = table.concat(new, '/', 1, j)
|
||||
else
|
||||
ret = ""
|
||||
end
|
||||
if startslash then
|
||||
ret = '/'..ret
|
||||
end
|
||||
if endslash then
|
||||
ret = ret..'/'
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
local function absolutePath(base_path, relative_path)
|
||||
if string.sub(relative_path, 1, 1) == "/" then
|
||||
return '/' .. string.gsub(relative_path, '^[%./]+', '')
|
||||
end
|
||||
local path = base_path
|
||||
if relative_path ~= "" then
|
||||
path = '/'..path:gsub("[^/]*$", "")
|
||||
end
|
||||
path = path .. relative_path
|
||||
path = path:gsub("([^/]*%./)", function (s)
|
||||
if s ~= "./" then return s else return "" end
|
||||
end)
|
||||
path = string.gsub(path, "/%.$", "/")
|
||||
local reduced
|
||||
while reduced ~= path do
|
||||
reduced = path
|
||||
path = string.gsub(reduced, "([^/]*/%.%./)", function (s)
|
||||
if s ~= "../../" then return "" else return s end
|
||||
end)
|
||||
end
|
||||
path = string.gsub(path, "([^/]*/%.%.?)$", function (s)
|
||||
if s ~= "../.." then return "" else return s end
|
||||
end)
|
||||
local reduced
|
||||
while reduced ~= path do
|
||||
reduced = path
|
||||
path = string.gsub(reduced, '^/?%.%./', '')
|
||||
end
|
||||
return '/' .. path
|
||||
end
|
||||
|
||||
--- builds a new url by using the one given as parameter and resolving paths
|
||||
-- @param other A string or a table representing a url
|
||||
-- @return a new url table
|
||||
function M:resolve(other)
|
||||
if type(self) == "string" then
|
||||
self = M.parse(self)
|
||||
end
|
||||
if type(other) == "string" then
|
||||
other = M.parse(other)
|
||||
end
|
||||
if other.scheme then
|
||||
return other
|
||||
else
|
||||
other.scheme = self.scheme
|
||||
if not other.authority or other.authority == "" then
|
||||
other:setAuthority(self.authority)
|
||||
if not other.path or other.path == "" then
|
||||
other.path = self.path
|
||||
local query = other.query
|
||||
if not query or not next(query) then
|
||||
other.query = self.query
|
||||
end
|
||||
else
|
||||
other.path = absolutePath(self.path, other.path)
|
||||
end
|
||||
end
|
||||
return other
|
||||
end
|
||||
end
|
||||
|
||||
--- normalize a url path following some common normalization rules
|
||||
-- described on <a href="http://en.wikipedia.org/wiki/URL_normalization">The URL normalization page of Wikipedia</a>
|
||||
-- @return the normalized path
|
||||
function M:normalize()
|
||||
if type(self) == 'string' then
|
||||
self = M.parse(self)
|
||||
end
|
||||
if self.path then
|
||||
local path = self.path
|
||||
path = absolutePath(path, "")
|
||||
-- normalize multiple slashes
|
||||
path = string.gsub(path, "//+", "/")
|
||||
self.path = path
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
return M
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
require("yaci")
|
||||
local lfs = require("lfs")
|
||||
local time = require("time")
|
||||
|
||||
CReader = newclass("CReader")
|
||||
|
||||
function CReader:init(is_debug, is_block, timer_ms, is_skip_empty)
|
||||
self.is_debug = false
|
||||
self.is_block = true
|
||||
self.timer_ms = 50
|
||||
self.is_skip_empty = true
|
||||
self.co = nil
|
||||
self.file_handle = nil
|
||||
self.last_size = 0
|
||||
self.last_modification = 0
|
||||
|
||||
if type(is_debug) == "boolean" then
|
||||
self.is_debug = is_debug
|
||||
end
|
||||
if type(is_block) == "boolean" then
|
||||
self.is_block = is_block
|
||||
end
|
||||
if type(timer_ms) == "number" then
|
||||
self.timer_ms = timer_ms
|
||||
end
|
||||
if type(is_skip_empty) == "boolean" then
|
||||
self.is_skip_empty = is_skip_empty
|
||||
end
|
||||
end
|
||||
|
||||
function CReader:print(...)
|
||||
if self.is_debug then
|
||||
print(...)
|
||||
end
|
||||
end
|
||||
|
||||
function CReader:is_modification()
|
||||
local attr, err = lfs.attributes(self.file_path)
|
||||
if not attr then
|
||||
return false, err
|
||||
elseif self.last_size > attr["size"] then
|
||||
return true, "truncate"
|
||||
elseif self.last_size < attr["size"] then
|
||||
return true, "change size"
|
||||
elseif self.last_modification ~= attr["modification"] then
|
||||
return true, "updated"
|
||||
end
|
||||
return false, "nothing"
|
||||
end
|
||||
|
||||
function CReader:get_modification()
|
||||
local attr, err = lfs.attributes(self.file_path)
|
||||
if not attr then
|
||||
return nil, err
|
||||
end
|
||||
return attr["modification"]
|
||||
end
|
||||
|
||||
function CReader:check_modification(await, is_close)
|
||||
repeat
|
||||
if is_close() then
|
||||
return false, "closed"
|
||||
end
|
||||
if not self.is_follow then
|
||||
return true, "not follow"
|
||||
end
|
||||
local is_mod, msg = self:is_modification()
|
||||
if msg == "nothing" then
|
||||
if self.is_block then
|
||||
return true, msg
|
||||
else
|
||||
await(self.timer_ms)
|
||||
end
|
||||
elseif msg == "updated" then
|
||||
self.last_modification = self:get_modification()
|
||||
elseif msg == "truncate" and not self:open() then
|
||||
return false, "error"
|
||||
elseif is_mod == false then
|
||||
return false, msg
|
||||
else
|
||||
return true, "changed"
|
||||
end
|
||||
until not self.is_block
|
||||
end
|
||||
|
||||
function CReader:get_size()
|
||||
local attr, err = lfs.attributes(self.file_path)
|
||||
if not attr then
|
||||
return nil, err
|
||||
end
|
||||
return attr["size"]
|
||||
end
|
||||
|
||||
function CReader:open(file_path, file_op, is_follow, limit, step)
|
||||
if self.file_handle and io.type(self.file_handle) == "file" then
|
||||
self:print("Reader already initialized and it will be closed")
|
||||
self.file_handle:close()
|
||||
else
|
||||
self.file_path = nil
|
||||
self.file_op = nil
|
||||
self.is_follow = false
|
||||
self.limit = -1
|
||||
self.step = 1
|
||||
|
||||
if type(file_path) == "string" then
|
||||
self.file_path = file_path
|
||||
end
|
||||
if type(file_op) == "string" then
|
||||
self.file_op = file_op
|
||||
end
|
||||
if type(is_follow) == "boolean" then
|
||||
self.is_follow = is_follow
|
||||
end
|
||||
if type(limit) == "number" and limit >= 0 then
|
||||
self.limit = limit
|
||||
end
|
||||
if type(step) == "number" and step > 0 then
|
||||
self.step = step
|
||||
end
|
||||
end
|
||||
|
||||
if self.limit ~= -1 and self.step > self.limit then
|
||||
return false, "limit should be greater than step"
|
||||
end
|
||||
if self.limit < -1 then
|
||||
return false, "limit should be greater or equal than -1"
|
||||
end
|
||||
if self.step <= 0 then
|
||||
return false, "step should be greater than 0"
|
||||
end
|
||||
|
||||
if self.file_path then
|
||||
self.dir, self.file_name, self.file_ext =
|
||||
string.match(self.file_path, "(.-)([^\\/]-%.?([^%.\\/]*))$")
|
||||
else
|
||||
return false, "File path doesn't set"
|
||||
end
|
||||
|
||||
self.file_handle = io.open(self.file_path, "rb")
|
||||
if not self.file_handle then
|
||||
return false, "Can't open file: " .. file_path
|
||||
end
|
||||
|
||||
if self.file_op == "tail" and self.is_follow then
|
||||
self.file_handle:seek("end")
|
||||
elseif self.file_op == "tail" and not self.is_follow then
|
||||
self.file_handle:seek("set")
|
||||
if self.limit ~= -1 then
|
||||
local ofsets = { 0 }
|
||||
for line in self.file_handle:lines() do
|
||||
local is_empty = string.match(line, "([^\r\n]*)")
|
||||
if is_empty and #is_empty > 0 then
|
||||
table.insert(ofsets, self.file_handle:seek())
|
||||
end
|
||||
end
|
||||
if #ofsets <= self.limit then
|
||||
self.file_handle:seek("set")
|
||||
else
|
||||
self.file_handle:seek("set", ofsets[#ofsets - self.limit])
|
||||
end
|
||||
end
|
||||
elseif self.file_op == "head" then
|
||||
self.file_handle:seek("set")
|
||||
else
|
||||
self:print("File operation doesn't set")
|
||||
end
|
||||
|
||||
self.last_size = self.file_handle:seek()
|
||||
self.last_modification = self:get_modification()
|
||||
return true
|
||||
end
|
||||
|
||||
function CReader:get_lines(await, is_close)
|
||||
local nline = 0
|
||||
local lines = {}
|
||||
while self.limit == -1 or nline < self.limit do
|
||||
local res, msg = self:check_modification(await, is_close)
|
||||
if not res then
|
||||
self:print("Check modifications failed: ", msg)
|
||||
return res, msg
|
||||
end
|
||||
|
||||
local date_marker = os.date("%Y-%m-%d %H:%M:%S ", os.time())
|
||||
for line in self.file_handle:lines() do
|
||||
self:print("<" .. date_marker .. "> " .. line)
|
||||
local is_empty = string.match(line, "([^\r\n]*)")
|
||||
if is_empty and #is_empty > 0 then
|
||||
nline = nline + 1
|
||||
table.insert(lines, line)
|
||||
if #lines == self.step or nline == self.limit then
|
||||
self.last_size = self.file_handle:seek()
|
||||
coroutine.yield(lines)
|
||||
lines = {}
|
||||
end
|
||||
if nline == self.limit then
|
||||
self:print("Lines limit exceeded: ", nline, self.limit)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not self.is_follow or nline == self.limit or not self.is_block then
|
||||
if #lines ~= 0 then
|
||||
self.last_size = self.file_handle:seek()
|
||||
coroutine.yield(lines)
|
||||
lines = {}
|
||||
end
|
||||
self:print("Get lines function was skipped: ",
|
||||
self.is_follow, nline, self.limit, self.is_block)
|
||||
break
|
||||
end
|
||||
end
|
||||
self:print("Get lines function was done")
|
||||
return true
|
||||
end
|
||||
|
||||
function CReader:close()
|
||||
if io.type(self.file_handle) == "file" then
|
||||
self.file_handle:close()
|
||||
end
|
||||
self.co = nil
|
||||
self.file_handle = nil
|
||||
self.last_size = 0
|
||||
self.last_modification = 0
|
||||
end
|
||||
|
||||
function CReader:get_sync_func(await, is_close)
|
||||
if type(await) ~= "function" then
|
||||
self:print("Replace await function to default: ", await)
|
||||
await = time.sleep
|
||||
end
|
||||
if type(is_close) ~= "function" then
|
||||
self:print("Replace is_close function to default: ", is_close)
|
||||
is_close = function()
|
||||
if not self.file_handle then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
return await, is_close
|
||||
end
|
||||
|
||||
function CReader:read_line(await, is_close)
|
||||
await, is_close = self:get_sync_func(await, is_close)
|
||||
if not self.co or type(self.co) ~= "thread" then
|
||||
self.co = coroutine.create(self.get_lines)
|
||||
end
|
||||
while coroutine.status(self.co) == "suspended" do
|
||||
local _, lines = coroutine.resume(self.co, self, await, is_close)
|
||||
if coroutine.status(self.co) ~= "dead" then
|
||||
return lines
|
||||
end
|
||||
end
|
||||
self:print("Read line function was done")
|
||||
if self.is_block then
|
||||
self:close()
|
||||
else
|
||||
self.co = nil
|
||||
end
|
||||
end
|
||||
|
||||
function CReader:read_line_cb(callback, await, is_close)
|
||||
if type(callback) ~= "function" then
|
||||
return false, "Callback doesn't set"
|
||||
end
|
||||
await, is_close = self:get_sync_func(await, is_close)
|
||||
if not self.co or type(self.co) ~= "thread" then
|
||||
self.co = coroutine.create(self.get_lines)
|
||||
end
|
||||
while coroutine.status(self.co) == "suspended" do
|
||||
local _, lines = coroutine.resume(self.co, self, await, is_close)
|
||||
if coroutine.status(self.co) ~= "dead" then
|
||||
callback(lines)
|
||||
end
|
||||
end
|
||||
self:print("Read line function callback was done")
|
||||
if self.is_block then
|
||||
self:close()
|
||||
else
|
||||
self.co = nil
|
||||
end
|
||||
return true
|
||||
end
|
||||
@@ -0,0 +1,95 @@
|
||||
require'reader'
|
||||
|
||||
__api = {}
|
||||
|
||||
function __api.await(delay)
|
||||
local time = require("time")
|
||||
time.sleep(delay / 1000.)
|
||||
end
|
||||
|
||||
function __api.is_close()
|
||||
return false
|
||||
end
|
||||
|
||||
--[[ Use cases:
|
||||
reader:open("access.log", "tail") -- default values
|
||||
reader:open("access.log", "tail", true, 15) -- set limit
|
||||
reader:open("access.log", "tail", true, -1, 3) -- set step
|
||||
reader:open("access.log", "tail", false, 15) -- set limit without follow
|
||||
reader:open("access.log", "tail", false, 20, 3) -- set limit and step without follow
|
||||
reader:open("access.log", "head") -- default values
|
||||
reader:open("access.log", "head", true, 15) -- set limit
|
||||
reader:open("access.log", "head", true, -1, 3) -- set step
|
||||
reader:open("access.log", "head", false, 15) -- set limit without follow
|
||||
reader:open("access.log", "head", false, 20, 3) -- set limit and step without follow
|
||||
]]
|
||||
|
||||
function reader_init()
|
||||
local reader = CReader()
|
||||
if not reader:open("access.log", "tail", true, -1, 1) then
|
||||
print("Failed to initialize file descriptor")
|
||||
os.exit(0)
|
||||
end
|
||||
return reader
|
||||
end
|
||||
|
||||
function print_lines(lines)
|
||||
local date_marker = os.date("%Y-%m-%d %H:%M:%S ", os.time())
|
||||
date_marker = "<" .. date_marker .. "> "
|
||||
if type(lines) == "table" then
|
||||
for n = 1, #lines do
|
||||
local prefix = date_marker .. tostring(n) .. ": "
|
||||
print(prefix .. lines[n])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[ Inline using:
|
||||
local reader = reader_init()
|
||||
while not __api.is_close() do
|
||||
local lines = reader:read_line(__api.await, __api.is_close)
|
||||
if type(lines) ~= "table" then
|
||||
break
|
||||
end
|
||||
print_lines(lines)
|
||||
end
|
||||
]]
|
||||
|
||||
--[[ Inline using with callback:
|
||||
local reader = reader_init()
|
||||
reader:read_line_cb(print_lines, __api.await, __api.is_close)
|
||||
]]
|
||||
|
||||
--[[ Thread using in box with callback:
|
||||
local thread = require'thread'
|
||||
local rth = thread.new(function (reader_init, print_lines, await, is_close)
|
||||
require'reader'
|
||||
local reader = reader_init()
|
||||
reader:read_line_cb(print_lines, await, is_close)
|
||||
end, reader_init, print_lines, __api.await, __api.is_close)
|
||||
rth:join()
|
||||
]]
|
||||
|
||||
--[[ Thread using queue out of the box with callback:
|
||||
local thread = require'thread'
|
||||
local q = thread.queue(1)
|
||||
local rth = thread.new(function (reader_init, q, await, is_close)
|
||||
require'reader'
|
||||
local reader = reader_init()
|
||||
reader:read_line_cb(function (lines) q:push(lines) end, await, is_close)
|
||||
q:push()
|
||||
end, reader_init, q, __api.await, __api.is_close)
|
||||
|
||||
while true do
|
||||
local _, v = q:shift()
|
||||
if type(v) ~= "table" then
|
||||
break
|
||||
end
|
||||
print_lines(v)
|
||||
end
|
||||
q:free()
|
||||
print("wait thread")
|
||||
rth:join()
|
||||
]]
|
||||
|
||||
print("done")
|
||||
@@ -0,0 +1,317 @@
|
||||
local openssl = require'openssl'
|
||||
local socket = require'socket'
|
||||
local ssl,pkey,x509,version = openssl.ssl,openssl.pkey,openssl.x509,openssl.version
|
||||
|
||||
local M = {}
|
||||
|
||||
local function load(path)
|
||||
local f = assert(io.open(path,'r'))
|
||||
if f then
|
||||
local c = f:read('*all')
|
||||
f:close()
|
||||
return c
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function M.newcontext(params)
|
||||
local protocol = params.protocol and string.upper(string.sub(params.protocol,1,3))
|
||||
..string.sub(params.protocol,4,-1) or 'TLSv1_2'
|
||||
local ctx = ssl.ctx_new(protocol,params.ciphers)
|
||||
local xkey = nil
|
||||
local keydata = params.keydata or params.key and load(params.key)
|
||||
local certdata = params.certdata or params.certificate and load(params.certificate)
|
||||
|
||||
if keydata then
|
||||
if (type(params.password)=='nil') then
|
||||
xkey = assert(pkey.read(keydata,true,'pem'))
|
||||
elseif (type(params.password)=='string') then
|
||||
xkey = assert(pkey.read(keydata,true,'pem',params.password))
|
||||
elseif (type(params.password)=='function') then
|
||||
local p = assert(params.password())
|
||||
xkey = assert(pkey.read(keydata,true,'pem',p))
|
||||
end
|
||||
|
||||
assert(xkey)
|
||||
|
||||
local xcert = nil
|
||||
if certdata then
|
||||
xcert = assert(x509.read(certdata))
|
||||
end
|
||||
assert(ctx:use(xkey,xcert))
|
||||
end
|
||||
|
||||
if params.cafile or params.capath then
|
||||
ctx:verify_locations(params.cafile,params.capath)
|
||||
end
|
||||
|
||||
if params.cadata then
|
||||
local certstore = nil
|
||||
local _, _, opensslv = version(true)
|
||||
if opensslv > 0x10002000 then
|
||||
certstore = assert(x509.store:new())
|
||||
local cert = assert(x509.read(params.cadata))
|
||||
assert(certstore:add(cert))
|
||||
end
|
||||
ctx:cert_store(certstore)
|
||||
end
|
||||
|
||||
unpack = unpack or table.unpack
|
||||
if params.verify then
|
||||
if type(params.verify) ~= "table" then
|
||||
params.verify = {params.verify}
|
||||
end
|
||||
|
||||
local luasec_flags = {
|
||||
["none"] = "none",
|
||||
["peer"] = "peer",
|
||||
["client_once"] = "once",
|
||||
["fail_if_no_peer_cert"] = "fail"
|
||||
}
|
||||
|
||||
local verify = 0
|
||||
for i,v in ipairs(params.verify) do
|
||||
verify = verify + (ssl[luasec_flags[v] or v] or v)
|
||||
end
|
||||
ctx:verify_mode(verify)
|
||||
end
|
||||
if params.options then
|
||||
if type(params.options) ~= "table" then
|
||||
params.options = {params.options}
|
||||
end
|
||||
ctx:options(unpack(params.options))
|
||||
end
|
||||
if params.verifyext then
|
||||
for k,v in pairs(params.verifyext) do
|
||||
params.verifyext[k] = string.gsub(v,'lsec_','')
|
||||
end
|
||||
ctx:set_cert_verify(params.verifyext)
|
||||
end
|
||||
if params.dhparam then
|
||||
ctx:set_tmp('dh',params.dhparam)
|
||||
end
|
||||
if params.curve then
|
||||
ctx:set_tmp('ecdh',params.curve)
|
||||
end
|
||||
local t = {}
|
||||
t.ctx = ctx
|
||||
t.mode = params.mode
|
||||
t.params = params
|
||||
return t
|
||||
end
|
||||
----------------------------------------------------------
|
||||
local S = {}
|
||||
|
||||
S.__index = {
|
||||
dohandshake = function(self)
|
||||
local ret,msg
|
||||
|
||||
socket.select({self.ssl}, {self.ssl}, self.timeout)
|
||||
|
||||
ret,msg = self.ssl:handshake()
|
||||
while not ret do
|
||||
if (msg=='want_read' or msg=='want_write') then
|
||||
ret,msg = self.ssl:handshake()
|
||||
else
|
||||
return ret,msg
|
||||
end
|
||||
end
|
||||
|
||||
if ret then
|
||||
self._bbf = assert(openssl.bio.filter('buffer'))
|
||||
self._sbf = assert(openssl.bio.filter('ssl',self.ssl,'noclose'))
|
||||
|
||||
self.bio = assert(self._bbf:push(self._sbf))
|
||||
else
|
||||
msg = msg and string.gsub(msg,'_','') or msg
|
||||
end
|
||||
return ret,msg
|
||||
end,
|
||||
getpeercertificate = function(self)
|
||||
self.peer,self.peerchain = self.ssl:peer()
|
||||
return self.peer
|
||||
end,
|
||||
getpeerverification = function(self)
|
||||
local r, t = self.ssl:getpeerverification()
|
||||
if not r then
|
||||
local tt = {}
|
||||
for i,err in pairs(t) do
|
||||
tt[i] = {}
|
||||
tt[i][1] = string.format('error=%d string=%s depth=%s',err.error,err.error_string,err.error_depth)
|
||||
end
|
||||
return r,tt
|
||||
end
|
||||
return r
|
||||
end,
|
||||
getfd = function(self)
|
||||
local fd = self.ssl:getfd()
|
||||
return fd
|
||||
end,
|
||||
getpeerchain = function(self)
|
||||
self.peer,self.peerchain = self.ssl:peer()
|
||||
local chains = {}
|
||||
--[[
|
||||
print(self.peerchain,#self.peerchain)
|
||||
for i=1,#self.peerchain do
|
||||
table.insert(chains,self.peerchain:get(i-1))
|
||||
end
|
||||
--]]
|
||||
if (self.peerchain) then
|
||||
chains = self.peerchain:totable()
|
||||
end
|
||||
return chains
|
||||
end,
|
||||
close = function(self)
|
||||
if self.ssl then
|
||||
self.ssl:shutdown()
|
||||
self.ssl = nil
|
||||
end
|
||||
end,
|
||||
send = function(self,msg,i,j)
|
||||
local m = msg
|
||||
if i then
|
||||
j = j or -1
|
||||
m = string.sub(msg,i,j)
|
||||
end
|
||||
return self.bio:write(m) and self.bio:flush()
|
||||
end,
|
||||
receive = function(self,fmt,prev,force)
|
||||
if type(fmt) == 'number' then
|
||||
local buff = prev and {prev} or {''}
|
||||
local buffsize = string.len(buff[1])
|
||||
local s = nil
|
||||
local len = fmt
|
||||
|
||||
while buffsize < len do
|
||||
if not force then
|
||||
local r, m = socket.select({self.ssl}, nil, self.timeout)
|
||||
if #r == 0 then
|
||||
return nil, 'timeout', buff
|
||||
end
|
||||
end
|
||||
|
||||
s = self.bio:read(len - buffsize)
|
||||
if s == nil then
|
||||
return nil, 'closed', table.concat(buff)
|
||||
elseif type(s) == "string" and s ~= '' then
|
||||
table.insert(buff, s)
|
||||
buffsize = buffsize + string.len(s)
|
||||
end
|
||||
end
|
||||
|
||||
buff = table.concat(buff)
|
||||
if buffsize > len then
|
||||
s = string.sub(buff, len + 1, -1)
|
||||
buff = string.sub(buff, 1, len)
|
||||
end
|
||||
|
||||
return buff
|
||||
end
|
||||
|
||||
fmt = fmt and string.sub(fmt, 1, 2) or '*l'
|
||||
if (fmt == '*l') then
|
||||
local s = nil
|
||||
local buff = prev or ''
|
||||
local _, _, p1, p2 = string.find(buff, '(.-)\r\n(.*)')
|
||||
|
||||
while not p1 do
|
||||
local r, m = socket.select({self.ssl}, nil, self.timeout)
|
||||
if #r == 0 then
|
||||
return nil, 'timeout', buff
|
||||
end
|
||||
|
||||
s = self.bio:gets(1024)
|
||||
if s == nil then
|
||||
return nil, 'closed', buff
|
||||
elseif type(s) == "string" and s ~= '' then
|
||||
buff = buff .. s
|
||||
end
|
||||
_, _, p1, p2 = string.find(buff, '(.-)\r\n(.*)')
|
||||
end
|
||||
|
||||
return p1
|
||||
end
|
||||
end,
|
||||
sni = function(self,arg)
|
||||
if type(arg) =='string' then
|
||||
self.ssl:set('hostname',arg)
|
||||
elseif type(arg)=='table' then
|
||||
local t = {}
|
||||
for k,v in pairs(arg) do
|
||||
t[k] = v.ctx
|
||||
end
|
||||
self.ssl:ctx():set_servername_callback(t)
|
||||
end
|
||||
end,
|
||||
|
||||
settimeout = function(self,n,b)
|
||||
self.timeout = n
|
||||
end,
|
||||
info = function(self,field)
|
||||
--[[
|
||||
algbits
|
||||
authentication
|
||||
bits
|
||||
cipher
|
||||
compression
|
||||
encryption
|
||||
export
|
||||
key
|
||||
mac
|
||||
protocol
|
||||
--]]
|
||||
local cc = self.ssl:current_cipher()
|
||||
if cc then
|
||||
local info = {
|
||||
bits = cc.bits,
|
||||
algbits = cc.algbits,
|
||||
protocol = cc.protocol
|
||||
}
|
||||
if cc.description then
|
||||
info.cipher, info.protocol, info.key,
|
||||
info.authentication, info.encryption, info.mac =
|
||||
string.match(cc.description,
|
||||
"^(%S+)%s+(%S+)%s+Kx=(%S+)%s+Au=(%S+)%s+Enc=(%S+)%s+Mac=(%S+)")
|
||||
info.export = (string.match(cc.description, "%sexport%s*$") ~= nil)
|
||||
end
|
||||
self.compression = self.ssl:current_compression()
|
||||
if field then
|
||||
return info[field]
|
||||
end
|
||||
return info
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
|
||||
function M.wrap(sock, cfg)
|
||||
local ctx, msg
|
||||
if type(cfg) == "table" and not cfg.ctx then
|
||||
ctx, msg = M.newcontext(cfg)
|
||||
if not ctx then return nil, msg end
|
||||
else
|
||||
ctx = cfg
|
||||
end
|
||||
|
||||
local s, msg = ctx.ctx:ssl(sock:getfd())
|
||||
if s then
|
||||
if(ctx.mode=='server') then
|
||||
s:set_accept_state()
|
||||
else
|
||||
s:set_connect_state()
|
||||
end
|
||||
local t = {}
|
||||
t.ssl = s
|
||||
t.socket = sock
|
||||
t.timeout = type(cfg)=='table' and cfg.timeout or nil
|
||||
setmetatable(t,S)
|
||||
return t
|
||||
end
|
||||
return nil, msg
|
||||
end
|
||||
|
||||
function M.loadcertificate(pem)
|
||||
return openssl.x509.read(pem,'pem')
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,103 @@
|
||||
local ffi = require "ffi"
|
||||
local ffi_new = ffi.new
|
||||
local ffi_str = ffi.string
|
||||
local ffi_load = ffi.load
|
||||
local ffi_cdef = ffi.cdef
|
||||
local C = ffi.C
|
||||
local OSX = ffi.os == "OSX"
|
||||
local pcall = pcall
|
||||
local assert = assert
|
||||
local tonumber = tonumber
|
||||
local setmetatable = setmetatable
|
||||
|
||||
ffi_cdef[[
|
||||
typedef unsigned char uuid_t[16];
|
||||
typedef long time_t;
|
||||
typedef struct timeval {
|
||||
time_t tv_sec;
|
||||
time_t tv_usec;
|
||||
} timeval;
|
||||
void uuid_generate(uuid_t out);
|
||||
void uuid_generate_random(uuid_t out);
|
||||
void uuid_generate_time(uuid_t out);
|
||||
int uuid_generate_time_safe(uuid_t out);
|
||||
int uuid_parse(const char *in, uuid_t uu);
|
||||
void uuid_unparse(const uuid_t uu, char *out);
|
||||
int uuid_type(const uuid_t uu);
|
||||
int uuid_variant(const uuid_t uu);
|
||||
time_t uuid_time(const uuid_t uu, struct timeval *ret_tv);
|
||||
]]
|
||||
|
||||
local function L(n)
|
||||
local ok, lib = pcall(ffi_load, n)
|
||||
if ok then return lib end
|
||||
ok, lib = pcall(ffi_load, n .. '.so.1')
|
||||
assert(ok, lib)
|
||||
return lib
|
||||
end
|
||||
|
||||
local lib = OSX and C or L "uuid"
|
||||
local uid = ffi_new "uuid_t"
|
||||
local tvl = ffi_new "timeval"
|
||||
local buf = ffi_new("char[?]", 36)
|
||||
|
||||
local uuid = {}
|
||||
local mt = {}
|
||||
|
||||
local function unparse(id)
|
||||
lib.uuid_unparse(id, buf)
|
||||
return ffi_str(buf, 36)
|
||||
end
|
||||
|
||||
local function parse(id)
|
||||
return lib.uuid_parse(id, uid) == 0 and uid or nil
|
||||
end
|
||||
|
||||
function uuid.generate()
|
||||
lib.uuid_generate(uid)
|
||||
return unparse(uid)
|
||||
end
|
||||
|
||||
function uuid.generate_random()
|
||||
lib.uuid_generate_random(uid)
|
||||
return unparse(uid)
|
||||
end
|
||||
|
||||
function uuid.generate_time()
|
||||
lib.uuid_generate_time(uid)
|
||||
return unparse(uid)
|
||||
end
|
||||
|
||||
function uuid.generate_time_safe()
|
||||
assert(not OSX, "uuid_generate_time_safe is not supported on OS X.")
|
||||
local safe = lib.uuid_generate_time_safe(uid) == 0
|
||||
return unparse(uid), safe
|
||||
end
|
||||
|
||||
function uuid.type(id)
|
||||
assert(not OSX, "uuid_type is not supported on OS X.")
|
||||
local parsed = parse(id)
|
||||
return parsed and lib.uuid_type(parsed)
|
||||
end
|
||||
|
||||
function uuid.variant(id)
|
||||
assert(not OSX, "uuid_variant is not supported on OS X.")
|
||||
local parsed = parse(id)
|
||||
return parsed and lib.uuid_variant(parsed)
|
||||
end
|
||||
|
||||
function uuid.time(id)
|
||||
local parsed = parse(id)
|
||||
if parsed then
|
||||
local secs = lib.uuid_time(parsed, tvl)
|
||||
return tonumber(secs), tonumber(tvl.tv_usec)
|
||||
end
|
||||
end
|
||||
|
||||
function uuid.is_valid(id)
|
||||
return not not parse(id)
|
||||
end
|
||||
|
||||
mt.__call = uuid.generate
|
||||
|
||||
return setmetatable(uuid, mt)
|
||||
@@ -0,0 +1,238 @@
|
||||
-----------------------------------------------------------------------------------
|
||||
-- Yet Another Class Implementation (version 1.2)
|
||||
--
|
||||
-- Julien Patte [julien.patte AT gmail DOT com] - 25 Feb 2007
|
||||
--
|
||||
-- Inspired from code written by Kevin Baca, Sam Lie, Christian Lindig and others
|
||||
-- Thanks to Damian Stewart and Frederic Thomas for their interest and comments
|
||||
-----------------------------------------------------------------------------------
|
||||
|
||||
do -- keep local things inside
|
||||
|
||||
-- associations between an object an its meta-informations
|
||||
-- e.g its class, its "lower" object (if any), ...
|
||||
local metaObj = {}
|
||||
setmetatable(metaObj, {__mode = "k"})
|
||||
|
||||
-----------------------------------------------------------------------------------
|
||||
-- internal function 'duplicate'
|
||||
-- return a shallow copy of table t
|
||||
|
||||
local function duplicate(t)
|
||||
local t2 = {}
|
||||
for k,v in pairs(t) do t2[k] = v end
|
||||
return t2
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------------------------
|
||||
-- internal function 'newInstance'
|
||||
|
||||
local function newInstance(class, ...)
|
||||
|
||||
local function set_fake_gc(class)
|
||||
local tclass = class
|
||||
local meta_gc = getmetatable(tclass) and getmetatable(tclass).__gc or tclass.__gc
|
||||
if _VERSION == 'Lua 5.1' and meta_gc then
|
||||
local destructor = newproxy(true)
|
||||
getmetatable(destructor).__gc = function(self) meta_gc(tclass) end
|
||||
rawset(tclass, destructor, true)
|
||||
end
|
||||
end
|
||||
|
||||
local function makeInstance(class, virtuals)
|
||||
local inst = duplicate(virtuals)
|
||||
local mo = {}
|
||||
setmetatable(mo, {__mode = "v"})
|
||||
mo.obj = inst
|
||||
mo.class = class
|
||||
metaObj[inst] = mo
|
||||
|
||||
if class:super()~=nil then
|
||||
inst.super = makeInstance(class:super(), virtuals)
|
||||
metaObj[inst].super = metaObj[inst.super] -- meta-info about inst
|
||||
metaObj[inst.super].lower = metaObj[inst]
|
||||
else
|
||||
inst.super = {}
|
||||
end
|
||||
|
||||
setmetatable(inst, class.static)
|
||||
set_fake_gc(inst)
|
||||
|
||||
return inst
|
||||
end
|
||||
|
||||
local inst = makeInstance(class, metaObj[class].virtuals)
|
||||
inst:init(...)
|
||||
|
||||
return inst
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------------------------
|
||||
-- internal function 'makeVirtual'
|
||||
|
||||
local function makeVirtual(class, fname)
|
||||
local func = class.static[fname]
|
||||
if func == nil then
|
||||
func = function() error("Attempt to call an undefined abstract method '"..fname.."'") end
|
||||
end
|
||||
metaObj[class].virtuals[fname] = func
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------------------------
|
||||
-- internal function 'trycast'
|
||||
-- try to cast an instance into an instance of one of its super- or subclasses
|
||||
|
||||
local function tryCast(class, inst)
|
||||
local meta = metaObj[inst]
|
||||
if meta.class==class then return inst end -- is it already the right class?
|
||||
|
||||
while meta~=nil do -- search lower in the hierarchy
|
||||
if meta.class==class then return meta.obj end
|
||||
meta = meta.lower
|
||||
end
|
||||
|
||||
meta = metaObj[inst].super -- not found, search through the superclasses
|
||||
while meta~=nil do
|
||||
if meta.class==class then return meta.obj end
|
||||
meta = meta.super
|
||||
end
|
||||
|
||||
return nil -- could not execute casting
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------------------------
|
||||
-- internal function 'secureCast'
|
||||
-- same as trycast but raise an error in case of failure
|
||||
|
||||
local function secureCast(class, inst)
|
||||
local casted = tryCast(class, inst)
|
||||
|
||||
if casted == nil then
|
||||
error("Failed to cast " .. tostring(inst) .. " to a " .. class:name())
|
||||
end
|
||||
|
||||
return casted
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------------------------
|
||||
-- internal function 'classMade'
|
||||
|
||||
local function classMade(class, obj)
|
||||
if metaObj[obj]==nil then return false end -- is this really an object?
|
||||
|
||||
return (tryCast(class,obj) ~= nil) -- check if that class could cast the object
|
||||
end
|
||||
|
||||
|
||||
-----------------------------------------------------------------------------------
|
||||
-- internal function 'subclass'
|
||||
|
||||
local function inst_init_def(inst, ...)
|
||||
inst.super:init()
|
||||
end
|
||||
|
||||
local function inst_newindex(inst,key,value)
|
||||
if inst.super[key] ~= nil then -- First check if this field isn't already
|
||||
-- defined higher in the hierarchy
|
||||
inst.super[key] = value; -- Update the old value
|
||||
else
|
||||
rawset(inst,key,value); -- Create the field
|
||||
end
|
||||
end
|
||||
|
||||
local function subclass(baseClass, name)
|
||||
if type(name)~="string" then name = "Unnamed" end
|
||||
|
||||
local theClass = {}
|
||||
|
||||
-- need to copy everything here because events can't be found through metatables
|
||||
local b = baseClass.static
|
||||
local inst_stuff = { __tostring=b.__tostring, __eq=b.__eq, __add=b.__add, __sub=b.__sub,
|
||||
__mul=b.__mul, __div=b.__div, __mod=b.__mod, __pow=b.__pow, __unm=b.__unm,
|
||||
__len=b.__len, __lt=b.__lt, __le=b.__le, __concat=b.__concat, __call=b.__call, __gc=b.__gc }
|
||||
inst_stuff.init = inst_init_def
|
||||
inst_stuff.__newindex = inst_newindex
|
||||
function inst_stuff.class() return theClass end
|
||||
|
||||
function inst_stuff.__index(inst, key) -- Look for field 'key' in instance 'inst'
|
||||
local res = inst_stuff[key] -- Is it present?
|
||||
if res~=nil then return res end -- Okay, return it
|
||||
|
||||
res = inst.super[key] -- Is it somewhere higher in the hierarchy?
|
||||
|
||||
if type(res)=='function' then
|
||||
return function (inst, ...) -- return closure for the superclass' method
|
||||
return res(inst and inst.super or inst, ...) -- call the superclass' method
|
||||
end
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
|
||||
local class_stuff = { static = inst_stuff, made = classMade, new = newInstance,
|
||||
subclass = subclass, virtual = makeVirtual, cast = secureCast, trycast = tryCast }
|
||||
metaObj[theClass] = { virtuals = duplicate(metaObj[baseClass].virtuals) }
|
||||
|
||||
function class_stuff.name(class) return name end
|
||||
function class_stuff.super(class) return baseClass end
|
||||
function class_stuff.inherits(class, other)
|
||||
return (baseClass==other or baseClass:inherits(other))
|
||||
end
|
||||
|
||||
local function newmethod(class, name, meth)
|
||||
inst_stuff[name] = meth;
|
||||
if metaObj[class].virtuals[name]~=nil then
|
||||
metaObj[class].virtuals[name] = meth
|
||||
end
|
||||
end
|
||||
|
||||
local function tos() return ("class "..name) end
|
||||
setmetatable(theClass, { __newindex = newmethod, __index = class_stuff,
|
||||
__tostring = tos, __call = newInstance } )
|
||||
|
||||
return theClass
|
||||
end
|
||||
|
||||
|
||||
-----------------------------------------------------------------------------------
|
||||
-- The 'Object' class
|
||||
|
||||
Object = {}
|
||||
|
||||
local function obj_newitem() error "May not modify the class 'Object'. Subclass it instead." end
|
||||
local obj_inst_stuff = {}
|
||||
local function obj_free(inst)
|
||||
if inst and inst.free then
|
||||
inst:free()
|
||||
end
|
||||
end
|
||||
function obj_inst_stuff.init(inst,...) end
|
||||
obj_inst_stuff.__index = obj_inst_stuff
|
||||
obj_inst_stuff.__newindex = obj_newitem
|
||||
obj_inst_stuff.__gc = obj_free
|
||||
function obj_inst_stuff.class() return Object end
|
||||
function obj_inst_stuff.__tostring(inst) return ("a "..inst:class():name()) end
|
||||
|
||||
local obj_class_stuff = { static = obj_inst_stuff, made = classMade, new = newInstance,
|
||||
subclass = subclass, cast = secureCast, trycast = tryCast }
|
||||
function obj_class_stuff.name(class) return "Object" end
|
||||
function obj_class_stuff.super(class) return nil end
|
||||
function obj_class_stuff.inherits(class, other) return false end
|
||||
metaObj[Object] = { virtuals={} }
|
||||
|
||||
local function tos() return ("class Object") end
|
||||
setmetatable(Object, { __newindex = obj_newitem, __index = obj_class_stuff,
|
||||
__tostring = tos, __call = newInstance } )
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- function 'newclass'
|
||||
|
||||
function newclass(name, baseClass)
|
||||
baseClass = baseClass or Object
|
||||
return baseClass:subclass(name)
|
||||
end
|
||||
|
||||
end -- 2 global things remain: 'Object' and 'newclass'
|
||||
|
||||
-- end of code
|
||||
Reference in New Issue
Block a user