From: Jo-Philipp Wich Date: Thu, 14 Aug 2008 20:24:31 +0000 (+0000) Subject: * luci/libs: add preliminary uvl code X-Git-Tag: 0.8.0~428 X-Git-Url: https://git.archive.openwrt.org/?a=commitdiff_plain;ds=sidebyside;h=7726e737252026683d10f5d85a32798f975ef437;p=project%2Fluci.git * luci/libs: add preliminary uvl code --- diff --git a/libs/uvl/Makefile b/libs/uvl/Makefile new file mode 100644 index 000000000..81a96f6a8 --- /dev/null +++ b/libs/uvl/Makefile @@ -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 index 000000000..4894d3006 --- /dev/null +++ b/libs/uvl/luasrc/uvl.lua @@ -0,0 +1,403 @@ +--[[ + +UCI Validation Layer - Main Library +(c) 2008 Jo-Philipp Wich +(c) 2008 Steven Barth + +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 index 000000000..586e3f8b8 --- /dev/null +++ b/libs/uvl/luasrc/uvl/datatypes.lua @@ -0,0 +1,135 @@ +--[[ + +UCI Validation Layer - Datatype Tests +(c) 2008 Jo-Philipp Wich +(c) 2008 Steven Barth + +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