* luci/libs: add preliminary uvl code
authorJo-Philipp Wich <jow@openwrt.org>
Thu, 14 Aug 2008 20:24:31 +0000 (20:24 +0000)
committerJo-Philipp Wich <jow@openwrt.org>
Thu, 14 Aug 2008 20:24:31 +0000 (20:24 +0000)
libs/uvl/Makefile [new file with mode: 0644]
libs/uvl/luasrc/uvl.lua [new file with mode: 0644]
libs/uvl/luasrc/uvl/datatypes.lua [new file with mode: 0644]

diff --git a/libs/uvl/Makefile b/libs/uvl/Makefile
new file mode 100644 (file)
index 0000000..81a96f6
--- /dev/null
@@ -0,0 +1,2 @@
+include ../../build/config.mk
+include ../../build/module.mk
\ No newline at end of file
diff --git a/libs/uvl/luasrc/uvl.lua b/libs/uvl/luasrc/uvl.lua
new file mode 100644 (file)
index 0000000..4894d30
--- /dev/null
@@ -0,0 +1,403 @@
+--[[
+
+UCI Validation Layer - Main Library
+(c) 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+(c) 2008 Steven Barth <steven@midlink.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+
+]]--
+
+module( "luci.uvl", package.seeall )
+
+require("luci.fs")
+require("luci.util")
+require("luci.model.uci")
+require("luci.uvl.datatypes")
+
+TYPE_SECTION  = 0x01
+TYPE_VARIABLE = 0x02
+TYPE_ENUM     = 0x03
+
+
+local default_schemedir = "/etc/scheme"
+local function _assert( condition, fmt, ... )
+       if not condition then
+               return assert( nil, string.format( fmt, ... ) )
+       else
+               return condition
+       end
+end
+
+UVL = luci.util.class()
+
+function UVL.__init__( self, schemedir )
+
+       self.schemedir  = schemedir or default_schemedir
+       self.packages   = { }
+       self.uci                = luci.model.uci
+       self.datatypes  = luci.uvl.datatypes
+end
+
+--- Validate given configuration.
+-- @param config       Name of the configuration to validate
+-- @param scheme       Scheme to validate against (optional)
+-- @return                     Boolean indicating weather the given config validates
+-- @return                     String containing the reason for errors (if any)
+function UVL.validate( self, config, scheme )
+
+       if not scheme then
+               return false, "No scheme found"
+       end
+
+       for k, v in pairs( config ) do
+               local ok, err = self:validate_section( config, k, scheme )
+
+               if not ok then
+                       return ok, err
+               end
+       end
+
+       return true, nil
+end
+
+--- Validate given section of given configuration.
+-- @param config       Name of the configuration to validate
+-- @param section      Key of the section to validate
+-- @param scheme       Scheme to validate against
+-- @return                     Boolean indicating weather the given config validates
+-- @return                     String containing the reason for errors (if any)
+function UVL.validate_section( self, config, section, scheme )
+
+       if not scheme then
+               return false, "No scheme found"
+       end
+
+       for k, v in pairs( config[section] ) do
+               local ok, err = self:validate_option( config, section, k, scheme )
+
+               if not ok then
+                       return ok, err
+               end
+       end
+
+       return true, nil
+end
+
+--- Validate given option within section of given configuration.
+-- @param config       Name of the configuration to validate
+-- @param section      Key of the section to validate
+-- @param option       Name of the option to validate
+-- @param scheme       Scheme to validate against
+-- @return                     Boolean indicating weather the given config validates
+-- @return                     String containing the reason for errors (if any)
+function UVL.validate_option( self, config, section, option, scheme )
+
+       if type(config) == "string" then
+               config = { ["variables"] = { [section] = { [option] = config } } }
+       end
+
+       if not scheme then
+               return false, "No scheme found"
+       end
+
+       local sv = scheme.variables[section]
+       if not sv then return false, "Requested section not found in scheme" end
+
+       sv = sv[option]
+       if not sv then return false, "Requested option not found in scheme" end
+
+       if not ( config[section] and config[section][option] ) and sv.required then
+               return false, "Mandatory variable doesn't have a value"
+       end
+
+       if sv.type then
+               if self.datatypes[sv.type] then
+                       if not self.datatypes[sv.type]( config[section][option] ) then
+                               return false, "Value of given option doesn't validate"
+                       end
+               else
+                       return false, "Unknown datatype '" .. sv.type .. "' encountered"
+               end
+       end
+
+       return true, nil
+end
+
+--- Find all parts of given scheme and construct validation tree
+-- @param scheme       Name of the scheme to parse
+-- @return                     Parsed scheme
+function UVL.read_scheme( self, scheme )
+       local schemes = { }
+
+       for i, file in ipairs( luci.fs.glob(self.schemedir .. '/*/' .. scheme) ) do
+               _assert( luci.fs.access(file), "Can't access file '%s'", file )
+
+               self.uci.set_confdir( luci.fs.dirname(file) )
+               self.uci.load( luci.fs.basename(file) )
+
+               table.insert( schemes, self.uci.get_all( luci.fs.basename(file) ) )
+       end
+
+       return self:_read_scheme_parts( scheme, schemes )
+end
+
+-- Process all given parts and construct validation tree
+function UVL._read_scheme_parts( self, scheme, schemes )
+
+       local stbl = { }
+
+       -- helper function to construct identifiers for given elements
+       local function _id( c, t )
+               if c == TYPE_SECTION then
+                       return string.format(
+                               "section '%s.%s'",
+                                       scheme, t.name or '?' )
+               elseif c == TYPE_VARIABLE then
+                       return string.format(
+                               "variable '%s.%s.%s'",
+                                       scheme, t.section or '?.?', t.name or '?' )
+               elseif c == TYPE_ENUM then
+                       return string.format(
+                               "enum '%s.%s.%s'",
+                                       scheme, t.variable or '?.?.?', t.value or '?' )
+               end
+       end
+
+       -- helper function to check for required fields
+       local function _req( c, t, r )
+               for i, v in ipairs(r) do
+                       _assert( t[v], "Missing required field '%s' in %s", v, _id(c, t) )
+               end
+       end
+
+       -- helper function to validate references
+       local function _ref( c, t )
+               local k
+               if c == TYPE_SECTION then
+                       k = "package"
+               elseif c == TYPE_VARIABLE then
+                       k = "section"
+               elseif c == TYPE_ENUM then
+                       k = "variable"
+               end
+
+               local r = luci.util.split( t[k], "." )
+               r[1] = ( #r[1] > 0 and r[1] or scheme )
+
+               _assert( #r == c, "Malformed %s reference in %s", k, _id(c, t) )
+
+               return r
+       end
+
+       -- Step 1: get all sections
+       for i, conf in ipairs( schemes ) do
+               for k, v in pairs( conf ) do
+                       if v['.type'] == 'section' then
+
+                               _req( TYPE_SECTION, v, { "name", "package" } )
+
+                               local r = _ref( TYPE_SECTION, v )
+
+                               stbl.packages[r[1]] =
+                                       stbl.packages[r[1]] or {
+                                               ["sections"]  = { };
+                                               ["variables"] = { };
+                                       }
+
+                               local p = stbl.packages[r[1]]
+                                         p.sections[v.name]  = p.sections[v.name]  or { }
+                                         p.variables[v.name] = p.variables[v.name] or { }
+
+                               local s = p.sections[v.name]
+
+                               for k, v in pairs(v) do
+                                       if k ~= "name" and k ~= "package" and k:sub(1,1) ~= "." then
+                                               if k:match("^depends") then
+                                                       s["depends"] = _assert(
+                                                               self:_read_depency( v, s["depends"] ),
+                                                               "Section '%s' in scheme '%s' has malformed " ..
+                                                               "depency specification in '%s'",
+                                                               v.name, scheme, k
+                                                       )
+                                               else
+                                                       s[k] = v
+                                               end
+                                       end
+                               end
+                       end
+               end
+       end
+
+       -- Step 2: get all variables
+       for i, conf in ipairs( schemes ) do
+               for k, v in pairs( conf ) do
+                       if v['.type'] == "variable" then
+
+                               _req( TYPE_VARIABLE, v, { "name", "type", "section" } )
+
+                               local r = _ref( TYPE_VARIABLE, v )
+
+                               local p = _assert( stbl.packages[r[1]],
+                                       "Variable '%s' in scheme '%s' references unknown package '%s'",
+                                       v.name, scheme, r[1] )
+
+                               local s = _assert( p.variables[r[2]],
+                                       "Variable '%s' in scheme '%s' references unknown section '%s'",
+                                       v.name, scheme, r[2] )
+
+                               s[v.name] = s[v.name] or { }
+
+                               local t = s[v.name]
+
+                               for k, v in pairs(v) do
+                                       if k ~= "name" and k ~= "section" and k:sub(1,1) ~= "." then
+                                               if k:match("^depends") then
+                                                       t["depends"] = _assert(
+                                                               self:_read_depency( v, t["depends"] ),
+                                                               "Variable '%s' in scheme '%s' has malformed " ..
+                                                               "depency specification in '%s'",
+                                                               v.name, scheme, k
+                                                       )
+                                               elseif k:match("^validator") then
+                                                       t["validators"] = _assert(
+                                                               self:_read_validator( v, t["validators"] ),
+                                                               "Variable '%s' in scheme '%s' has malformed " ..
+                                                               "validator specification in '%s'",
+                                                               v.name, scheme, k
+                                                       )
+                                               else
+                                                       t[k] = v
+                                               end
+                                       end
+                               end
+                       end
+               end
+       end
+
+       -- Step 3: get all enums
+       for i, conf in ipairs( schemes ) do
+               for k, v in pairs( conf ) do
+                       if v['.type'] == "enum" then
+
+                               _req( TYPE_ENUM, v, { "value", "variable" } )
+
+                               local r = _ref( TYPE_ENUM, v )
+
+                               local p = _assert( stbl.packages[r[1]],
+                                       "Enum '%s' in scheme '%s' references unknown package '%s'",
+                                       v.value, scheme, r[1] )
+
+                               local s = _assert( p.variables[r[2]],
+                                       "Enum '%s' in scheme '%s' references unknown section '%s'",
+                                       v.value, scheme, r[2] )
+
+                               local t = _assert( s[r[3]],
+                                       "Enum '%s' in scheme '%s', section '%s' references " ..
+                                       "unknown variable '%s'",
+                                       v.value, scheme, r[2], r[3] )
+
+                               _assert( t.type == "enum",
+                                       "Enum '%s' in scheme '%s', section '%s' references " ..
+                                       "variable '%s' with non enum type '%s'",
+                                       v.value, scheme, r[2], r[3], t.type )
+
+                               if not t.values then
+                                       t.values = { [v.value] = v.title or v.value }
+                               else
+                                       t.values[v.value] = v.title or v.value
+                               end
+
+                               if v.default then
+                                       _assert( not t.default,
+                                               "Enum '%s' in scheme '%s', section '%s' redeclares " ..
+                                               "the default value of variable '%s'",
+                                               v.value, scheme, r[2], v.variable )
+
+                                       t.default = v.value
+                               end
+                       end
+               end
+       end
+
+       return stbl
+end
+
+-- Read a depency specification
+function UVL._read_depency( self, value, deps )
+       local parts     = luci.util.split( value, "%s*;%s*" )
+       local condition = { }
+
+       for i, val in ipairs(parts) do
+               local k, v = unpack(luci.util.split( val, "%s*=%s*" ))
+
+               if k and (
+                       k:match("^%$?[a-zA-Z0-9_]+%.%$?[a-zA-Z0-9_]+%.%$?[a-zA-Z0-9_]+$")
+                       k:match("^%$?[a-zA-Z0-9_]+%.%$?[a-zA-Z0-9_]+$") or
+                       k:match("^%$?[a-zA-Z0-9_]+$") or
+               ) then
+                       condition[k] = v or true
+               else
+                       return nil
+               end
+       end
+
+       if not deps then
+               deps = { condition }
+       else
+               table.insert( deps, condition )
+       end
+
+       return deps
+end
+
+-- Read a validator specification
+function UVL._read_validator( self, value, validators )
+       local validator
+
+       if value and value:match("/") and self.datatypes.file(value) then
+               validator = value
+       else
+               validator = self:_resolve_function( value )
+       end
+
+       if validator then
+               if not validators then
+                       validators = { validator }
+               else
+                       table.insert( validators, validator )
+               end
+
+               return validators
+       end
+end
+
+-- Resolve given path
+function UVL._resolve_function( self, value )
+       local path = luci.util.split(value, ".")
+
+       for i=1, #path-1 do
+               local stat, mod = pcall(require, table.concat(path, ".", 1, i))
+               if stat and mod then
+                       for j=i+1, #path-1 do
+                               if not type(mod) == "table" then
+                                       break;
+                               end
+                               mod = mod[path[j]]
+                               if not mod then
+                                       break
+                               end
+                       end
+                       mod = type(mod) == "table" and mod[path[#path]] or nil
+                       if type(mod) == "function" then
+                               return mod
+                       end
+               end
+       end
+end
diff --git a/libs/uvl/luasrc/uvl/datatypes.lua b/libs/uvl/luasrc/uvl/datatypes.lua
new file mode 100644 (file)
index 0000000..586e3f8
--- /dev/null
@@ -0,0 +1,135 @@
+--[[
+
+UCI Validation Layer - Datatype Tests
+(c) 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+(c) 2008 Steven Barth <steven@midlink.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+
+]]--
+
+module( "luci.uvl.datatypes", package.seeall )
+
+require("luci.fs")
+require("luci.ip")
+require("luci.util")
+
+
+function boolean( val )
+       if val == "1" or val == "yes" or val == "on" then
+               return true
+       elseif val == "0" or val == "no" or val == "off" then
+               return true
+       end
+
+       return false
+end
+
+function integer( val )
+       local n = tonumber(val)
+       if n ~= nil and math.floor(n) == n then
+               return true
+       end
+
+       return false
+end
+
+function float( val )
+       return ( tonumber(val) ~= nil )
+end
+
+function ip4addr( val )
+       if val then
+               return luci.ip.IPv4(val) and true or false
+       end
+
+       return false
+end
+
+function ip4prefix( val )
+       val = tonumber(val)
+       return ( val and val >= 0 and val <= 32 )
+end
+
+function ip6addr( val )
+       if val then
+               return luci.ip.IPv6(val) and true or false
+       end
+
+       return false
+end
+
+function ip6prefix( val )
+       val = tonumber(val)
+       return ( val and val >= 0 and val <= 128 )
+end
+
+function macaddr( val )
+       if val and val:match(
+               "^[a-fA-F0-9]+:[a-fA-F0-9]+:[a-fA-F0-9]+:" ..
+                "[a-fA-F0-9]+:[a-fA-F0-9]+:[a-fA-F0-9]+$"
+       ) then
+               local parts = luci.util.split( val, ":" )
+
+               for i = 1,6 do
+                       parts[i] = tonumber( parts[i], 16 )
+                       if parts[i] < 0 or parts[i] > 255 then
+                               return false
+                       end
+               end
+
+               return true
+       end
+
+       return false
+end
+
+function hostname( val )
+       if val and val:match("[a-zA-Z0-9_][a-zA-Z0-9_%-%.]*") then
+               return true     -- XXX: ToDo: need better solution
+       end
+
+       return false
+end
+
+function string( val )
+       return true             -- Everything qualifies as valid string
+end
+
+function directory( val, seen )
+       local s = luci.fs.stat( val )
+       seen = seen or { }
+
+       if s and not seen[s.ino] then
+               seen[s.ino] = true
+               if s.type == "directory" then
+                       return true
+               elseif s.type == "link" then
+                       return directory( luci.fs.readlink(val), seen )
+               end
+       end
+
+       return false
+end
+
+function file( val, seen )
+       local s = luci.fs.stat( val )
+       seen = seen or { }
+
+       if s and not seen[s.ino] then
+               seen[s.ino] = true
+               if s.type == "regular" then
+                       return true
+               elseif s.type == "link" then
+                       return file( luci.fs.readlink(val), seen )
+               end
+       end
+
+       return false
+end