0e32dc1e239c7f9d137fa6d1e736f4d895ea9a53
[project/luci.git] / libs / uvl / luasrc / uvl.lua
1 --[[
2
3 UCI Validation Layer - Main Library
4 (c) 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
5 (c) 2008 Steven Barth <steven@midlink.org>
6
7 Licensed under the Apache License, Version 2.0 (the "License");
8 you may not use this file except in compliance with the License.
9 You may obtain a copy of the License at
10
11         http://www.apache.org/licenses/LICENSE-2.0
12
13 $Id$
14
15 ]]--
16
17 module( "luci.uvl", package.seeall )
18
19 require("luci.fs")
20 require("luci.util")
21 require("luci.model.uci")
22 require("luci.uvl.datatypes")
23 --require("luci.uvl.validation")
24 require("luci.uvl.dependencies")
25
26 TYPE_SECTION  = 0x01
27 TYPE_VARIABLE = 0x02
28 TYPE_ENUM     = 0x03
29
30 STRICT_UNKNOWN_SECTIONS = true
31 STRICT_UNKNOWN_OPTIONS  = true
32
33
34 local default_schemedir = "/etc/scheme"
35
36 local function _assert( condition, fmt, ... )
37         if not condition then
38                 return assert( nil, string.format( fmt, ... ) )
39         else
40                 return condition
41         end
42 end
43
44 UVL = luci.util.class()
45
46 function UVL.__init__( self, schemedir )
47         self.schemedir  = schemedir or default_schemedir
48         self.packages   = { }
49         self.beenthere  = { }
50         self.uci                = luci.model.uci
51         self.datatypes  = luci.uvl.datatypes
52 end
53
54 function UVL._keys( self, tbl )
55         local keys = { }
56         if tbl then
57                 for k, _ in luci.util.kspairs(tbl) do
58                         table.insert( keys, k )
59                 end
60         end
61         return keys
62 end
63
64
65 --- Validate given configuration.
66 -- @param config        Name of the configuration to validate
67 -- @param scheme        Scheme to validate against (optional)
68 -- @return                      Boolean indicating weather the given config validates
69 -- @return                      String containing the reason for errors (if any)
70 function UVL.validate( self, config )
71
72         self.uci.set_confdir( self.uci.confdir_default )
73         self.uci.load( config )
74         self.beenthere = { }
75
76         local co = self.uci.get_all( config )
77
78         local function _uci_foreach( type, func )
79                 local ok, err
80                 for k, v in pairs(co) do
81                         if co[k]['.type'] == type then
82                                 ok, err = func( k, v )
83                                 if not ok then break end
84                         end
85                 end
86                 return ok, err
87         end
88
89         for k, v in pairs( self.packages[config].sections ) do
90                 local ok, err = _uci_foreach( k,
91                         function(s)
92                                 local sect = luci.uvl.section( self, co, k, config, s )
93                                 return self:_validate_section( sect )
94                         end
95                 )
96                 if not ok then return false, err end
97         end
98
99         if STRICT_UNKNOWN_SECTIONS then
100                 for k, v in pairs(co) do
101                         if not self.beenthere[config..'.'..k] then
102                                 return false, "Section '" .. config .. '.' .. co[k]['.type'] ..
103                                         "' not found in scheme"
104                         end
105                 end
106         end
107
108         return true, nil
109 end
110
111 function UVL.validate_section( self, config, section )
112         self.uci.set_confdir( self.uci.confdir_default )
113         self.uci.load( config )
114         self.beenthere = { }
115
116         local co = self.uci.get_all( config )
117         if co[section] then
118                 return self:_validate_section( luci.uvl.section(
119                         self, co, co[section]['.type'], config, section
120                 ) )
121         else
122                 return false, "Section '" .. config .. '.' .. section ..
123                         "' not found in config. Nothing to do."
124         end
125 end
126
127 function UVL.validate_option( self, config, section, option )
128         self.uci.set_confdir( self.uci.confdir_default )
129         self.uci.load( config )
130         self.beenthere = { }
131
132         local co = self.uci.get_all( config )
133         if co[section] and co[section][option] then
134                 return self:_validate_option( luci.uvl.option(
135                         self, co, co[section]['.type'], config, section, option
136                 ) )
137         else
138                 return false, "Option '" ..
139                         config .. '.' .. section .. '.' .. option ..
140                         "' not found in config. Nothing to do."
141         end
142 end
143
144 --- Validate given section of given configuration.
145 -- @param config        Name of the configuration to validate
146 -- @param section       Key of the section to validate
147 -- @param scheme        Scheme to validate against
148 -- @return                      Boolean indicating weather the given config validates
149 -- @return                      String containing the reason for errors (if any)
150 function UVL._validate_section( self, section )
151
152         if section:values() then
153
154                 for _, v in ipairs(section:variables()) do
155                         local ok, err = self:_validate_option( v )
156
157                         if not ok then
158                                 return ok, err
159                         end
160                 end
161
162                 local ok, err = luci.uvl.dependencies.check( self, section )
163
164                 if not ok then
165                         return false, "All possible dependencies failed"
166                 end
167         else
168                 print( "Error, scheme section '" .. section:sid() .. "' not found in data" )
169         end
170
171         if STRICT_UNKNOWN_OPTIONS and not section:section().dynamic then
172                 for k, v in pairs(section:values()) do
173                         if k:sub(1,1) ~= "." and not self.beenthere[
174                                 section:cid() .. '.' .. k
175                         ] then
176                                 return false, "Option '" .. section:sid() .. '.' .. k ..
177                                         "' not found in scheme"
178                         end
179                 end
180         end
181
182         return true, nil
183 end
184
185 --- Validate given option within section of given configuration.
186 -- @param config        Name of the configuration to validate
187 -- @param section       Key of the section to validate
188 -- @param option        Name of the option to validate
189 -- @param scheme        Scheme to validate against
190 -- @return                      Boolean indicating weather the given config validates
191 -- @return                      String containing the reason for errors (if any)
192 function UVL._validate_option( self, option, nodeps )
193
194         if not option:option() and
195            not ( option:section() and option:section().dynamic )
196         then
197                 return false, "Requested option '" .. option:sid() ..
198                         "' not found in scheme"
199         end
200
201         if option:option() then
202                 if option:option().required and not option:value() then
203                         return false, "Mandatory variable '" .. option:cid() ..
204                                 "' doesn't have a value"
205                 end
206
207                 if option:option().type == "enum" and option:value() then
208                         if not option:option().values or
209                            not option:option().values[option:value()]
210                         then
211                                 return false, "Value '" .. ( option:value() or '<nil>' ) ..
212                                         "' of given option '" .. option:cid() ..
213                                         "' is not defined in enum { " ..
214                                         table.concat(self:_keys(option:option().values),", ") ..
215                                         " }"
216                         end
217                 end
218
219                 if option:option().datatype and option:value() then
220                         if self.datatypes[option:option().datatype] then
221                                 if not self.datatypes[option:option().datatype](
222                                         option:value()
223                                 ) then
224                                         return false, "Value '" .. ( option:value() or '<nil>' ) ..
225                                                 "' of given option '" .. option:cid() ..
226                                                 "' doesn't validate as datatype '" ..
227                                                 option:option().datatype .. "'"
228                                 end
229                         else
230                                 return false, "Unknown datatype '" ..
231                                         option:option().datatype .. "' encountered"
232                         end
233                 end
234
235                 if not nodeps then
236                         return luci.uvl.dependencies.check( self, option )
237                 end
238         end
239
240         return true, nil
241 end
242
243 --- Find all parts of given scheme and construct validation tree
244 -- @param scheme        Name of the scheme to parse
245 -- @return                      Parsed scheme
246 function UVL.read_scheme( self, scheme )
247         local schemes = { }
248
249         for i, file in ipairs( luci.fs.glob(self.schemedir .. '/*/' .. scheme) ) do
250                 _assert( luci.fs.access(file), "Can't access file '%s'", file )
251
252                 self.uci.set_confdir( luci.fs.dirname(file) )
253                 self.uci.load( luci.fs.basename(file) )
254
255                 table.insert( schemes, self.uci.get_all( luci.fs.basename(file) ) )
256         end
257
258         return self:_read_scheme_parts( scheme, schemes )
259 end
260
261 -- Process all given parts and construct validation tree
262 function UVL._read_scheme_parts( self, scheme, schemes )
263
264         -- helper function to construct identifiers for given elements
265         local function _id( c, t )
266                 if c == TYPE_SECTION then
267                         return string.format(
268                                 "section '%s.%s'",
269                                         scheme, t.name or '?' )
270                 elseif c == TYPE_VARIABLE then
271                         return string.format(
272                                 "variable '%s.%s.%s'",
273                                         scheme, t.section or '?.?', t.name or '?' )
274                 elseif c == TYPE_ENUM then
275                         return string.format(
276                                 "enum '%s.%s.%s'",
277                                         scheme, t.variable or '?.?.?', t.value or '?' )
278                 end
279         end
280
281         -- helper function to check for required fields
282         local function _req( c, t, r )
283                 for i, v in ipairs(r) do
284                         _assert( t[v], "Missing required field '%s' in %s", v, _id(c, t) )
285                 end
286         end
287
288         -- helper function to validate references
289         local function _ref( c, t )
290                 local k
291                 if c == TYPE_SECTION then
292                         k = "package"
293                 elseif c == TYPE_VARIABLE then
294                         k = "section"
295                 elseif c == TYPE_ENUM then
296                         k = "variable"
297                 end
298
299                 local r = luci.util.split( t[k], "." )
300                 r[1] = ( #r[1] > 0 and r[1] or scheme )
301
302                 _assert( #r == c, "Malformed %s reference in %s", k, _id(c, t) )
303
304                 return r
305         end
306
307         -- Step 1: get all sections
308         for i, conf in ipairs( schemes ) do
309                 for k, v in pairs( conf ) do
310                         if v['.type'] == 'section' then
311
312                                 _req( TYPE_SECTION, v, { "name", "package" } )
313
314                                 local r = _ref( TYPE_SECTION, v )
315
316                                 self.packages[r[1]] =
317                                         self.packages[r[1]] or {
318                                                 ["sections"]  = { };
319                                                 ["variables"] = { };
320                                         }
321
322                                 local p = self.packages[r[1]]
323                                           p.sections[v.name]  = p.sections[v.name]  or { }
324                                           p.variables[v.name] = p.variables[v.name] or { }
325
326                                 local s = p.sections[v.name]
327
328                                 for k, v2 in pairs(v) do
329                                         if k ~= "name" and k ~= "package" and k:sub(1,1) ~= "." then
330                                                 if k:match("^depends") then
331                                                         s["depends"] = _assert(
332                                                                 self:_read_dependency( v2, s["depends"] ),
333                                                                 "Section '%s' in scheme '%s' has malformed " ..
334                                                                 "dependency specification in '%s'",
335                                                                 v.name or '<nil>', scheme or '<nil>', k
336                                                         )
337                                                 else
338                                                         s[k] = v2
339                                                 end
340                                         end
341                                 end
342                         end
343                 end
344         end
345
346         -- Step 2: get all variables
347         for i, conf in ipairs( schemes ) do
348                 for k, v in pairs( conf ) do
349                         if v['.type'] == "variable" then
350
351                                 _req( TYPE_VARIABLE, v, { "name", "section" } )
352
353                                 local r = _ref( TYPE_VARIABLE, v )
354
355                                 local p = _assert( self.packages[r[1]],
356                                         "Variable '%s' in scheme '%s' references unknown package '%s'",
357                                         v.name, scheme, r[1] )
358
359                                 local s = _assert( p.variables[r[2]],
360                                         "Variable '%s' in scheme '%s' references unknown section '%s'",
361                                         v.name, scheme, r[2] )
362
363                                 s[v.name] = s[v.name] or { }
364
365                                 local t = s[v.name]
366
367                                 for k, v in pairs(v) do
368                                         if k ~= "name" and k ~= "section" and k:sub(1,1) ~= "." then
369                                                 if k:match("^depends") then
370                                                         t["depends"] = _assert(
371                                                                 self:_read_dependency( v, t["depends"] ),
372                                                                 "Variable '%s' in scheme '%s' has malformed " ..
373                                                                 "dependency specification in '%s'",
374                                                                 v.name, scheme, k
375                                                         )
376                                                 elseif k:match("^validator") then
377                                                         t["validators"] = _assert(
378                                                                 self:_read_validator( v, t["validators"] ),
379                                                                 "Variable '%s' in scheme '%s' has malformed " ..
380                                                                 "validator specification in '%s'",
381                                                                 v.name, scheme, k
382                                                         )
383                                                 else
384                                                         t[k] = v
385                                                 end
386                                         end
387                                 end
388                         end
389                 end
390         end
391
392         -- Step 3: get all enums
393         for i, conf in ipairs( schemes ) do
394                 for k, v in pairs( conf ) do
395                         if v['.type'] == "enum" then
396
397                                 _req( TYPE_ENUM, v, { "value", "variable" } )
398
399                                 local r = _ref( TYPE_ENUM, v )
400
401                                 local p = _assert( self.packages[r[1]],
402                                         "Enum '%s' in scheme '%s' references unknown package '%s'",
403                                         v.value, scheme, r[1] )
404
405                                 local s = _assert( p.variables[r[2]],
406                                         "Enum '%s' in scheme '%s' references unknown section '%s'",
407                                         v.value, scheme, r[2] )
408
409                                 local t = _assert( s[r[3]],
410                                         "Enum '%s' in scheme '%s', section '%s' references " ..
411                                         "unknown variable '%s'",
412                                         v.value, scheme, r[2], r[3] )
413
414                                 _assert( t.type == "enum",
415                                         "Enum '%s' in scheme '%s', section '%s' references " ..
416                                         "variable '%s' with non enum type '%s'",
417                                         v.value, scheme, r[2], r[3], t.type )
418
419                                 if not t.values then
420                                         t.values = { [v.value] = v.title or v.value }
421                                 else
422                                         t.values[v.value] = v.title or v.value
423                                 end
424
425                                 if v.default then
426                                         _assert( not t.default,
427                                                 "Enum '%s' in scheme '%s', section '%s' redeclares " ..
428                                                 "the default value of variable '%s'",
429                                                 v.value, scheme, r[2], v.variable )
430
431                                         t.default = v.value
432                                 end
433                         end
434                 end
435         end
436
437         return self
438 end
439
440 -- Read a dependency specification
441 function UVL._read_dependency( self, value, deps )
442         local parts     = luci.util.split( value, "%s*,%s*", nil, true )
443         local condition = { }
444
445         for i, val in ipairs(parts) do
446                 local k, v = unpack(luci.util.split( val, "%s*=%s*", nil, true ))
447
448                 if k and (
449                         k:match("^%$?[a-zA-Z0-9_]+%.%$?[a-zA-Z0-9_]+%.%$?[a-zA-Z0-9_]+$") or
450                         k:match("^%$?[a-zA-Z0-9_]+%.%$?[a-zA-Z0-9_]+$") or
451                         k:match("^%$?[a-zA-Z0-9_]+$")
452                 ) then
453                         condition[k] = v or true
454                 else
455                         return nil
456                 end
457         end
458
459         if not deps then
460                 deps = { condition }
461         else
462                 table.insert( deps, condition )
463         end
464
465         return deps
466 end
467
468 -- Read a validator specification
469 function UVL._read_validator( self, value, validators )
470         local validator
471
472         if value and value:match("/") and self.datatypes.file(value) then
473                 validator = value
474         else
475                 validator = self:_resolve_function( value )
476         end
477
478         if validator then
479                 if not validators then
480                         validators = { validator }
481                 else
482                         table.insert( validators, validator )
483                 end
484
485                 return validators
486         end
487 end
488
489 -- Resolve given path
490 function UVL._resolve_function( self, value )
491         local path = luci.util.split(value, ".")
492
493         for i=1, #path-1 do
494                 local stat, mod = pcall(require, table.concat(path, ".", 1, i))
495                 if stat and mod then
496                         for j=i+1, #path-1 do
497                                 if not type(mod) == "table" then
498                                         break
499                                 end
500                                 mod = mod[path[j]]
501                                 if not mod then
502                                         break
503                                 end
504                         end
505                         mod = type(mod) == "table" and mod[path[#path]] or nil
506                         if type(mod) == "function" then
507                                 return mod
508                         end
509                 end
510         end
511 end
512
513
514 section = luci.util.class()
515
516 function section.__init__(self, scheme, co, st, c, s)
517         self.csection = co[s]
518         self.ssection = scheme.packages[c].sections[st]
519         self.cref     = { c, s }
520         self.sref     = { c, st }
521         self.scheme   = scheme
522         self.config   = co
523         self.type     = luci.uvl.TYPE_SECTION
524 end
525
526 function section.cid(self)
527         return ( self.cref[1] or '?' ) .. '.' .. ( self.cref[2] or '?' )
528 end
529
530 function section.sid(self)
531         return ( self.sref[1] or '?' ) .. '.' .. ( self.sref[2] or '?' )
532 end
533
534 function section.values(self)
535         return self.csection
536 end
537
538 function section.section(self)
539         return self.ssection
540 end
541
542 function section.variables(self)
543         local v = { }
544         if self.scheme.packages[self.sref[1]].variables[self.sref[2]] then
545                 for o, _ in pairs(
546                         self.scheme.packages[self.sref[1]].variables[self.sref[2]]
547                 ) do
548                         table.insert( v, luci.uvl.option(
549                                 self.scheme, self.config, self.sref[2],
550                                 self.cref[1], self.cref[2], o
551                         ) )
552                 end
553         end
554         return v
555 end
556
557
558 option = luci.util.class()
559
560 function option.__init__(self, scheme, co, st, c, s, o)
561         self.coption = co[s] and co[s][o] or nil
562         self.soption = scheme.packages[c].variables[st][o]
563         self.cref    = { c, s, o }
564         self.sref    = { c, st, o }
565         self.scheme  = scheme
566         self.config  = co
567         self.type    = luci.uvl.TYPE_OPTION
568 end
569
570 function option.cid(self)
571         return ( self.cref[1] or '?' ) .. '.' ..
572                    ( self.cref[2] or '?' ) .. '.' ..
573                    ( self.cref[3] or '?' )
574 end
575
576 function option.sid(self)
577         return ( self.sref[1] or '?' ) .. '.' ..
578                    ( self.sref[2] or '?' ) .. '.' ..
579                    ( self.sref[3] or '?' )
580 end
581
582 function option.value(self)
583         return self.coption
584 end
585
586 function option.option(self)
587         return self.soption
588 end
589
590 function option.section(self)
591         return self.scheme.packages[self.sref[1]].sections[self.sref[2]]
592 end