-- Copyright 2008 Steven Barth <steven@midlink.org> -- 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 = rawget, rawset, unpack 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 -- 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 -- function pcdata(value) return value and tparser.pcdata(tostring(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 function striptags(value) return value and tparser.striptags(tostring(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, "^\-", "\\-") res, _ = string.gsub(res, "^-", "\-") return shellsqescape(value) 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 interator 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.pcdata = pcdata string.striptags = striptags 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" } function ubus(object, method, data) if not _ubus_connection then _ubus_connection = _ubus.connect() assert(_ubus_connection, "Unable to establish ubus connection") end if object and method then if type(data) ~= "table" then data = { } end local rv, err = _ubus_connection:call(object, method, data) return rv, err, ubus_codes[err] 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 modified for Luci -- original version: -- coxpcall 1.13 - Copyright 2005 - Kepler Project (www.keplerproject.org) -- -- Copyright © 2005 Kepler Project. -- Permission is hereby granted, free of charge, to any person obtaining a -- copy of this software and associated documentation files (the "Software"), -- to deal in the Software without restriction, including without limitation -- the rights to use, copy, modify, merge, publish, distribute, sublicense, -- and/or sell copies of the Software, and to permit persons to whom the -- Software is furnished to do so, subject to the following conditions: -- -- The above copyright notice and this permission notice shall be -- included in all copies or substantial portions of the Software. -- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -- OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -- DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE -- OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. local performResume, handleReturnValue local oldpcall, oldxpcall = pcall, xpcall coxpt = {} setmetatable(coxpt, {__mode = "kv"}) -- Identity function for copcall local function copcall_id(trace, ...) return ... end -- values of either the function or the error handler function coxpcall(f, err, ...) local res, co = oldpcall(coroutine.create, f) if not res then local params = {...} local newf = function() return f(unpack(params)) end co = coroutine.create(newf) end local c = coroutine.running() coxpt[co] = coxpt[c] or c or 0 return performResume(err, co, ...) end -- values of the function or the error object function copcall(f, ...) return coxpcall(f, copcall_id, ...) end -- Handle return value of protected call function handleReturnValue(err, co, status, ...) if not status then return false, err(debug.traceback(co, (...)), ...) end if coroutine.status(co) ~= 'suspended' then return true, ... end return performResume(err, co, coroutine.yield(...)) end -- Resume execution of protected function call function performResume(err, co, ...) return handleReturnValue(err, co, coroutine.resume(co, ...)) end