-- Copyright 2008 Steven Barth -- Licensed to the public under the Apache License 2.0. local io = require "io" local math = require "math" local table = require "table" local debug = require "debug" local ldebug = require "luci.debug" local string = require "string" local coroutine = require "coroutine" local tparser = require "luci.template.parser" local json = require "luci.jsonc" local lhttp = require "lucihttp" local _ubus = require "ubus" local _ubus_connection = nil local getmetatable, setmetatable = getmetatable, setmetatable local rawget, rawset, unpack, select = rawget, rawset, unpack, select local tostring, type, assert, error = tostring, type, assert, error local ipairs, pairs, next, loadstring = ipairs, pairs, next, loadstring local require, pcall, xpcall = require, pcall, xpcall local collectgarbage, get_memory_limit = collectgarbage, get_memory_limit module "luci.util" -- -- Pythonic string formatting extension -- getmetatable("").__mod = function(a, b) local ok, res if not b then return a elseif type(b) == "table" then local k, _ for k, _ in pairs(b) do if type(b[k]) == "userdata" then b[k] = tostring(b[k]) end end ok, res = pcall(a.format, a, unpack(b)) if not ok then error(res, 2) end return res else if type(b) == "userdata" then b = tostring(b) end ok, res = pcall(a.format, a, b) if not ok then error(res, 2) end return res end end -- -- Class helper routines -- -- Instantiates a class local function _instantiate(class, ...) local inst = setmetatable({}, {__index = class}) if inst.__init__ then inst:__init__(...) end return inst end -- The class object can be instantiated by calling itself. -- Any class functions or shared parameters can be attached to this object. -- Attaching a table to the class object makes this table shared between -- all instances of this class. For object parameters use the __init__ function. -- Classes can inherit member functions and values from a base class. -- Class can be instantiated by calling them. All parameters will be passed -- to the __init__ function of this class - if such a function exists. -- The __init__ function must be used to set any object parameters that are not shared -- with other objects of this class. Any return values will be ignored. function class(base) return setmetatable({}, { __call = _instantiate, __index = base }) end function instanceof(object, class) local meta = getmetatable(object) while meta and meta.__index do if meta.__index == class then return true end meta = getmetatable(meta.__index) end return false end -- -- Scope manipulation routines -- coxpt = setmetatable({}, { __mode = "kv" }) local tl_meta = { __mode = "k", __index = function(self, key) local t = rawget(self, coxpt[coroutine.running()] or coroutine.running() or 0) return t and t[key] end, __newindex = function(self, key, value) local c = coxpt[coroutine.running()] or coroutine.running() or 0 local r = rawget(self, c) if not r then rawset(self, c, { [key] = value }) else r[key] = value end end } -- the current active coroutine. A thread local store is private a table object -- whose values can't be accessed from outside of the running coroutine. function threadlocal(tbl) return setmetatable(tbl or {}, tl_meta) end -- -- Debugging routines -- function perror(obj) return io.stderr:write(tostring(obj) .. "\n") end function dumptable(t, maxdepth, i, seen) i = i or 0 seen = seen or setmetatable({}, {__mode="k"}) for k,v in pairs(t) do perror(string.rep("\t", i) .. tostring(k) .. "\t" .. tostring(v)) if type(v) == "table" and (not maxdepth or i < maxdepth) then if not seen[v] then seen[v] = true dumptable(v, maxdepth, i+1, seen) else perror(string.rep("\t", i) .. "*** RECURSION ***") end end end end -- -- String and data manipulation routines -- -- compatibility wrapper for xml.pcdata function pcdata(value) local xml = require "luci.xml" perror("luci.util.pcdata() has been replaced by luci.xml.pcdata() - Please update your code.") return xml.pcdata(value) end function urlencode(value) if value ~= nil then local str = tostring(value) return lhttp.urlencode(str, lhttp.ENCODE_IF_NEEDED + lhttp.ENCODE_FULL) or str end return nil end function urldecode(value, decode_plus) if value ~= nil then local flag = decode_plus and lhttp.DECODE_PLUS or 0 local str = tostring(value) return lhttp.urldecode(str, lhttp.DECODE_IF_NEEDED + flag) or str end return nil end -- compatibility wrapper for xml.striptags function striptags(value) local xml = require "luci.xml" perror("luci.util.striptags() has been replaced by luci.xml.striptags() - Please update your code.") return xml.striptags(value) end function shellquote(value) return string.format("'%s'", string.gsub(value or "", "'", "'\\''")) end -- for bash, ash and similar shells single-quoted strings are taken -- literally except for single quotes (which terminate the string) -- (and the exception noted below for dash (-) at the start of a -- command line parameter). function shellsqescape(value) local res res, _ = string.gsub(value, "'", "'\\''") return res end -- bash, ash and other similar shells interpret a dash (-) at the start -- of a command-line parameters as an option indicator regardless of -- whether it is inside a single-quoted string. It must be backlash -- escaped to resolve this. This requires in some funky special-case -- handling. It may actually be a property of the getopt function -- rather than the shell proper. function shellstartsqescape(value) res, _ = string.gsub(value, "^%-", "\\-") return shellsqescape(res) end -- containing the resulting substrings. The optional max parameter specifies -- the number of bytes to process, regardless of the actual length of the given -- string. The optional last parameter, regex, specifies whether the separator -- sequence is interpreted as regular expression. -- pattern as regular expression (optional, default is false) function split(str, pat, max, regex) pat = pat or "\n" max = max or #str local t = {} local c = 1 if #str == 0 then return {""} end if #pat == 0 then return nil end if max == 0 then return str end repeat local s, e = str:find(pat, c, not regex) max = max - 1 if s and max < 0 then t[#t+1] = str:sub(c) else t[#t+1] = str:sub(c, s and s - 1) end c = e and e + 1 or #str + 1 until not s or max < 0 return t end function trim(str) return (str:gsub("^%s*(.-)%s*$", "%1")) end function cmatch(str, pat) local count = 0 for _ in str:gmatch(pat) do count = count + 1 end return count end -- one token per invocation, the tokens are separated by whitespace. If the -- input value is a table, it is transformed into a string first. A nil value -- will result in a valid iterator which aborts with the first invocation. function imatch(v) if type(v) == "table" then local k = nil return function() k = next(v, k) return v[k] end elseif type(v) == "number" or type(v) == "boolean" then local x = true return function() if x then x = false return tostring(v) end end elseif type(v) == "userdata" or type(v) == "string" then return tostring(v):gmatch("%S+") end return function() end end -- value or 0 if the unit is unknown. Upper- or lower case is irrelevant. -- Recognized units are: -- o "y" - one year (60*60*24*366) -- o "m" - one month (60*60*24*31) -- o "w" - one week (60*60*24*7) -- o "d" - one day (60*60*24) -- o "h" - one hour (60*60) -- o "min" - one minute (60) -- o "kb" - one kilobyte (1024) -- o "mb" - one megabyte (1024*1024) -- o "gb" - one gigabyte (1024*1024*1024) -- o "kib" - one si kilobyte (1000) -- o "mib" - one si megabyte (1000*1000) -- o "gib" - one si gigabyte (1000*1000*1000) function parse_units(ustr) local val = 0 -- unit map local map = { -- date stuff y = 60 * 60 * 24 * 366, m = 60 * 60 * 24 * 31, w = 60 * 60 * 24 * 7, d = 60 * 60 * 24, h = 60 * 60, min = 60, -- storage sizes kb = 1024, mb = 1024 * 1024, gb = 1024 * 1024 * 1024, -- storage sizes (si) kib = 1000, mib = 1000 * 1000, gib = 1000 * 1000 * 1000 } -- parse input string for spec in ustr:lower():gmatch("[0-9%.]+[a-zA-Z]*") do local num = spec:gsub("[^0-9%.]+$","") local spn = spec:gsub("^[0-9%.]+", "") if map[spn] or map[spn:sub(1,1)] then val = val + num * ( map[spn] or map[spn:sub(1,1)] ) else val = val + num end end return val end -- also register functions above in the central string class for convenience string.split = split string.trim = trim string.cmatch = cmatch string.parse_units = parse_units function append(src, ...) for i, a in ipairs({...}) do if type(a) == "table" then for j, v in ipairs(a) do src[#src+1] = v end else src[#src+1] = a end end return src end function combine(...) return append({}, ...) end function contains(table, value) for k, v in pairs(table) do if value == v then return k end end return false end -- Both table are - in fact - merged together. function update(t, updates) for k, v in pairs(updates) do t[k] = v end end function keys(t) local keys = { } if t then for k, _ in kspairs(t) do keys[#keys+1] = k end end return keys end function clone(object, deep) local copy = {} for k, v in pairs(object) do if deep and type(v) == "table" then v = clone(v, deep) end copy[k] = v end return setmetatable(copy, getmetatable(object)) end -- Serialize the contents of a table value. function _serialize_table(t, seen) assert(not seen[t], "Recursion detected.") seen[t] = true local data = "" local idata = "" local ilen = 0 for k, v in pairs(t) do if type(k) ~= "number" or k < 1 or math.floor(k) ~= k or ( k - #t ) > 3 then k = serialize_data(k, seen) v = serialize_data(v, seen) data = data .. ( #data > 0 and ", " or "" ) .. '[' .. k .. '] = ' .. v elseif k > ilen then ilen = k end end for i = 1, ilen do local v = serialize_data(t[i], seen) idata = idata .. ( #idata > 0 and ", " or "" ) .. v end return idata .. ( #data > 0 and #idata > 0 and ", " or "" ) .. data end -- with loadstring(). function serialize_data(val, seen) seen = seen or setmetatable({}, {__mode="k"}) if val == nil then return "nil" elseif type(val) == "number" then return val elseif type(val) == "string" then return "%q" % val elseif type(val) == "boolean" then return val and "true" or "false" elseif type(val) == "function" then return "loadstring(%q)" % get_bytecode(val) elseif type(val) == "table" then return "{ " .. _serialize_table(val, seen) .. " }" else return '"[unhandled data type:' .. type(val) .. ']"' end end function restore_data(str) return loadstring("return " .. str)() end -- -- Byte code manipulation routines -- -- will be stripped before it is returned. function get_bytecode(val) local code if type(val) == "function" then code = string.dump(val) else code = string.dump( loadstring( "return " .. serialize_data(val) ) ) end return code -- and strip_bytecode(code) end -- numbers and debugging numbers will be discarded. Original version by -- Peter Cawley (http://lua-users.org/lists/lua-l/2008-02/msg01158.html) function strip_bytecode(code) local version, format, endian, int, size, ins, num, lnum = code:byte(5, 12) local subint if endian == 1 then subint = function(code, i, l) local val = 0 for n = l, 1, -1 do val = val * 256 + code:byte(i + n - 1) end return val, i + l end else subint = function(code, i, l) local val = 0 for n = 1, l, 1 do val = val * 256 + code:byte(i + n - 1) end return val, i + l end end local function strip_function(code) local count, offset = subint(code, 1, size) local stripped = { string.rep("\0", size) } local dirty = offset + count offset = offset + count + int * 2 + 4 offset = offset + int + subint(code, offset, int) * ins count, offset = subint(code, offset, int) for n = 1, count do local t t, offset = subint(code, offset, 1) if t == 1 then offset = offset + 1 elseif t == 4 then offset = offset + size + subint(code, offset, size) elseif t == 3 then offset = offset + num elseif t == 254 or t == 9 then offset = offset + lnum end end count, offset = subint(code, offset, int) stripped[#stripped+1] = code:sub(dirty, offset - 1) for n = 1, count do local proto, off = strip_function(code:sub(offset, -1)) stripped[#stripped+1] = proto offset = offset + off - 1 end offset = offset + subint(code, offset, int) * int + int count, offset = subint(code, offset, int) for n = 1, count do offset = offset + subint(code, offset, size) + size + int * 2 end count, offset = subint(code, offset, int) for n = 1, count do offset = offset + subint(code, offset, size) + size end stripped[#stripped+1] = string.rep("\0", int * 3) return table.concat(stripped), offset end return code:sub(1,12) .. strip_function(code:sub(13,-1)) end -- -- Sorting iterator functions -- function _sortiter( t, f ) local keys = { } local k, v for k, v in pairs(t) do keys[#keys+1] = k end local _pos = 0 table.sort( keys, f ) return function() _pos = _pos + 1 if _pos <= #keys then return keys[_pos], t[keys[_pos]], _pos end end end -- the provided callback function. function spairs(t,f) return _sortiter( t, f ) end -- The table pairs are sorted by key. function kspairs(t) return _sortiter( t ) end -- The table pairs are sorted by value. function vspairs(t) return _sortiter( t, function (a,b) return t[a] < t[b] end ) end -- -- System utility functions -- function bigendian() return string.byte(string.dump(function() end), 7) == 0 end function exec(command) local pp = io.popen(command) local data = pp:read("*a") pp:close() return data end function execi(command) local pp = io.popen(command) return pp and function() local line = pp:read() if not line then pp:close() end return line end end -- Deprecated function execl(command) local pp = io.popen(command) local line = "" local data = {} while true do line = pp:read() if (line == nil) then break end data[#data+1] = line end pp:close() return data end local ubus_codes = { "INVALID_COMMAND", "INVALID_ARGUMENT", "METHOD_NOT_FOUND", "NOT_FOUND", "NO_DATA", "PERMISSION_DENIED", "TIMEOUT", "NOT_SUPPORTED", "UNKNOWN_ERROR", "CONNECTION_FAILED" } local function ubus_return(...) if select('#', ...) == 2 then local rv, err = select(1, ...), select(2, ...) if rv == nil and type(err) == "number" then return nil, err, ubus_codes[err] end end return ... end function ubus(object, method, data, path, timeout) if not _ubus_connection then _ubus_connection = _ubus.connect(path, timeout) assert(_ubus_connection, "Unable to establish ubus connection") end if object and method then if type(data) ~= "table" then data = { } end return ubus_return(_ubus_connection:call(object, method, data)) elseif object then return _ubus_connection:signatures(object) else return _ubus_connection:objects() end end function serialize_json(x, cb) local js = json.stringify(x) if type(cb) == "function" then cb(js) else return js end end function libpath() return require "nixio.fs".dirname(ldebug.__file__) end function checklib(fullpathexe, wantedlib) local fs = require "nixio.fs" local haveldd = fs.access('/usr/bin/ldd') local haveexe = fs.access(fullpathexe) if not haveldd or not haveexe then return false end local libs = exec(string.format("/usr/bin/ldd %s", shellquote(fullpathexe))) if not libs then return false end for k, v in ipairs(split(libs)) do if v:find(wantedlib) then return true end end return false end ------------------------------------------------------------------------------- -- Coroutine safe xpcall and pcall versions -- -- Encapsulates the protected calls with a coroutine based loop, so errors can -- be dealed without the usual Lua 5.x pcall/xpcall issues with coroutines -- yielding inside the call to pcall or xpcall. -- -- Authors: Roberto Ierusalimschy and Andre Carregal -- Contributors: Thomas Harning Jr., Ignacio BurgueƱo, Fabio Mascarenhas -- -- Copyright 2005 - Kepler Project -- -- $Id: coxpcall.lua,v 1.13 2008/05/19 19:20:02 mascarenhas Exp $ ------------------------------------------------------------------------------- ------------------------------------------------------------------------------- -- Implements xpcall with coroutines ------------------------------------------------------------------------------- local coromap = setmetatable({}, { __mode = "k" }) local function handleReturnValue(err, co, status, ...) if not status then return false, err(debug.traceback(co, (...)), ...) end if coroutine.status(co) == 'suspended' then return performResume(err, co, coroutine.yield(...)) else return true, ... end end function performResume(err, co, ...) return handleReturnValue(err, co, coroutine.resume(co, ...)) end local function id(trace, ...) return trace end function coxpcall(f, err, ...) local current = coroutine.running() if not current then if err == id then return pcall(f, ...) else if select("#", ...) > 0 then local oldf, params = f, { ... } f = function() return oldf(unpack(params)) end end return xpcall(f, err) end else local res, co = pcall(coroutine.create, f) if not res then local newf = function(...) return f(...) end co = coroutine.create(newf) end coromap[co] = current coxpt[co] = coxpt[current] or current or 0 return performResume(err, co, ...) end end function copcall(f, ...) return coxpcall(f, id, ...) end