From 33728ad3382d74281412d4556561d479bb88832b Mon Sep 17 00:00:00 2001 From: Kaarle Ritvanen Date: Tue, 8 Oct 2013 18:50:56 +0300 Subject: changed module paths from acf to acf2 --- Makefile | 6 +- acf/error.lua | 99 ------------ acf/init.lua | 18 --- acf/loader.lua | 23 --- acf/model/aaa.lua | 87 ----------- acf/model/combination.lua | 50 ------ acf/model/field.lua | 241 ----------------------------- acf/model/init.lua | 205 ------------------------ acf/model/model.lua | 204 ------------------------ acf/model/net.lua | 96 ------------ acf/model/node.lua | 275 --------------------------------- acf/model/permission.lua | 22 --- acf/model/root.lua | 91 ----------- acf/model/set.lua | 47 ------ acf/modules/awall.lua | 149 ------------------ acf/modules/generic.lua | 14 -- acf/modules/net.lua | 25 --- acf/object.lua | 68 -------- acf/path.lua | 111 ------------- acf/persistence/backends/augeas.lua | 153 ------------------ acf/persistence/backends/files.lua | 107 ------------- acf/persistence/backends/json.lua | 79 ---------- acf/persistence/backends/null.lua | 10 -- acf/persistence/backends/volatile.lua | 46 ------ acf/persistence/init.lua | 79 ---------- acf/persistence/util.lua | 22 --- acf/transaction/backend.lua | 70 --------- acf/transaction/init.lua | 210 ------------------------- acf/util.lua | 84 ---------- acf2/error.lua | 99 ++++++++++++ acf2/init.lua | 18 +++ acf2/loader.lua | 23 +++ acf2/model/aaa.lua | 87 +++++++++++ acf2/model/combination.lua | 50 ++++++ acf2/model/field.lua | 241 +++++++++++++++++++++++++++++ acf2/model/init.lua | 205 ++++++++++++++++++++++++ acf2/model/model.lua | 204 ++++++++++++++++++++++++ acf2/model/net.lua | 96 ++++++++++++ acf2/model/node.lua | 275 +++++++++++++++++++++++++++++++++ acf2/model/permission.lua | 22 +++ acf2/model/root.lua | 91 +++++++++++ acf2/model/set.lua | 47 ++++++ acf2/modules/awall.lua | 149 ++++++++++++++++++ acf2/modules/generic.lua | 14 ++ acf2/modules/net.lua | 25 +++ acf2/object.lua | 68 ++++++++ acf2/path.lua | 111 +++++++++++++ acf2/persistence/backends/augeas.lua | 153 ++++++++++++++++++ acf2/persistence/backends/files.lua | 107 +++++++++++++ acf2/persistence/backends/json.lua | 79 ++++++++++ acf2/persistence/backends/null.lua | 10 ++ acf2/persistence/backends/volatile.lua | 46 ++++++ acf2/persistence/init.lua | 79 ++++++++++ acf2/persistence/util.lua | 22 +++ acf2/transaction/backend.lua | 70 +++++++++ acf2/transaction/init.lua | 210 +++++++++++++++++++++++++ acf2/util.lua | 84 ++++++++++ server.lua | 6 +- 58 files changed, 2691 insertions(+), 2691 deletions(-) delete mode 100644 acf/error.lua delete mode 100644 acf/init.lua delete mode 100644 acf/loader.lua delete mode 100644 acf/model/aaa.lua delete mode 100644 acf/model/combination.lua delete mode 100644 acf/model/field.lua delete mode 100644 acf/model/init.lua delete mode 100644 acf/model/model.lua delete mode 100644 acf/model/net.lua delete mode 100644 acf/model/node.lua delete mode 100644 acf/model/permission.lua delete mode 100644 acf/model/root.lua delete mode 100644 acf/model/set.lua delete mode 100644 acf/modules/awall.lua delete mode 100644 acf/modules/generic.lua delete mode 100644 acf/modules/net.lua delete mode 100644 acf/object.lua delete mode 100644 acf/path.lua delete mode 100644 acf/persistence/backends/augeas.lua delete mode 100644 acf/persistence/backends/files.lua delete mode 100644 acf/persistence/backends/json.lua delete mode 100644 acf/persistence/backends/null.lua delete mode 100644 acf/persistence/backends/volatile.lua delete mode 100644 acf/persistence/init.lua delete mode 100644 acf/persistence/util.lua delete mode 100644 acf/transaction/backend.lua delete mode 100644 acf/transaction/init.lua delete mode 100644 acf/util.lua create mode 100644 acf2/error.lua create mode 100644 acf2/init.lua create mode 100644 acf2/loader.lua create mode 100644 acf2/model/aaa.lua create mode 100644 acf2/model/combination.lua create mode 100644 acf2/model/field.lua create mode 100644 acf2/model/init.lua create mode 100644 acf2/model/model.lua create mode 100644 acf2/model/net.lua create mode 100644 acf2/model/node.lua create mode 100644 acf2/model/permission.lua create mode 100644 acf2/model/root.lua create mode 100644 acf2/model/set.lua create mode 100644 acf2/modules/awall.lua create mode 100644 acf2/modules/generic.lua create mode 100644 acf2/modules/net.lua create mode 100644 acf2/object.lua create mode 100644 acf2/path.lua create mode 100644 acf2/persistence/backends/augeas.lua create mode 100644 acf2/persistence/backends/files.lua create mode 100644 acf2/persistence/backends/json.lua create mode 100644 acf2/persistence/backends/null.lua create mode 100644 acf2/persistence/backends/volatile.lua create mode 100644 acf2/persistence/init.lua create mode 100644 acf2/persistence/util.lua create mode 100644 acf2/transaction/backend.lua create mode 100644 acf2/transaction/init.lua create mode 100644 acf2/util.lua diff --git a/Makefile b/Makefile index 18e9c64..063faa2 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ ROOT_DIR := / LUA_VERSION := 5.2 confdir := etc/acf2 -luadir := usr/share/lua/$(LUA_VERSION)/acf +luadir := usr/share/lua/$(LUA_VERSION)/acf2 resdir := usr/share/acf2 webdir := $(resdir)/web starter := usr/sbin/acf2 @@ -40,8 +40,8 @@ files += $(resdir)/$(1) endef -$(eval $(call link,acf,$(luadir))) -$(eval $(call rcopy,acf,$(luadir),lua,-not -path 'acf/modules/*')) +$(eval $(call link,acf2,$(luadir))) +$(eval $(call rcopy,acf2,$(luadir),lua,-not -path 'acf2/modules/*')) $(ROOT_DIR)/$(luadir)/modules: install -d $@ diff --git a/acf/error.lua b/acf/error.lua deleted file mode 100644 index c3a8d63..0000000 --- a/acf/error.lua +++ /dev/null @@ -1,99 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local M = {} - -local object = require('acf.object') -local class = object.class - -local util = require('acf.util') - -local json = require('cjson') - - -local ErrorTable = class() - -function ErrorTable:init() self.errors = {} end - -function ErrorTable:success() return not next(self.errors) end - -function ErrorTable:raise() - if not self:success() then error(json.encode(self.errors)) end -end - - -local ErrorList = class(ErrorTable) - -function ErrorList:init(label) - object.super(self, ErrorList):init() - self.label = label -end - -function ErrorList:insert(msg) - table.insert(util.setdefault(self.errors, self.label, {}), msg) -end - - -M.ErrorDict = class(ErrorTable) - -function M.ErrorDict:collect(func, ...) - local function pack(success, ...) - local arg = {...} - return success, success and arg or arg[1] - end - - local arg = {...} - local success, res = pack( - xpcall( - function() return func(unpack(arg)) end, - function(err) - local _, _, data = err:find('.-: (.+)') - local success, res = pcall(json.decode, data) - if success and type(res) == 'table' then return res end - return data..'\n'..debug.traceback() - end - ) - ) - - if success then return unpack(res) end - - if type(res) == 'table' then - for label, errors in pairs(res) do - for _, err in ipairs(errors) do - table.insert(util.setdefault(self.errors, label, {}), err) - end - end - else error(res) end -end - - -function M.raise(label, msg) - local err = ErrorList(label) - err:insert(msg) - err:raise() -end - -function M.relabel(label, ...) - local err = M.ErrorDict() - local res = {err:collect(...)} - if err:success() then return unpack(res) end - - elist = ErrorList(label) - for lbl, el in pairs(err.errors) do - for _, e in ipairs(el) do elist:insert(lbl..': '..e) end - end - elist:raise() -end - -function M.call(...) - local err = M.ErrorDict() - local res = {err:collect(...)} - if err:success() then return true, unpack(res) end - if err.errors.system then error(err.errors.system[1]) end - return false, err.errors -end - - -return M diff --git a/acf/init.lua b/acf/init.lua deleted file mode 100644 index 62c76e9..0000000 --- a/acf/init.lua +++ /dev/null @@ -1,18 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local M = {} - -M.model = require('acf.model') - -require('acf.model.aaa') -require('acf.loader')('modules') - -M.call = require('acf.error').call -M.object = require('acf.object') -M.path = require('acf.path') -M.start_txn = require('acf.transaction') - -return M diff --git a/acf/loader.lua b/acf/loader.lua deleted file mode 100644 index 13b2db0..0000000 --- a/acf/loader.lua +++ /dev/null @@ -1,23 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -module(..., package.seeall) - -local pth = require('acf.path') - -local posix = require('posix') -local stringy = require('stringy') - -return function(subdir) - local comps = pth.split('acf/'..subdir) - local res = {} - for _, modfile in ipairs(posix.dir(pth.join(unpack(comps)))) do - if stringy.endswith(modfile, '.lua') then - local name = modfile:sub(1, -5) - res[name] = require(table.concat(comps, '.')..'.'..name) - end - end - return res - end diff --git a/acf/model/aaa.lua b/acf/model/aaa.lua deleted file mode 100644 index 17ad98c..0000000 --- a/acf/model/aaa.lua +++ /dev/null @@ -1,87 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local M = require('acf.model') -local object = require('acf.object') - -local digest = require('crypto').digest - - -Role = M.new() -Role.permissions = M.Set{type=M.Reference{scope='../../../permissions'}} - - -local function hash_password(algorithm, salt, password) - return algorithm..'$'..salt..'$'..digest(algorithm, salt..password) -end - -local hash_pattern = '^(%w+)%$(%w+)%$%x+$' - - -local Password = object.class(M.String) - -function Password:_validate(context, value) - value = object.super(self, M.String):_validate(context, value) - if not value or value:find(hash_pattern) then return value end - - local salt = '' - for i = 1,12 do - local c = math.random(48, 109) - if c > 57 then c = c + 7 end - if c > 90 then c = c + 6 end - salt = salt..string.char(c) - end - return hash_password('sha256', salt, value) -end - - -User = M.new() -User.password = Password -User['real-name'] = M.String -User.superuser = M.Boolean{default=false} -User.roles = M.Set{type=M.Reference{scope='../../../roles'}} - -function User:check_password(password) - if not self.password then return false end - local _, _, algorithm, salt = self.password:find(hash_pattern) - if not salt then return false end - return hash_password(algorithm, salt, password) == self.password -end - -function User:check_permission(permission) - -- TODO audit trail - print('check permission', permission) - - if self.superuser then return true end - - assert(getmetatable(self).txn:fetch('/auth/permissions')[permission]) - - for _, role in M.node.pairs(self.roles, true) do - for _, p in M.node.pairs(role.permissions, true) do - if p == permission then return true end - end - end - return false -end - - -Authentication = M.new() -Authentication.users = M.Collection{type=User} -Authentication.roles = M.Collection{type=Role} -Authentication.permissions = M.Set{ - type=M.String, - addr='/volatile/aaa/permissions' -} - -M.register( - 'auth', - Authentication, - { - addr='/json'..require('posix').getcwd()..'/config/aaa.json', - ui_name='Authentication' - } -) - -M.permission.defaults('/auth') diff --git a/acf/model/combination.lua b/acf/model/combination.lua deleted file mode 100644 index 19f84cc..0000000 --- a/acf/model/combination.lua +++ /dev/null @@ -1,50 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local M = {} - -local err = require('acf.error') -local raise = err.raise - -local fld = require('acf.model.field') -local String = fld.String - -local to_field = require('acf.model.model').to_field - -local object = require('acf.object') -local class = object.class -local super = object.super - - -local stringy = require('stringy') - - -M.Range = class(String) - -function M.Range:init(params) - super(self, M.Range):init(params) - if not self.type then self.type = fld.Integer end -end - -function M.Range:validate(context, value) - local comps = stringy.split(value, '-') - if #comps > 2 then raise(context.path, 'Invalid range') end - for _, v in ipairs(comps) do to_field(self.type):_validate(context, v) end -end - - -M.Union = class(String) - -function M.Union:validate(context, value) - super(self, M.Union):validate(context, value) - for _, tpe in ipairs(self.types) do - local field = to_field(tpe) - if err.call(field.validate, field, context, value) then return end - end - raise(context.path, self.error or 'Invalid value') -end - - -return M diff --git a/acf/model/field.lua b/acf/model/field.lua deleted file mode 100644 index 4d539e8..0000000 --- a/acf/model/field.lua +++ /dev/null @@ -1,241 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local M = {} - -local err = require('acf.error') -local raise = err.raise - -local node = require('acf.model.node') - -local object = require('acf.object') -local class = object.class -local super = object.super - -local util = require('acf.util') - - -local function contains(list, value) - for k, v in ipairs(list) do if v == value then return true end end - return false -end - -M.Member = class() - -function M.Member:init(params) - for k, v in pairs(params or {}) do - if self[k] == nil then self[k] = v end - end -end - -function M.Member:auto_ui_name(name) - if not name then return end - return (name:sub(1, 1):upper()..name:sub(2)):gsub('-', ' ') -end - -function M.Member:meta(context) - return { - name=self.name, - description=self.description, - ['ui-name']=self.ui_name or self:auto_ui_name(self.name) - } -end - - -M.Field = class(M.Member) - -function M.Field:init(params) - super(self, M.Field):init(params) - - if self.choice and not self['ui-choice'] then - self['ui-choice'] = util.map( - function(name) return self:auto_ui_name(name) end, - self.choice - ) - end - - if not self.widget then - self.widget = self.choice and 'combobox' or 'field' - end -end - -function M.Field:meta(context) - assert(self.dtype) - local res = super(self, M.Field):meta(context) - - res.type = self.dtype - res.required = self.required - res.default = self.default - res.choice = self.choice - res.widget = self.widget - res['ui-choice'] = self['ui-choice'] - - return res -end - -function M.Field:topology(context) - return { - {path=context.path, addr=context.addr, type=self.dtype} - } -end - -function M.Field:load(context) - if not context.txn then return setmetatable({}, context) end - local value = context.txn:get(context.addr) - if value == nil then return self.default end - return value -end - -function M.Field:_validate(context, value) - if self.required and value == nil then - raise(context.path, 'Required value not set') - end - if self.choice and value ~= nil and not contains(self.choice, value) then - raise(context.path, 'Invalid value') - end - if value ~= nil then self:validate(context, value) end - return value -end - -function M.Field:validate(context, value) end - -function M.Field:save(context, value) - context.txn:set(context.addr, self:_validate(context, value)) -end - -function M.Field:validate_saved(context) - self:save(context, self:load(context)) -end - - -local Primitive = class(M.Field) - -function Primitive:validate(context, value) - local t = self.dtype - if type(value) ~= t then raise(context.path, 'Not a '..t) end -end - - -M.String = class(Primitive) - -function M.String:init(params) - super(self, M.String):init(params) - self.dtype = 'string' -end - -function M.String:validate(context, value) - super(self, M.String):validate(context, value) - if self['max-length'] and value:len() > self['max-length'] then - raise(context.path, 'Maximum length exceeded') - end - if self.pattern and not value:match('^'..self.pattern..'$') then - raise(context.path, 'Invalid value') - end -end - -function M.String:meta(context) - local res = super(self, M.String):meta(context) - res['max-length'] = self['max-length'] - return res -end - - -M.Number = class(Primitive) - -function M.Number:init(params) - super(self, M.Number):init(params) - self.dtype = 'number' -end - -function M.Number:_validate(context, value) - return super(self, M.Number):_validate( - context, - value and tonumber(value) or value - ) -end - - -M.Integer = class(M.Number) - -function M.Integer:validate(context, value) - super(self, M.Integer):validate(context, value) - if math.floor(value) ~= value then raise(context.path, 'Not an integer') end -end - - -M.Boolean = class(Primitive) - -function M.Boolean:init(params) - super(self, M.Boolean):init(params) - self.dtype = 'boolean' - self.widget = self.dtype -end - - -M.TreeNode = class(M.Field) - -function M.TreeNode:init(params) - if not params.widget then params.widget = 'link' end - super(self, M.TreeNode):init(params) -end - -function M.TreeNode:topology(context) - local res = super(self, M.TreeNode):topology(context) - res[1].type = 'table' - util.extend(res, node.topology(self:load(context, {create=true}))) - return res -end - -function M.TreeNode:load(context, options) - if context.txn and not ( - ( - options and options.create - ) or self.create or context.txn:get(context.addr) - ) then return end - return self.itype(context, self.iparams) -end - -function M.TreeNode:save(context, value) - local path = context.path - - if value == path then return end - if type(value) == 'string' then value = context.txn:fetch(value) end - if object.isinstance(value, node.TreeNode) and node.path(value) == path then - return - end - - context.txn:set(context.addr) - - if value then - if type(value) ~= 'table' then - raise(path, 'Cannot assign primitive value') - end - - context.txn:set(context.addr, {}) - local new = self:load(context, {create=true}) - - local errors = err.ErrorDict() - for k, v in node.pairs(value) do - errors:collect(self.save_member, new, k, v) - end - errors:raise() - end -end - -function M.TreeNode.save_member(node, k, v) node[k] = v end - - -M.Model = class(M.TreeNode) - -function M.Model:init(params) - super(self, M.Model):init(params) - - assert(self.model) - self.itype = self.model - self.dtype = 'model' -end - - -return M diff --git a/acf/model/init.lua b/acf/model/init.lua deleted file mode 100644 index 1de5202..0000000 --- a/acf/model/init.lua +++ /dev/null @@ -1,205 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local M = {} - -M.error = require('acf.error') -local raise = M.error.raise - -local combination = require('acf.model.combination') -M.Union = combination.Union -M.Range = combination.Range - -local fld = require('acf.model.field') -local Field = fld.Field -M.Boolean = fld.Boolean -M.Integer = fld.Integer -M.Number = fld.Number -M.String = fld.String - -local model = require('acf.model.model') -M.Action = model.Action -M.new = model.new -local to_field = model.to_field - -M.net = require('acf.model.net') - -local node = require('acf.model.node') -M.node = {} -for _, m in ipairs{ - 'List', - 'Set', - 'TreeNode', - 'has_permission', - 'insert', - 'meta', - 'mmeta', - 'path', - 'pairs', - 'ipairs' -} do M.node[m] = node[m] end - -M.permission = require('acf.model.permission') -M.register = require('acf.model.root').register -M.node.Set = require('acf.model.set').Set - -local object = require('acf.object') -local class = object.class -local isinstance = object.isinstance -local super = object.super - -local pth = require('acf.path') -local map = require('acf.util').map - - -local stringy = require('stringy') - - -M.Reference = class(Field) - -function M.Reference:init(params) - if not params.widget then params.widget = 'reference' end - super(self, M.Reference):init(params) - self.dtype = 'reference' - if not self.scope then self.scope = '/' end -end - -function M.Reference:topology(context) - local res = super(self, M.Reference):topology(context) - res[1].scope = self.scope - return res -end - -function M.Reference:abs_scope(context) - return pth.to_absolute(self.scope, node.path(context.parent)) -end - -function M.Reference:meta(context) - local res = super(self, M.Reference):meta(context) - res.scope = self:abs_scope(context) - return res -end - -function M.Reference:follow(context, value) - return context.txn:fetch(pth.rawjoin(self:abs_scope(context), value)) -end - -function M.Reference:load(context, options) - local ref = super(self, M.Reference):load(context) - return ( - (not options or options.dereference ~= false) and context.txn and ref - ) and self:follow(context, ref) or ref -end - -function M.Reference:_validate(context, value) - super(self, M.Reference):_validate(context, value) - - if value == nil then return end - - if isinstance(value, node.TreeNode) then value = node.path(value) end - - local path = context.path - if type(value) ~= 'string' then raise(path, 'Path name must be string') end - - if pth.is_absolute(value) then - local scope = self:abs_scope(context) - local prefix = scope..'/' - if not stringy.startswith(value, prefix) then - raise(path, 'Reference out of scope ('..scope..')') - end - value = value:sub(prefix:len() + 1, -1) - end - - -- assume one-level ref for now - if #pth.split(value) > 1 then - raise(path, 'Subtree references not yet supported') - end - - -- TODO check instance type - M.error.relabel(path, self.follow, self, context, value) - - return value -end - -function M.Reference:deleted(context, addr) - local target = self:load(context) - if target and node.addr(target) == addr then - -- TODO raise error for the target object - raise(context.path, 'Refers to '..addr) - end -end - - -M.Model = fld.Model - - -M.Collection = class(fld.TreeNode) - -function M.Collection:init(params, itype) - if params.create == nil then params.create = true end - super(self, M.Collection):init(params) - - assert(self.type) - self.itype = itype or node.Collection - self.iparams = { - destroy=self.destroy, - layout=self.layout, - required=self.required, - ui_member=self.ui_member - } - - self.dtype = 'collection' -end - -function M.Collection:auto_ui_name(name) - if not name then return end - if name:sub(-1, -1) ~= 's' then name = name..'s' end - return super(self, M.Collection):auto_ui_name(name) -end - -function M.Collection:load(context, options) - if not self.iparams.field then self.iparams.field = to_field(self.type) end - return super(self, M.Collection):load(context, options) -end - - -M.List = class(M.Collection) -function M.List:init(params) super(self, M.List):init(params, node.List) end - - -M.Set = class(M.Collection) -function M.Set:init(params) - if not params.widget and isinstance(params.type, M.Reference) then - params.widget = 'checkboxes' - end - super(self, M.Set):init(params, M.node.Set) -end -function M.Set.save_member(tn, k, v) node.insert(tn, v) end - - --- experimental -M.Mixed = class(M.Collection) - -function M.Mixed:init(params) - params.type = M.Mixed - super(self, M.Mixed):init(params, node.Mixed) - self.pfield = Field() -end - -function M.Mixed:topology(context) return {} end - -function M.Mixed:load(context) - local value = self.pfield:load(context) - if type(value) == 'table' then return super(self, M.Mixed):load(context) end - return value -end - -function M.Mixed:save(context, value) - if type(value) == 'table' then super(self, M.Mixed):save(context, value) - else self.pfield:save(context, value) end -end - - -return M diff --git a/acf/model/model.lua b/acf/model/model.lua deleted file mode 100644 index 6878497..0000000 --- a/acf/model/model.lua +++ /dev/null @@ -1,204 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local M = {} - -local raise = require('acf.error').raise - -local fld = require('acf.model.field') -local Field = fld.Field -local Member = fld.Member - -local node = require('acf.model.node') -local BoundMember = node.BoundMember - -local object = require('acf.object') -local class = object.class -local super = object.super -local isinstance = object.isinstance - -local pth = require('acf.path') -local util = require('acf.util') - - -local function to_member(obj, params) - if not params then params = {} end - if object.issubclass(obj, M.Model) then - params.model = obj - return fld.Model(params) - end - local res = getmetatable(obj).class and obj or obj(params) - assert(isinstance(res, Member)) - return res -end - -function M.to_field(obj, params) - local res = to_member(obj, params) - assert(isinstance(res, Field)) - return res -end - - -M.Action = class(Member) - -function M.Action:init(params) - super(self, M.Action):init(params) - - if not self.func then error('Function not defined for action') end - - if self.field then - assert(type(self.field) == 'table') - self.field = M.to_field(self.field) - self.field.addr = '/null/action' - end - - getmetatable(self).__newindex = function(t, k, v) - assert(k == 'name') - rawset(t, k, v) - if t.field then t.field.name = v end - end -end - -function M.Action:meta(context) - local res = super(self, M.Action):meta(context) - if self.field then res.arg = self.field:meta(context) end - return res -end - - -function M.new(base) - if not base then base = M.Model end - - local res = class(base) - res.members = base == node.TreeNode and {} or util.copy(base.members) - - local mt = util.copy(getmetatable(res)) - - function mt.__index(t, k) return base[k] end - - function mt.__newindex(t, k, v) - assert(v) - - local override = t[k] - if type(v) == 'table' then v = to_member(v) end - - rawset(t, k, v) - - if isinstance(v, Member) then - v.name = k - if not override then table.insert(t.members, k) end - end - end - - setmetatable(res, mt) - - if isinstance(base, M.Model) then util.setdefaults(res, base) end - - return res -end - -M.Model = M.new(node.TreeNode) - -function M.Model:init(context) - super(self, M.Model):init(context) - - local mt = getmetatable(self) - - function mt.member(name, loose, tpe) - local m = mt.class[name] - - if not tpe then tpe = Member end - if not isinstance(m, tpe) then m = nil end - - if m == nil then - if loose then return end - raise(mt.path, 'Does not exist: '..name) - end - - return BoundMember(self, name, m) - end - - local function _members(tpe) - local res = {} - for _, name in ipairs(self.members) do - local m = mt.member(name, true, tpe) - if m then table.insert(res, m) end - end - return res - end - - function mt.topology() - local res = {} - for _, f in ipairs(_members(Field)) do util.extend(res, f:topology()) end - return res - end - - function mt.load(k, options) - local v = mt.class[k] - local create = options and options.create - - if isinstance(v, Field) then - v = BoundMember(self, k, v) - if v.compute then return v:compute() end - return v:load{create=create} - end - - assert(mt.txn) - - if isinstance(v, M.Action) then - local f = v.field and BoundMember(self, k, v.field) - if create then return f and f:load{create=true} end - - return function(var) - if f then f:save(var) - elseif var ~= nil then - raise( - pth.join(mt.path, v.name), - 'Action does not accept an input argument' - ) - end - local res = v.func(self, f and f:load()) - if f then mt.txn:set(v.field.addr) end - return res - end - end - - return v - end - - if not mt.txn then return end - - - function mt.mmeta(name) return mt.member(name):meta() end - - function mt.save(k, v) return mt.member(k, false, Field):save(v) end - - local function tmeta(tpe) - return util.map(function(m) return m:meta() end, _members(tpe)) - end - - mt.meta.type = 'model' - mt.meta.fields = tmeta(Field) - mt.meta.actions = tmeta(M.Action) - - function mt.members() - return util.map(function(f) return f.name end, mt.meta.fields) - end - - function mt.validate() - for _, f in ipairs(_members(Field)) do - if not f.compute then f:validate_saved() end - end - end - - if self.has_permission then - function mt.has_permission(user, permission) - return self:has_permission(user, permission) - end - end -end - - -return M diff --git a/acf/model/net.lua b/acf/model/net.lua deleted file mode 100644 index ae82f1e..0000000 --- a/acf/model/net.lua +++ /dev/null @@ -1,96 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local M = {} - -local raise = require('acf.error').raise -local Union = require('acf.model.combination').Union - -local fld = require('acf.model.field') -local String = fld.String - -local object = require('acf.object') -local class = object.class -local super = object.super - -local update = require('acf.util').update - - -local stringy = require('stringy') - - -M.IPv4Address = class(String) - -function M.IPv4Address:validate(context, value) - super(self, M.IPv4Address):validate(context, value) - local function test(...) - if #{...} ~= 4 then return true end - for _, octet in ipairs{...} do - if tonumber(octet) > 255 then return true end - end - end - if test(value:match('^(%d+)%.(%d+)%.(%d+)%.(%d+)$')) then - raise(context.path, 'Invalid IPv4 address') - end -end - - -M.IPv6Address = class(String) - -function M.IPv6Address:validate(context, value) - super(self, M.IPv6Address):validate(context, value) - - local function invalid() raise(context.path, 'Invalid IPv6 address') end - - if value == '' then invalid() end - - local comps = stringy.split(value, ':') - if #comps < 3 then invalid() end - - local function collapse(i, ofs) - if comps[i] > '' then return end - if comps[i + ofs] > '' then invalid() end - table.remove(comps, i) - end - collapse(1, 1) - collapse(#comps, -1) - if #comps > 8 then invalid() end - - local short = false - for _, comp in ipairs(comps) do - if comp == '' then - if short then invalid() end - short = true - elseif not comp:match('^%x%x?%x?%x?$') then invalid() end - end - if ( - short and #comps == 3 and comps[2] == '' - ) or (not short and #comps < 8) then - invalid() - end -end - - -M.IPAddress = class(Union) - -function M.IPAddress:init(params) - super(self, M.IPAddress):init( - update( - params, - {types={M.IPv4Address, M.IPv6Address}, error='Invalid IP address'} - ) - ) -end - - -M.Port = class(fld.Integer) - -function M.Port:validate(context, value) - super(self, M.Port):validate(context, value) - if value < 0 or value > 65535 then raise(context.path, 'Invalid port') end -end - - -return M diff --git a/acf/model/node.lua b/acf/model/node.lua deleted file mode 100644 index 56d3416..0000000 --- a/acf/model/node.lua +++ /dev/null @@ -1,275 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local M = {} - -local raise = require('acf.error').raise - -local object = require('acf.object') -local class = object.class -local isinstance = object.isinstance -local super = object.super - -local pth = require('acf.path') -local util = require('acf.util') - - -M.BoundMember = class() - -function M.BoundMember:init(parent, name, field) - local pmt = getmetatable(parent) - local mt = {} - - function mt.__index(t, k) - local member = field[k] - if type(member) ~= 'function' then return member end - return function(self, ...) - return member( - field, - { - txn=pmt.txn, - parent=parent, - path=pth.join(pmt.path, name), - addr=pth.to_absolute( - field.addr or pth.escape(name), pmt.addr - ) - }, - ... - ) - end - end - - setmetatable(self, mt) -end - - -M.TreeNode = class() - -function M.TreeNode:init(context) - local mt = getmetatable(self) - util.update(mt, context) - - mt.dereference = true - mt.meta = {} - function mt.get(k, create) return mt.load(k, {create=create}) end - - if not mt.txn then return end - - if mt.parent then - mt.meta['ui-name'] = getmetatable(mt.parent).mmeta( - pth.name(mt.path) - )['ui-name'] - end - - function mt.save(k, v) rawset(self, k, v) end - function mt.__index(t, k) return mt.get(k) end - function mt.__newindex(t, k, v) mt.save(k, v) end - - function mt.has_permission(user, permission) - local p = permission..mt.path - if mt.txn:fetch('/auth/permissions')[p] then - return user:check_permission(p) - end - - if ({create=true, delete=true})[permission] then - permission = 'modify' - end - return M.has_permission(mt.parent, user, permission) - end - - mt.txn.validable[mt.path] = mt.addr -end - -function M.TreeNode:fetch(path, create) - if type(path) == 'string' then path = pth.split(path) end - - if #path == 0 then return self end - - local mt = getmetatable(self) - local name = path[1] - if not mt.member(name) then - raise(mt.path, 'Member does not exist: '..name) - end - - local next = mt.get(name, create) - if next == nil and (not create or #path > 1) then - raise(mt.path, 'Subordinate does not exist: '..name) - end - - table.remove(path, 1) - if #path > 0 and type(next) ~= 'table' then - raise(pth.join(mt.path, name), 'Is a primitive value') - end - - return M.TreeNode.fetch(next, path, create) -end - -function M.TreeNode:search_refs(path) - if type(path) == 'string' then path = pth.split(path) end - - if #path == 0 then return {} end - - local mt = getmetatable(self) - local name = path[1] - table.remove(path, 1) - - local function collect(name) - local next = mt.load(name) - if not next then return {} end - - local member = mt.member(name) - if member.deleted then return {member} end - - return isinstance(next, M.TreeNode) and M.TreeNode.search_refs( - next, path - ) or {} - end - - if name == pth.wildcard then - local res = {} - for _, member in ipairs(mt.members()) do - util.extend(res, collect(member)) - end - return res - end - - return collect(name) -end - - -M.Collection = class(M.TreeNode) - -function M.Collection:init(context, params) - super(self, M.Collection):init(context) - - self.init = nil - self.fetch = nil - self.search_refs = nil - - local mt = getmetatable(self) - local field = M.BoundMember(self, pth.wildcard, params.field) - - function mt.topology() return field:topology() end - - function mt.member(name) - return M.BoundMember(self, name, params.field) - end - - function mt.load(k, options) return mt.member(k):load(options) end - - if not mt.txn then return end - - mt.meta.type = 'collection' - mt.meta.members = field:meta() - mt.meta['ui-member'] = params.ui_member or mt.meta['ui-name']:gsub('s$', '') - mt.meta.widget = params.layout - - function mt.mmeta(name) - local res = util.copy(mt.meta.members) - if name ~= pth.wildcard then - res['ui-name'] = mt.meta['ui-member']..' '..name - end - return res - end - - function mt.members() return mt.txn:get(mt.addr) or {} end - - function mt.validate() - if #mt.members() > 0 then return end - if params.required then raise(mt.path, 'Collection cannot be empty') end - if params.destroy then - mt.txn:set(mt.addr) - validate(mt.parent) - end - end - - function mt.save(k, v) mt.member(k):save(v) end -end - - -M.List = class(M.Collection) - -function M.List:init(context, params) - super(self, M.List):init(context, params) - - local mt = getmetatable(self) - mt.meta.type = 'list' - - local save = mt.save - function mt.save(k, v) - assert(type(k) == 'number') - if v == nil then - local len = #mt.members() - while k < len do - mt.save(k, mt.load(k + 1, {dereference=false})) - k = k + 1 - end - end - save(k, v) - end - - function mt.insert(v, i) - local len = #mt.members() - if not i then i = len + 1 end - for j = len,i,-1 do mt.save(j + 1, mt.load(j, {dereference=false})) end - mt.save(i, v) - end -end - - --- experimental -M.Mixed = class(M.Collection) - -function M.Mixed:init(context, params) - super(self, M.Mixed):init(context, params) - - -- TODO dynamic meta: list non-leaf children - local mt = getmetatable(self) - mt.meta = {type='mixed', ['ui-name']=mt.path} - function mt.mmeta(name) - return {type='mixed', ['ui-name']=pth.join(mt.path, name)} - end -end - - -local function meta_func(attr) - return function(node, ...) - local res = getmetatable(node)[attr] - if type(res) == 'function' then return res(...) end - return res - end -end - -for _, mf in ipairs{ - 'addr', 'has_permission', 'insert', 'meta', 'mmeta', 'path', 'topology' -} do M[mf] = meta_func(mf) end - - -function M.pairs(tbl, dereference) - if not isinstance(tbl, M.TreeNode) then return pairs(tbl) end - - local mt = getmetatable(tbl) - if dereference == nil then dereference = mt.dereference end - - local res = {} - for _, member in ipairs(mt.members()) do - res[member] = mt.load(member, {dereference=dereference}) - end - return pairs(res) -end - -local function _ipairs(mt, i) - i = i + 1 - local v = mt.load(i) - if v == nil then return end - return i, v -end -function M.ipairs(tbl) - if not isinstance(tbl, M.TreeNode) then return ipairs(tbl) end - return _ipairs, getmetatable(tbl), 0 -end - - -return M diff --git a/acf/model/permission.lua b/acf/model/permission.lua deleted file mode 100644 index 271d478..0000000 --- a/acf/model/permission.lua +++ /dev/null @@ -1,22 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local M = {} - -local insert = require('acf.model.node').insert -local start_txn = require('acf.transaction') - -function M.define(path, ...) - local txn = start_txn() - local db = txn:fetch('/auth/permissions') - for _, permission in ipairs{...} do insert(db, permission..path) end - txn:commit() -end - -function M.defaults(path) - M.define(path, 'read', 'create', 'modify', 'delete') -end - -return M diff --git a/acf/model/root.lua b/acf/model/root.lua deleted file mode 100644 index 17c5cfb..0000000 --- a/acf/model/root.lua +++ /dev/null @@ -1,91 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local M = {} - -local model = require('acf.model.model') -local node = require('acf.model.node') -local object = require('acf.object') -local pth = require('acf.path') - -local util = require('acf.util') -local setdefault = util.setdefault - - -M.RootModel = model.new() - -function M.RootModel:init(txn) - object.super(self, M.RootModel):init{txn=txn, path='/', addr='/null/root'} -end - -function M.RootModel:has_permission(user, permission) - return permission == 'read' -end - -function M.RootModel:meta(path) - local obj = self:fetch(path, true) - if object.isinstance(obj, node.TreeNode) then return node.meta(obj) end - return node.mmeta(self:fetch(pth.parent(path), true), pth.name(path)) -end - - -local _topology = {} -local order = 0 - -function M.topology(addr, create) - local top = _topology - if type(addr) == 'table' then addr = util.copy(addr) - else addr = pth.split(addr) end - - local function defaults(top) - return util.setdefaults(top, {members={}, paths={}, referrers={}}) - end - - while #addr > 0 do - if create then - top = setdefault(defaults(top).members, addr[1], {}) - else - top = top.members[addr[1]] or top.members[pth.wildcard] - if not top then return end - end - table.remove(addr, 1) - end - - return defaults(top) -end - -function M.register(name, field, params) - if not params then params = {} end - params.create = true - M.RootModel[name] = model.to_field(field, params) - - local root = M.RootModel() - - for _, record in ipairs(node.topology(root:fetch(name))) do - local top = M.topology(record.addr, true) - - setdefault(top, 'order', order) - order = order + 1 - - local function set(k, v) - setdefault(top, k, v) - assert(top[k] == v) - end - - set('type', record.type) - table.insert(top.paths, record.path) - - if record.scope then - local scope = node.addr( - root:fetch(pth.to_absolute(record.scope, pth.parent(record.path))) - ) - set('scope', scope) - table.insert(M.topology(scope, true).referrers, record.path) - end - end -end - - -return M diff --git a/acf/model/set.lua b/acf/model/set.lua deleted file mode 100644 index b5b8dc3..0000000 --- a/acf/model/set.lua +++ /dev/null @@ -1,47 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local M = {} - -local TreeNode = require('acf.model.field').TreeNode -local npairs = require('acf.model.node').pairs -local object = require('acf.object') - - -M.Set = object.class(require('acf.model.node').List) - -function M.Set:init(context, params) - assert(not object.isinstance(params.field, TreeNode)) - object.super(self, M.Set):init(context, params) - - local function find(value) - for i, member in npairs(self) do - if member == value then return i end - end - end - - local mt = getmetatable(self) - mt.dereference = false - mt.meta.type = 'set' - - function mt.get(k, create) - local i = find(k) - if i then return mt.load(i) end - if create then return k end - end - - function mt.__newindex(t, k, v) - assert(v == nil) - local i = find(k) - if not i then return end - mt.save(i, nil) - end - - local insert = mt.insert - function mt.insert(v) if not find(v) then insert(v) end end -end - - -return M diff --git a/acf/modules/awall.lua b/acf/modules/awall.lua deleted file mode 100644 index 01896b4..0000000 --- a/acf/modules/awall.lua +++ /dev/null @@ -1,149 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local M = require('acf.model') -local object = require('acf.object') - - -local Direction = object.class(M.String) -function Direction:init(params) - if not params then params = {} end - params.choice = {'in', 'out'} - object.super(self, Direction):init(params) -end - - --- TODO reference types? - -local IPSet = M.new() --- TODO choices -IPSet.type = M.String{required=true} -IPSet.family = M.String{required=true, choice={'inet', 'inet6'}} --- TODO only for bitmaps -IPSet.range = M.Range{type=M.net.IPv4Address} - -local Service = M.new() -Service.proto = M.String{required=true, ui_name='Protocol'} -Service.port = M.Set{type=M.Range{type=M.net.Port}} -Service['icmp-type'] = M.String{ui_name='ICMP type'} -Service['ct-helper'] = M.String{ui_name='Connection tracking helper'} - --- TODO fw zone - -local Zone = M.new() -Zone.iface = M.Set{type=M.String, ui_name='Interfaces'} -Zone.addr = M.Set{type=M.String, ui_name='Addresses'} -Zone['route-back'] = M.Boolean{default=false} - -local LogClass = M.new() -LogClass.mode = M.String{ - required=true, default='log', choice={'log', 'nflog', 'ulog'} -} -LogClass.every = M.Integer{ui_name='Sampling frequency'} -LogClass.limit = M.Integer -LogClass.prefix = M.String -LogClass.probability = M.Number -LogClass.group = M.Integer -LogClass.range = M.Integer -LogClass.threshold = M.Integer - -local IPSetReference = M.new() -IPSetReference.name = M.Reference{scope='/awall/ipset', required=true} -IPSetReference.args = M.List{ - type=Direction, required=true, ui_name='Arguments' -} - -local Rule = M.new() -Rule['in'] = M.Set{ - type=M.Reference{scope='/awall/zone'}, ui_name='Ingress zones' -} -Rule.out = M.Set{ - type=M.Reference{scope='/awall/zone'}, ui_name='Egress zones' -} -Rule.src = M.Set{type=M.String, ui_name='Sources'} -Rule.dest = M.Set{type=M.String, ui_name='Destinations'} -Rule.ipset = M.Model{model=IPSetReference, ui_name='IP set'} -Rule.ipsec = Direction{ui_name='Require IPsec'} -Rule.service = M.Set{type=M.Reference{scope='/awall/service'}} -Rule.action = M.String{choice={'accept'}} - - -local PacketLogRule = M.new(Rule) -PacketLogRule.log = M.Reference{scope='../../log', ui_name='Log class'} - --- TODO no service field -local PolicyRule = M.new(PacketLogRule) -PolicyRule.action = M.String{ - required=true, choice={'accept', 'drop', 'reject', 'tarpit'} -} - -local Limit = M.new() -Limit.count = M.Integer -Limit.interval = M.Integer -Limit.log = M.Reference{scope='../../../log'} - -local FilterRule = M.new(PolicyRule) -FilterRule['conn-limit'] = M.Model{model=Limit, ui_name='Connection limit'} -FilterRule['flow-limit'] = M.Model{model=Limit, ui_name='Flow limit'} -FilterRule.dnat = M.net.IPv4Address{ui_name='DNAT target'} -FilterRule['no-track'] = M.Boolean{default=false, ui_name='CT bypass'} -FilterRule.related = M.List{type=Rule, ui_name='Related packet rules'} - -local DivertRule = M.new(Rule) -DivertRule['to-port'] = M.Range{type=M.net.Port, ui_name='Target port'} - -local NATRule = M.new(DivertRule) -NATRule['to-addr'] = M.Range{type=M.net.IPv4Address, ui_name='Target address'} - -local MarkRule = M.new(Rule) -MarkRule.mark = M.Integer{required=true} - -local ClampMSSRule = M.new(Rule) -ClampMSSRule.mss = M.Integer{ui_name='MSS'} - - -local AWall = M.new() --- TODO differentiate lists? -AWall.service = M.Collection{type=M.List{type=Service}} -AWall.zone = M.Collection{type=Zone} -AWall.log = M.Collection{ - type=LogClass, ui_name='Log classes', ui_member='Log class' -} -AWall.policy = M.List{type=PolicyRule, ui_name='Policies', ui_member='Policy'} -AWall['packet-log'] = M.List{ - type=PacketLogRule, ui_name='Logging', ui_member='Logging rule' -} -AWall.filter = M.List{type=FilterRule} -AWall.dnat = M.List{type=NATRule, ui_name='DNAT', ui_member='DNAT rule'} -AWall.snat = M.List{type=NATRule, ui_name='SNAT', ui_member='SNAT rule'} -AWall.mark = M.List{ - type=MarkRule, ui_name='Packet marking', ui_member='Packet marking rule' -} -AWall['route-track'] = M.List{ - type=MarkRule, ui_name='Route tracking', ui_member='Route tracking rule' -} -AWall.tproxy = M.List{ - type=DivertRule, - ui_name='Transparent proxy', - ui_member='Transparent proxy rule' -} -AWall['clamp-mss'] = M.List{ - type=ClampMSSRule, ui_name='MSS clamping', ui_member='MSS clamping rule' -} -AWall['no-track'] = M.List{ - type=Rule, ui_name='CT bypass', ui_member='Connection tracking bypass rule' -} -AWall.ipset = M.Collection{type=IPSet, ui_name='IP sets', ui_member='IP set'} - -M.register( - 'awall', - AWall, - { - addr='/json'..require('posix').getcwd()..'/config/awall.json', - ui_name='Alpine Wall' - } -) - -M.permission.defaults('/awall') diff --git a/acf/modules/generic.lua b/acf/modules/generic.lua deleted file mode 100644 index 5477bd0..0000000 --- a/acf/modules/generic.lua +++ /dev/null @@ -1,14 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - --- provided as an example, to be removed from production version - -local M = require('acf.model') - -M.register('proc', M.Mixed, {addr='/files/proc', ui_name='/proc'}) -M.permission.defaults('/proc') - -M.register('augeas', M.Mixed, {addr='/augeas'}) -M.permission.defaults('/augeas') diff --git a/acf/modules/net.lua b/acf/modules/net.lua deleted file mode 100644 index ad6ba95..0000000 --- a/acf/modules/net.lua +++ /dev/null @@ -1,25 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local M = require('acf.model') - -local Host = M.new() -Host.address = M.net.IPAddress{addr='ipaddr'} -Host.canonical = M.String{ui_name='Canonical name'} -Host.alias = M.Set{type=M.String, ui_name='Aliases', ui_member='Alias'} - -local Resolv = M.new() -Resolv.servers = M.List{type=M.net.IPAddress, addr='nameserver'} -Resolv['search-domains'] = M.List{type=M.String, addr='search/domain'} - -local Net = M.new() -Net['host-name'] = M.String{addr='/augeas/etc/hostname/hostname'} -Net.hosts = M.List{type=Host, addr='/augeas/etc/hosts'} -Net.resolver = M.Model{ - model=Resolv, addr='/augeas/etc/resolv.conf', ui_name='DNS resolver' -} - -M.register('net', Net, {ui_name='Network'}) -M.permission.defaults('/net') diff --git a/acf/object.lua b/acf/object.lua deleted file mode 100644 index 64fbb9c..0000000 --- a/acf/object.lua +++ /dev/null @@ -1,68 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local M = {} - -function M.class(base) - local cls = {} - - local mt = { - __call=function(self, ...) - local obj = {} - setmetatable(obj, {__index=cls, class=cls}) - obj:init(...) - return obj - end - } - - if not base and M.Object then base = M.Object end - if base then - cls._base = base - mt.__index = base - end - - return setmetatable(cls, mt) -end - - -M.Object = M.class() - -function M.Object:init(...) end - - -function M.super(obj, cls) - assert(M.isinstance(obj, cls)) - - local mt = {} - - function mt.__index(t, k) - local v = cls._base[k] - if type(v) ~= 'function' then return v end - return function(...) - local arg = {...} - arg[1] = obj - return v(unpack(arg)) - end - end - - return setmetatable({}, mt) -end - - -function M.issubclass(cls1, cls2) - if cls1 == cls2 then return true end - if cls1._base then return M.issubclass(cls1._base, cls2) end - return false -end - -function M.isinstance(obj, cls) - if not obj or type(obj) ~= 'table' then return false end - local mt = getmetatable(obj) - if not mt then return false end - return type(obj) == 'table' and mt.class and M.issubclass(mt.class, cls) -end - - -return M diff --git a/acf/path.lua b/acf/path.lua deleted file mode 100644 index d824141..0000000 --- a/acf/path.lua +++ /dev/null @@ -1,111 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local M = {} - -local map = require('acf.util').map - - -local up = {} -M.wildcard = {} -local special = {['..']=up, ['*']=M.wildcard} - - -function M.is_absolute(path) return path:sub(1, 1) == '/' end - - -function M.escape(comp) - for symbol, item in pairs(special) do - if comp == item then return symbol end - end - if type(comp) == 'number' then return tostring(comp) end - local res = comp:gsub('([\\/])', '\\%1') - return (special[res] or tonumber(res)) and '\\'..res or res -end - - -function M.rawjoin(p1, p2, ...) - if not p2 then return p1 end - if not M.is_absolute(p2) then p2 = '/'..p2 end - return M.rawjoin((p1 == '/' and '' or p1)..p2, ...) -end - -function M.join(parent, ...) - return M.rawjoin(parent, unpack(map(M.escape, {...}))) -end - - -function M.split(path) - local res = {} - local comp = '' - local escaped - - local function merge(s) - if s > '' then - table.insert(res, not escaped and (special[s] or tonumber(s)) or s) - end - end - - while true do - local prefix, sep, suffix = path:match('([^\\/]*)([\\/])(.*)') - if not prefix then - merge(comp..path) - return res - end - - comp = comp..prefix - if sep == '\\' then - comp = comp..suffix:sub(1, 1) - escaped = true - path = suffix:sub(2, -1) - else - merge(comp) - comp = '' - escaped = false - path = suffix - end - end -end - - -function M.is_unique(path) - for _, comp in ipairs(M.split(path)) do - if comp == M.wildcard then return false end - end - return true -end - - -function M.to_absolute(path, base) - if not M.is_absolute(path) then - path = base..(base ~= '/' and '/' or '')..path - end - local comps = M.split(path) - local i = 1 - while i <= #comps do - if comps[i] == up then - if i == 1 then error('Invalid path: '..path) end - table.remove(comps, i - 1) - table.remove(comps, i - 1) - i = i - 1 - else i = i + 1 end - end - return M.join('/', unpack(comps)) -end - - -function M.parent(path) - local comps = M.split(path) - table.remove(comps) - return M.join('/', unpack(comps)) -end - -function M.name(path) - local comps = M.split(path) - return comps[#comps] -end - - -return M diff --git a/acf/persistence/backends/augeas.lua b/acf/persistence/backends/augeas.lua deleted file mode 100644 index 4cf623a..0000000 --- a/acf/persistence/backends/augeas.lua +++ /dev/null @@ -1,153 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local topology = require('acf.model.root').topology -local pth = require('acf.path') - -local util = require('acf.util') -local copy = util.copy - - -local function aug_path(path) return pth.join('/files', unpack(path)) end - -local function strip_name(name) - return type(name) == 'string' and name:match('^[^][/=)%s]+') or name -end - -local function ipath(path, index) return path..'['..index..']' end - - -local backend = require('acf.object').class() - -function backend:init() self.aug = require('augeas').init() end - -function backend:find(path, leaf) - util.map( - function(comp) - assert( - comp == strip_name(comp) and ( - type(comp) == 'number' or not comp:match('^%.+$') - ) - ) - end, - path - ) - local res = aug_path(path) - - if #self.aug:match(res) == 0 and #path > 1 and leaf then - local index = path[#path] - if type(index) == 'number' then - local ppath = copy(path) - table.remove(ppath) - ppath = aug_path(ppath) - - if #self.aug:match(ppath) > 0 and #self.aug:match( - ppath..'/*' - ) == 0 then - return ipath(ppath, index), ppath, index - end - end - end - - return res -end - -function backend:get(path, top) - local tpe = top and top.type - local leaf = tpe and tpe ~= 'table' - local apath, mvpath = self:find(path, leaf or not tpe) - - local matches = self.aug:match(apath) - if mvpath then - assert(#matches < 2) - leaf = true - end - - if #matches == 0 then return end - - if #matches > 1 then - assert(not leaf) - local res = {} - path = copy(path) - for i, _ in ipairs(matches) do - table.insert(path, i) - if self:get(path) then table.insert(res, i) end - table.remove(path) - end - return res - end - - local value = self.aug:get(matches[1]) - if value then return tpe == 'table' and {1} or value end - if leaf then return end - - local names = {} - for _, child in ipairs(self.aug:match(apath..'/*')) do - names[strip_name(pth.name(child))] = true - end - return util.keys(names) -end - -function backend:set(mods) - local gcpaths = {} - - for _, mod in ipairs(mods) do - local path, value = unpack(mod) - - local delete = value == nil - self.aug:rm(aug_path(path)..(delete and '' or '/*')) - - local apath, mvpath, index = self:find(path, type(value) ~= 'table') - local mpath = mvpath or apath - - if not delete then - if #self.aug:match(mpath) == 0 then - - local function order(path) - return topology('/augeas'..path).order - end - local ord = order(pth.join('/', unpack(path))) - - for _, sibling in ipairs(self.aug:match(pth.parent(mpath)..'/*')) do - if order(strip_name(sibling):sub(7, -1)) > ord then - self.aug:insert(sibling, pth.name(mpath), true) - break - end - end - end - - if mvpath then - local size = #self.aug:match(mvpath) - while size < index do - self.aug:insert(ipath(mvpath, size), pth.name(mvpath)) - size = size + 1 - end - end - end - - if type(value) == 'table' then value = nil end - if not delete or mvpath then self.aug:set(apath, value) - elseif apath > '/' then apath = pth.parent(apath) end - - if delete or value == '' then gcpaths[mpath] = true end - end - - local function gc(path) - if path == '/' or #self.aug:match(path..'/*') > 0 then return end - if self.aug:rm(path.."[. = '']") > 0 then gc(pth.parent(path)) end - end - for p, _ in pairs(gcpaths) do gc(p) end - - if self.aug:save() ~= 0 then - print('Augeas save failed') - for _, ep in ipairs(self.aug:match('/augeas//error')) do - print(ep, self.aug:get(ep)) - end - assert(false) - end -end - - -return backend diff --git a/acf/persistence/backends/files.lua b/acf/persistence/backends/files.lua deleted file mode 100644 index 7d03e12..0000000 --- a/acf/persistence/backends/files.lua +++ /dev/null @@ -1,107 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local topology = require('acf.model.root').topology -local pth = require('acf.path') -local util = require('acf.persistence.util') -local copy = require('acf.util').copy - -local posix = require('posix') -local stringy = require('stringy') - - -local function get_scope(top) - if not top or top.type ~= 'reference' or not pth.is_unique(top.scope) then - return - end - - return stringy.startswith( - top.scope, '/files/' - ) and top.scope:sub(7, -1) or nil -end - - -local backend = require('acf.object').class() - --- TODO cache expiration -function backend:init() self.cache = {} end - -function backend:get(path, top) - local name = pth.join('/', unpack(path)) - - if not self.cache[name] then - local t = posix.stat(name, 'type') - if not t then return end - - if t == 'regular' then - self.cache[name] = util.read_file(name) - - elseif t == 'link' then - -- TODO handle relative symlinks - local target = posix.readlink(name) - assert(target) - - local scope = get_scope(top) - assert(scope) - scope = scope..'/' - - local slen = scope:len() - assert(target:sub(1, slen) == scope) - return target:sub(slen + 1, -1) - - elseif t == 'directory' then - local res = {} - for _, fname in ipairs(posix.dir(name)) do - if not ({['.']=true, ['..']=true})[fname] then - table.insert(res, pth.name(fname)) - end - end - return res - - else error('Unsupported file type: '..name) end - end - - return self.cache[name] -end - -function backend:set(mods) - for _, mod in pairs(mods) do - local path, value = unpack(mod) - local name = pth.join('/', unpack(path)) - - if value == nil then - print('DEL', name) - - local t = posix.stat(name, 'type') - if t == 'directory' then - assert(posix.rmdir(name)) - elseif t then assert(os.remove(name)) end - - self.cache[name] = nil - - elseif type(value) == 'table' then - assert(posix.mkdir(name)) - - else - local scope = get_scope(topology('/files'..name)) - - if scope then - -- TODO use relative symlink - os.remove(name) - assert(posix.link(pth.to_absolute(value, scope), name, true)) - - else - local file = util.open_file(name, 'w') - file:write(tostring(value)) - file:close() - - self.cache[name] = value - end - end - end -end - - -return backend diff --git a/acf/persistence/backends/json.lua b/acf/persistence/backends/json.lua deleted file mode 100644 index e0cd66e..0000000 --- a/acf/persistence/backends/json.lua +++ /dev/null @@ -1,79 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local pth = require('acf.path') -local Cache = require('acf.persistence.backends.volatile') -local util = require('acf.persistence.util') -local copy = require('acf.util').copy - -local json = require('cjson') -local posix = require('posix') - - -local backend = require('acf.object').class() - -function backend:init() - -- TODO cache expiration - self.cache = {} - self.dirty = {} -end - -function backend:split_path(path) - local fpath = copy(path) - local jpath = {} - local res - - while #fpath > 0 do - local fp = pth.join('/', unpack(fpath)) - if self.cache[fp] then return fp, jpath end - table.insert(jpath, 1, fpath[#fpath]) - table.remove(fpath) - end - - fpath = '/' - - while true do - fpath = pth.join(fpath, jpath[1]) - table.remove(jpath, 1) - - local t = posix.stat(fpath, 'type') - if t == 'link' then t = posix.stat(posix.readlink(fpath), 'type') end - if not t or not ({directory=true, regular=true})[t] then - error('File or directory does not exist: '..fpath) - end - - if t == 'regular' then return fpath, jpath end - - assert(#jpath > 0) - end -end - -function backend:get(path, top) - local fpath, jpath = self:split_path(path) - if not self.cache[fpath] then - self.cache[fpath] = Cache(json.decode(util.read_file(fpath))) - end - return self.cache[fpath]:get(jpath, top) -end - -function backend:set(mods) - local dirty = {} - - for _, mod in ipairs(mods) do - local path, value = unpack(mod) - local fpath, jpath = self:split_path(path) - self.cache[fpath]:_set(jpath, value) - dirty[fpath] = true - end - - for path, _ in pairs(dirty) do - local file = util.open_file(path, 'w') - file:write(json.encode(self.cache[path]:_get{})) - file:close() - end -end - - -return backend diff --git a/acf/persistence/backends/null.lua b/acf/persistence/backends/null.lua deleted file mode 100644 index 7ff58ce..0000000 --- a/acf/persistence/backends/null.lua +++ /dev/null @@ -1,10 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local backend = require('acf.object').class() -function backend:get(path, top) if #path == 0 then return {} end end -function backend:set(mods) end - -return backend diff --git a/acf/persistence/backends/volatile.lua b/acf/persistence/backends/volatile.lua deleted file mode 100644 index a83f7e3..0000000 --- a/acf/persistence/backends/volatile.lua +++ /dev/null @@ -1,46 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local util = require('acf.util') - - -local backend = require('acf.object').class() - -function backend:init(data) self.data = data or {} end - -function backend:_get(path) - local res = self.data - for _, comp in ipairs(path) do - if res == nil then return end - assert(type(res) == 'table') - res = res[comp] - end - return res -end - -function backend:get(path, top) - local res = self:_get(path) - return type(res) == 'table' and util.keys(res) or res -end - -function backend:_set(path, value) - if type(value) == 'table' then value = {} end - - if #path == 0 then self.data = value - - else - local comps = util.copy(path) - local name = comps[#comps] - table.remove(comps) - self:_get(comps)[name] = value - end -end - -function backend:set(mods) - for _, mod in ipairs(mods) do self:_set(unpack(mod)) end -end - - -return backend diff --git a/acf/persistence/init.lua b/acf/persistence/init.lua deleted file mode 100644 index 044d4b2..0000000 --- a/acf/persistence/init.lua +++ /dev/null @@ -1,79 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local M = {} - -local loadmods = require('acf.loader') -local topology = require('acf.model.root').topology -local object = require('acf.object') -local pth = require('acf.path') -local util = require('acf.util') - -local stringy = require('stringy') - - -local DataStore = object.class( - require('acf.transaction.backend').TransactionBackend -) - -function DataStore:init() - object.super(self, DataStore):init() - self.backends = util.map( - function(m) return m() end, - loadmods('persistence/backends') - ) -end - -function DataStore:split_path(path) - local comps = pth.split(path) - local backend = self.backends[comps[1]] - assert(backend) - table.remove(comps, 1) - return backend, comps -end - -function DataStore:get(path) - local backend, comps = self:split_path(path) - local top = topology(path) - - local res = backend:get(comps, top) - - if top then - local t = top.type - if t and res ~= nil then - local atype = type(res) - - if t == 'table' then assert(atype == 'table') - - else - assert(atype ~= 'table') - - if t == 'string' then res = tostring(res) - elseif t == 'number' then res = tonumber(res) - elseif t == 'boolean' then - res = (res and res ~= 'false') and true or false - elseif t == 'reference' then assert(atype == 'string') - else assert(false) end - end - end - end - - return util.copy(res), self.mod_time[path] or 0 -end - -function DataStore:_set_multiple(mods) - local bms = {} - - for _, mod in ipairs(mods) do - local path, value = unpack(mod) - local backend, comps = self:split_path(path) - table.insert(util.setdefault(bms, backend, {}), {comps, value}) - end - - for backend, bm in pairs(bms) do backend:set(bm) end -end - - -return DataStore() diff --git a/acf/persistence/util.lua b/acf/persistence/util.lua deleted file mode 100644 index a657411..0000000 --- a/acf/persistence/util.lua +++ /dev/null @@ -1,22 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local M = {} - -function M.open_file(path, mode) - local file = io.open(path, mode) - if not file then error('Cannot open file: '..path) end - return file -end - -function M.read_file(path) - local file = M.open_file(path) - local data = '' - for line in file:lines() do data = data..line end - file:close() - return data -end - -return M diff --git a/acf/transaction/backend.lua b/acf/transaction/backend.lua deleted file mode 100644 index 4393101..0000000 --- a/acf/transaction/backend.lua +++ /dev/null @@ -1,70 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local M = {} - -local err = require('acf.error') - --- TODO each transaction backend (i.e. persistence manager or --- transaction proper) should be implemented as a thread or have its --- internal state stored in shared storage (with appropriate locking) - - -local generation = 0 -function M.gen_number() - generation = generation + 1 - return generation -end - - -M.TransactionBackend = require('acf.object').class() - -function M.TransactionBackend:init() self.mod_time = {} end - -function M.TransactionBackend:get_if_older(path, timestamp) - local value, ts = self:get(path) - if ts > timestamp then err.raise('conflict', path) end - return value, ts -end - -function M.TransactionBackend:set(path, value) - self:set_multiple{{path, value}} -end - -function M.TransactionBackend:set_multiple(mods) - -- TODO delegate to PM backends? - local timestamp = M.gen_number() - local effective = {} - - local function tostr(s) return s ~= nil and tostring(s) or nil end - - for _, mod in ipairs(mods) do - local path, value = unpack(mod) - - if type(value) == 'table' or type( - self:get(path) - ) == 'table' or self:get(path) ~= value then - - table.insert(effective, mod) - self.mod_time[path] = timestamp - end - end - - self:_set_multiple(effective) -end - --- TODO should be atomic, mutex with set_multiple -function M.TransactionBackend:comp_and_setm(accessed, mods) - local errors = err.ErrorDict() - for path, timestamp in pairs(accessed) do - errors:collect(self.get_if_older, self, path, timestamp) - end - errors:raise() - - self:set_multiple(mods) -end - - -return M diff --git a/acf/transaction/init.lua b/acf/transaction/init.lua deleted file mode 100644 index 2ca63c5..0000000 --- a/acf/transaction/init.lua +++ /dev/null @@ -1,210 +0,0 @@ ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - -local ErrorDict = require('acf.error').ErrorDict -local root = require('acf.model.root') -local object = require('acf.object') -local pth = require('acf.path') -local be_mod = require('acf.transaction.backend') - -local util = require('acf.util') -local copy = util.copy - - -local function remove_list_value(list, value) - value = tostring(value) - - for i, v in ipairs(list) do - if tostring(v) == value then - table.remove(list, i) - return - end - end - - assert(false) -end - - -local Transaction = object.class(be_mod.TransactionBackend) - -function Transaction:init(backend, validate) - object.super(self, Transaction):init() - - self.backend = backend - - self.started = be_mod.gen_number() - self.access_time = {} - - self.added = {} - self.modified = {} - self.deleted = {} - - self.validate = validate - self.validable = {} - - self.root = root.RootModel(self) -end - -function Transaction:check() - if not self.backend then error('Transaction already committed') end -end - -function Transaction:get(path) - self:check() - - if self.deleted[path] then return nil, self.mod_time[path] end - for _, tbl in ipairs{self.added, self.modified} do - if tbl[path] ~= nil then - return copy(tbl[path]), self.mod_time[path] - end - end - - local value, timestamp = self.backend:get_if_older(path, self.started) - self.access_time[path] = timestamp - return value, timestamp -end - -function Transaction:_set_multiple(mods) - - local function set(path, value, new) - local delete = value == nil - - if self.added[path] == nil and (not new or self.deleted[path]) then - self.modified[path] = value - self.deleted[path] = delete - else self.added[path] = value end - end - - for _, mod in ipairs(mods) do - local path, value = unpack(mod) - - local ppath = pth.parent(path) - local parent = self:get(ppath) - if parent == nil then - parent = {} - self:set(ppath, parent) - end - - local name = pth.name(path) - local old = self:get(path) - - local is_table = type(value) == 'table' - local delete = value == nil - - if delete then - -- assume one-level refs for now - local top = root.topology(ppath) - if top then - local errors = ErrorDict() - for _, refs in ipairs(top.referrers) do - for _, ref in ipairs(self.root:search_refs(refs)) do - errors:collect(ref.deleted, ref, path) - end - end - errors:raise() - end - end - - if type(old) == 'table' then - if delete then - for _, child in ipairs(old) do self:set(pth.join(path, child)) end - elseif is_table then return - elseif #old > 0 then - error('Cannot assign a primitive value to non-leaf node '..path) - end - end - - if is_table then value = {} end - set(path, value, old == nil) - - local function set_parent() - set(ppath, parent) - self.mod_time[ppath] = self.mod_time[path] - end - - if old == nil and not delete then - table.insert(parent, name) - set_parent() - elseif old ~= nil and delete then - remove_list_value(parent, name) - set_parent() - end - end -end - -function Transaction:fetch(path) return self.root:fetch(path) end - -function Transaction:meta(path) return self.root:meta(path) end - -function Transaction:commit() - self:check() - - if self.validate then - local errors = ErrorDict() - for path, addr in pairs(copy(self.validable)) do - if self:get(addr) ~= nil then - errors:collect(getmetatable(self:fetch(path)).validate) - end - end - errors:raise() - end - - local mods = {} - local handled = {} - - local function insert(path, value) - assert(not handled[path]) - table.insert(mods, {path, value}) - handled[path] = true - end - - local function insert_add(path) - if not handled[path] then - local pp = pth.parent(path) - if self.added[pp] then insert_add(pp) end - insert(path, self.added[path]) - end - end - - local function insert_del(path) - if not handled[path] then - local value = self.backend:get(path) - if type(value) == 'table' then - for _, child in ipairs(value) do - local cp = pth.join(path, child) - assert(self.deleted[cp]) - insert_del(cp) - end - end - insert(path) - end - end - - for path, deleted in pairs(self.deleted) do - if deleted then insert_del(path) end - end - - for path, value in pairs(self.modified) do - if type(value) ~= 'table' then insert(path, value) end - end - - for path, _ in pairs(self.added) do insert_add(path) end - - self.backend:comp_and_setm(self.access_time, mods) - - - if not self.validate then - util.update(self.backend.validable, self.validable) - end - - self.backend = nil -end - - -local store = require('acf.persistence') - -return function(txn, defer_validation) - return Transaction(txn or store, not (txn and defer_validation)) - end diff --git a/acf/util.lua b/acf/util.lua deleted file mode 100644 index 3c93b47..0000000 --- a/acf/util.lua +++ /dev/null @@ -1,84 +0,0 @@ ---- ACF utility functions. --- --- @module acf.util - ---[[ -Copyright (c) 2012-2013 Kaarle Ritvanen -See LICENSE file for license details ---]] - - -local M = {} - ---- set default value for a key in a table unless it is already set. --- @param t the table --- @param k the key --- @param v the default value --- @return the value `t[k]` -function M.setdefault(t, k, v) - if t[k] == nil then t[k] = v end - return t[k] -end - ---- merge a table into another. --- Copy values for all keys from `src` to `dst` and optionally keep existing --- values. --- @param dst the destination table --- @param src the source table --- @param preserve a boolean. If true then will existing entries in `dst` be --- kept. --- @return the destination table, `dst` -function M.update(dst, src, preserve) - for k, v in pairs(src) do - if not preserve or dst[k] == nil then dst[k] = v end - end - return dst -end - ---- copy default vaules from one table to another. --- Copy all entries in `src` to `dst` but keep any already existing values --- in `dst`. --- @param dst the destination table --- @param src the source table containing the default values --- @return the destination table, `dst` -function M.setdefaults(dst, src) return M.update(dst, src, true) end - ---- copy a varable. --- If `var` is a table, then the table is cloned, otherwise return the value --- of `var`. --- @param var the variable to copy --- @return a clone of `var` -function M.copy(var) - return type(var) == 'table' and M.setdefaults({}, var) or var -end - ---- extend an array. --- inserts all elements in `src` into `dst` --- @param dst the destination array --- @param src the source array --- @return the destination array, `dst` -function M.extend(dst, src) - for _, v in ipairs(src) do table.insert(dst, v) end - return dst -end - ---- extract the keys of a table as an array. --- @param tbl a table --- @return an array of keys -function M.keys(tbl) - local res = {} - for k, v in pairs(tbl) do table.insert(res, k) end - return res -end - ---- map a function over a table. --- @param func a function with one argument --- @param tbl the table --- @return the transformed table -function M.map(func, tbl) - local res = {} - for k, v in pairs(tbl) do res[k] = func(v) end - return res -end - -return M diff --git a/acf2/error.lua b/acf2/error.lua new file mode 100644 index 0000000..14a2570 --- /dev/null +++ b/acf2/error.lua @@ -0,0 +1,99 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local object = require('acf2.object') +local class = object.class + +local util = require('acf2.util') + +local json = require('cjson') + + +local ErrorTable = class() + +function ErrorTable:init() self.errors = {} end + +function ErrorTable:success() return not next(self.errors) end + +function ErrorTable:raise() + if not self:success() then error(json.encode(self.errors)) end +end + + +local ErrorList = class(ErrorTable) + +function ErrorList:init(label) + object.super(self, ErrorList):init() + self.label = label +end + +function ErrorList:insert(msg) + table.insert(util.setdefault(self.errors, self.label, {}), msg) +end + + +M.ErrorDict = class(ErrorTable) + +function M.ErrorDict:collect(func, ...) + local function pack(success, ...) + local arg = {...} + return success, success and arg or arg[1] + end + + local arg = {...} + local success, res = pack( + xpcall( + function() return func(unpack(arg)) end, + function(err) + local _, _, data = err:find('.-: (.+)') + local success, res = pcall(json.decode, data) + if success and type(res) == 'table' then return res end + return data..'\n'..debug.traceback() + end + ) + ) + + if success then return unpack(res) end + + if type(res) == 'table' then + for label, errors in pairs(res) do + for _, err in ipairs(errors) do + table.insert(util.setdefault(self.errors, label, {}), err) + end + end + else error(res) end +end + + +function M.raise(label, msg) + local err = ErrorList(label) + err:insert(msg) + err:raise() +end + +function M.relabel(label, ...) + local err = M.ErrorDict() + local res = {err:collect(...)} + if err:success() then return unpack(res) end + + elist = ErrorList(label) + for lbl, el in pairs(err.errors) do + for _, e in ipairs(el) do elist:insert(lbl..': '..e) end + end + elist:raise() +end + +function M.call(...) + local err = M.ErrorDict() + local res = {err:collect(...)} + if err:success() then return true, unpack(res) end + if err.errors.system then error(err.errors.system[1]) end + return false, err.errors +end + + +return M diff --git a/acf2/init.lua b/acf2/init.lua new file mode 100644 index 0000000..eef3ff0 --- /dev/null +++ b/acf2/init.lua @@ -0,0 +1,18 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +M.model = require('acf2.model') + +require('acf2.model.aaa') +require('acf2.loader')('modules') + +M.call = require('acf2.error').call +M.object = require('acf2.object') +M.path = require('acf2.path') +M.start_txn = require('acf2.transaction') + +return M diff --git a/acf2/loader.lua b/acf2/loader.lua new file mode 100644 index 0000000..71aeabf --- /dev/null +++ b/acf2/loader.lua @@ -0,0 +1,23 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +module(..., package.seeall) + +local pth = require('acf2.path') + +local posix = require('posix') +local stringy = require('stringy') + +return function(subdir) + local comps = pth.split('acf2/'..subdir) + local res = {} + for _, modfile in ipairs(posix.dir(pth.join(unpack(comps)))) do + if stringy.endswith(modfile, '.lua') then + local name = modfile:sub(1, -5) + res[name] = require(table.concat(comps, '.')..'.'..name) + end + end + return res + end diff --git a/acf2/model/aaa.lua b/acf2/model/aaa.lua new file mode 100644 index 0000000..c8a8328 --- /dev/null +++ b/acf2/model/aaa.lua @@ -0,0 +1,87 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = require('acf2.model') +local object = require('acf2.object') + +local digest = require('crypto').digest + + +Role = M.new() +Role.permissions = M.Set{type=M.Reference{scope='../../../permissions'}} + + +local function hash_password(algorithm, salt, password) + return algorithm..'$'..salt..'$'..digest(algorithm, salt..password) +end + +local hash_pattern = '^(%w+)%$(%w+)%$%x+$' + + +local Password = object.class(M.String) + +function Password:_validate(context, value) + value = object.super(self, M.String):_validate(context, value) + if not value or value:find(hash_pattern) then return value end + + local salt = '' + for i = 1,12 do + local c = math.random(48, 109) + if c > 57 then c = c + 7 end + if c > 90 then c = c + 6 end + salt = salt..string.char(c) + end + return hash_password('sha256', salt, value) +end + + +User = M.new() +User.password = Password +User['real-name'] = M.String +User.superuser = M.Boolean{default=false} +User.roles = M.Set{type=M.Reference{scope='../../../roles'}} + +function User:check_password(password) + if not self.password then return false end + local _, _, algorithm, salt = self.password:find(hash_pattern) + if not salt then return false end + return hash_password(algorithm, salt, password) == self.password +end + +function User:check_permission(permission) + -- TODO audit trail + print('check permission', permission) + + if self.superuser then return true end + + assert(getmetatable(self).txn:fetch('/auth/permissions')[permission]) + + for _, role in M.node.pairs(self.roles, true) do + for _, p in M.node.pairs(role.permissions, true) do + if p == permission then return true end + end + end + return false +end + + +Authentication = M.new() +Authentication.users = M.Collection{type=User} +Authentication.roles = M.Collection{type=Role} +Authentication.permissions = M.Set{ + type=M.String, + addr='/volatile/aaa/permissions' +} + +M.register( + 'auth', + Authentication, + { + addr='/json'..require('posix').getcwd()..'/config/aaa.json', + ui_name='Authentication' + } +) + +M.permission.defaults('/auth') diff --git a/acf2/model/combination.lua b/acf2/model/combination.lua new file mode 100644 index 0000000..28c1570 --- /dev/null +++ b/acf2/model/combination.lua @@ -0,0 +1,50 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local err = require('acf2.error') +local raise = err.raise + +local fld = require('acf2.model.field') +local String = fld.String + +local to_field = require('acf2.model.model').to_field + +local object = require('acf2.object') +local class = object.class +local super = object.super + + +local stringy = require('stringy') + + +M.Range = class(String) + +function M.Range:init(params) + super(self, M.Range):init(params) + if not self.type then self.type = fld.Integer end +end + +function M.Range:validate(context, value) + local comps = stringy.split(value, '-') + if #comps > 2 then raise(context.path, 'Invalid range') end + for _, v in ipairs(comps) do to_field(self.type):_validate(context, v) end +end + + +M.Union = class(String) + +function M.Union:validate(context, value) + super(self, M.Union):validate(context, value) + for _, tpe in ipairs(self.types) do + local field = to_field(tpe) + if err.call(field.validate, field, context, value) then return end + end + raise(context.path, self.error or 'Invalid value') +end + + +return M diff --git a/acf2/model/field.lua b/acf2/model/field.lua new file mode 100644 index 0000000..6cbfa96 --- /dev/null +++ b/acf2/model/field.lua @@ -0,0 +1,241 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local err = require('acf2.error') +local raise = err.raise + +local node = require('acf2.model.node') + +local object = require('acf2.object') +local class = object.class +local super = object.super + +local util = require('acf2.util') + + +local function contains(list, value) + for k, v in ipairs(list) do if v == value then return true end end + return false +end + +M.Member = class() + +function M.Member:init(params) + for k, v in pairs(params or {}) do + if self[k] == nil then self[k] = v end + end +end + +function M.Member:auto_ui_name(name) + if not name then return end + return (name:sub(1, 1):upper()..name:sub(2)):gsub('-', ' ') +end + +function M.Member:meta(context) + return { + name=self.name, + description=self.description, + ['ui-name']=self.ui_name or self:auto_ui_name(self.name) + } +end + + +M.Field = class(M.Member) + +function M.Field:init(params) + super(self, M.Field):init(params) + + if self.choice and not self['ui-choice'] then + self['ui-choice'] = util.map( + function(name) return self:auto_ui_name(name) end, + self.choice + ) + end + + if not self.widget then + self.widget = self.choice and 'combobox' or 'field' + end +end + +function M.Field:meta(context) + assert(self.dtype) + local res = super(self, M.Field):meta(context) + + res.type = self.dtype + res.required = self.required + res.default = self.default + res.choice = self.choice + res.widget = self.widget + res['ui-choice'] = self['ui-choice'] + + return res +end + +function M.Field:topology(context) + return { + {path=context.path, addr=context.addr, type=self.dtype} + } +end + +function M.Field:load(context) + if not context.txn then return setmetatable({}, context) end + local value = context.txn:get(context.addr) + if value == nil then return self.default end + return value +end + +function M.Field:_validate(context, value) + if self.required and value == nil then + raise(context.path, 'Required value not set') + end + if self.choice and value ~= nil and not contains(self.choice, value) then + raise(context.path, 'Invalid value') + end + if value ~= nil then self:validate(context, value) end + return value +end + +function M.Field:validate(context, value) end + +function M.Field:save(context, value) + context.txn:set(context.addr, self:_validate(context, value)) +end + +function M.Field:validate_saved(context) + self:save(context, self:load(context)) +end + + +local Primitive = class(M.Field) + +function Primitive:validate(context, value) + local t = self.dtype + if type(value) ~= t then raise(context.path, 'Not a '..t) end +end + + +M.String = class(Primitive) + +function M.String:init(params) + super(self, M.String):init(params) + self.dtype = 'string' +end + +function M.String:validate(context, value) + super(self, M.String):validate(context, value) + if self['max-length'] and value:len() > self['max-length'] then + raise(context.path, 'Maximum length exceeded') + end + if self.pattern and not value:match('^'..self.pattern..'$') then + raise(context.path, 'Invalid value') + end +end + +function M.String:meta(context) + local res = super(self, M.String):meta(context) + res['max-length'] = self['max-length'] + return res +end + + +M.Number = class(Primitive) + +function M.Number:init(params) + super(self, M.Number):init(params) + self.dtype = 'number' +end + +function M.Number:_validate(context, value) + return super(self, M.Number):_validate( + context, + value and tonumber(value) or value + ) +end + + +M.Integer = class(M.Number) + +function M.Integer:validate(context, value) + super(self, M.Integer):validate(context, value) + if math.floor(value) ~= value then raise(context.path, 'Not an integer') end +end + + +M.Boolean = class(Primitive) + +function M.Boolean:init(params) + super(self, M.Boolean):init(params) + self.dtype = 'boolean' + self.widget = self.dtype +end + + +M.TreeNode = class(M.Field) + +function M.TreeNode:init(params) + if not params.widget then params.widget = 'link' end + super(self, M.TreeNode):init(params) +end + +function M.TreeNode:topology(context) + local res = super(self, M.TreeNode):topology(context) + res[1].type = 'table' + util.extend(res, node.topology(self:load(context, {create=true}))) + return res +end + +function M.TreeNode:load(context, options) + if context.txn and not ( + ( + options and options.create + ) or self.create or context.txn:get(context.addr) + ) then return end + return self.itype(context, self.iparams) +end + +function M.TreeNode:save(context, value) + local path = context.path + + if value == path then return end + if type(value) == 'string' then value = context.txn:fetch(value) end + if object.isinstance(value, node.TreeNode) and node.path(value) == path then + return + end + + context.txn:set(context.addr) + + if value then + if type(value) ~= 'table' then + raise(path, 'Cannot assign primitive value') + end + + context.txn:set(context.addr, {}) + local new = self:load(context, {create=true}) + + local errors = err.ErrorDict() + for k, v in node.pairs(value) do + errors:collect(self.save_member, new, k, v) + end + errors:raise() + end +end + +function M.TreeNode.save_member(node, k, v) node[k] = v end + + +M.Model = class(M.TreeNode) + +function M.Model:init(params) + super(self, M.Model):init(params) + + assert(self.model) + self.itype = self.model + self.dtype = 'model' +end + + +return M diff --git a/acf2/model/init.lua b/acf2/model/init.lua new file mode 100644 index 0000000..51cdbe8 --- /dev/null +++ b/acf2/model/init.lua @@ -0,0 +1,205 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +M.error = require('acf2.error') +local raise = M.error.raise + +local combination = require('acf2.model.combination') +M.Union = combination.Union +M.Range = combination.Range + +local fld = require('acf2.model.field') +local Field = fld.Field +M.Boolean = fld.Boolean +M.Integer = fld.Integer +M.Number = fld.Number +M.String = fld.String + +local model = require('acf2.model.model') +M.Action = model.Action +M.new = model.new +local to_field = model.to_field + +M.net = require('acf2.model.net') + +local node = require('acf2.model.node') +M.node = {} +for _, m in ipairs{ + 'List', + 'Set', + 'TreeNode', + 'has_permission', + 'insert', + 'meta', + 'mmeta', + 'path', + 'pairs', + 'ipairs' +} do M.node[m] = node[m] end + +M.permission = require('acf2.model.permission') +M.register = require('acf2.model.root').register +M.node.Set = require('acf2.model.set').Set + +local object = require('acf2.object') +local class = object.class +local isinstance = object.isinstance +local super = object.super + +local pth = require('acf2.path') +local map = require('acf2.util').map + + +local stringy = require('stringy') + + +M.Reference = class(Field) + +function M.Reference:init(params) + if not params.widget then params.widget = 'reference' end + super(self, M.Reference):init(params) + self.dtype = 'reference' + if not self.scope then self.scope = '/' end +end + +function M.Reference:topology(context) + local res = super(self, M.Reference):topology(context) + res[1].scope = self.scope + return res +end + +function M.Reference:abs_scope(context) + return pth.to_absolute(self.scope, node.path(context.parent)) +end + +function M.Reference:meta(context) + local res = super(self, M.Reference):meta(context) + res.scope = self:abs_scope(context) + return res +end + +function M.Reference:follow(context, value) + return context.txn:fetch(pth.rawjoin(self:abs_scope(context), value)) +end + +function M.Reference:load(context, options) + local ref = super(self, M.Reference):load(context) + return ( + (not options or options.dereference ~= false) and context.txn and ref + ) and self:follow(context, ref) or ref +end + +function M.Reference:_validate(context, value) + super(self, M.Reference):_validate(context, value) + + if value == nil then return end + + if isinstance(value, node.TreeNode) then value = node.path(value) end + + local path = context.path + if type(value) ~= 'string' then raise(path, 'Path name must be string') end + + if pth.is_absolute(value) then + local scope = self:abs_scope(context) + local prefix = scope..'/' + if not stringy.startswith(value, prefix) then + raise(path, 'Reference out of scope ('..scope..')') + end + value = value:sub(prefix:len() + 1, -1) + end + + -- assume one-level ref for now + if #pth.split(value) > 1 then + raise(path, 'Subtree references not yet supported') + end + + -- TODO check instance type + M.error.relabel(path, self.follow, self, context, value) + + return value +end + +function M.Reference:deleted(context, addr) + local target = self:load(context) + if target and node.addr(target) == addr then + -- TODO raise error for the target object + raise(context.path, 'Refers to '..addr) + end +end + + +M.Model = fld.Model + + +M.Collection = class(fld.TreeNode) + +function M.Collection:init(params, itype) + if params.create == nil then params.create = true end + super(self, M.Collection):init(params) + + assert(self.type) + self.itype = itype or node.Collection + self.iparams = { + destroy=self.destroy, + layout=self.layout, + required=self.required, + ui_member=self.ui_member + } + + self.dtype = 'collection' +end + +function M.Collection:auto_ui_name(name) + if not name then return end + if name:sub(-1, -1) ~= 's' then name = name..'s' end + return super(self, M.Collection):auto_ui_name(name) +end + +function M.Collection:load(context, options) + if not self.iparams.field then self.iparams.field = to_field(self.type) end + return super(self, M.Collection):load(context, options) +end + + +M.List = class(M.Collection) +function M.List:init(params) super(self, M.List):init(params, node.List) end + + +M.Set = class(M.Collection) +function M.Set:init(params) + if not params.widget and isinstance(params.type, M.Reference) then + params.widget = 'checkboxes' + end + super(self, M.Set):init(params, M.node.Set) +end +function M.Set.save_member(tn, k, v) node.insert(tn, v) end + + +-- experimental +M.Mixed = class(M.Collection) + +function M.Mixed:init(params) + params.type = M.Mixed + super(self, M.Mixed):init(params, node.Mixed) + self.pfield = Field() +end + +function M.Mixed:topology(context) return {} end + +function M.Mixed:load(context) + local value = self.pfield:load(context) + if type(value) == 'table' then return super(self, M.Mixed):load(context) end + return value +end + +function M.Mixed:save(context, value) + if type(value) == 'table' then super(self, M.Mixed):save(context, value) + else self.pfield:save(context, value) end +end + + +return M diff --git a/acf2/model/model.lua b/acf2/model/model.lua new file mode 100644 index 0000000..dc506b3 --- /dev/null +++ b/acf2/model/model.lua @@ -0,0 +1,204 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local raise = require('acf2.error').raise + +local fld = require('acf2.model.field') +local Field = fld.Field +local Member = fld.Member + +local node = require('acf2.model.node') +local BoundMember = node.BoundMember + +local object = require('acf2.object') +local class = object.class +local super = object.super +local isinstance = object.isinstance + +local pth = require('acf2.path') +local util = require('acf2.util') + + +local function to_member(obj, params) + if not params then params = {} end + if object.issubclass(obj, M.Model) then + params.model = obj + return fld.Model(params) + end + local res = getmetatable(obj).class and obj or obj(params) + assert(isinstance(res, Member)) + return res +end + +function M.to_field(obj, params) + local res = to_member(obj, params) + assert(isinstance(res, Field)) + return res +end + + +M.Action = class(Member) + +function M.Action:init(params) + super(self, M.Action):init(params) + + if not self.func then error('Function not defined for action') end + + if self.field then + assert(type(self.field) == 'table') + self.field = M.to_field(self.field) + self.field.addr = '/null/action' + end + + getmetatable(self).__newindex = function(t, k, v) + assert(k == 'name') + rawset(t, k, v) + if t.field then t.field.name = v end + end +end + +function M.Action:meta(context) + local res = super(self, M.Action):meta(context) + if self.field then res.arg = self.field:meta(context) end + return res +end + + +function M.new(base) + if not base then base = M.Model end + + local res = class(base) + res.members = base == node.TreeNode and {} or util.copy(base.members) + + local mt = util.copy(getmetatable(res)) + + function mt.__index(t, k) return base[k] end + + function mt.__newindex(t, k, v) + assert(v) + + local override = t[k] + if type(v) == 'table' then v = to_member(v) end + + rawset(t, k, v) + + if isinstance(v, Member) then + v.name = k + if not override then table.insert(t.members, k) end + end + end + + setmetatable(res, mt) + + if isinstance(base, M.Model) then util.setdefaults(res, base) end + + return res +end + +M.Model = M.new(node.TreeNode) + +function M.Model:init(context) + super(self, M.Model):init(context) + + local mt = getmetatable(self) + + function mt.member(name, loose, tpe) + local m = mt.class[name] + + if not tpe then tpe = Member end + if not isinstance(m, tpe) then m = nil end + + if m == nil then + if loose then return end + raise(mt.path, 'Does not exist: '..name) + end + + return BoundMember(self, name, m) + end + + local function _members(tpe) + local res = {} + for _, name in ipairs(self.members) do + local m = mt.member(name, true, tpe) + if m then table.insert(res, m) end + end + return res + end + + function mt.topology() + local res = {} + for _, f in ipairs(_members(Field)) do util.extend(res, f:topology()) end + return res + end + + function mt.load(k, options) + local v = mt.class[k] + local create = options and options.create + + if isinstance(v, Field) then + v = BoundMember(self, k, v) + if v.compute then return v:compute() end + return v:load{create=create} + end + + assert(mt.txn) + + if isinstance(v, M.Action) then + local f = v.field and BoundMember(self, k, v.field) + if create then return f and f:load{create=true} end + + return function(var) + if f then f:save(var) + elseif var ~= nil then + raise( + pth.join(mt.path, v.name), + 'Action does not accept an input argument' + ) + end + local res = v.func(self, f and f:load()) + if f then mt.txn:set(v.field.addr) end + return res + end + end + + return v + end + + if not mt.txn then return end + + + function mt.mmeta(name) return mt.member(name):meta() end + + function mt.save(k, v) return mt.member(k, false, Field):save(v) end + + local function tmeta(tpe) + return util.map(function(m) return m:meta() end, _members(tpe)) + end + + mt.meta.type = 'model' + mt.meta.fields = tmeta(Field) + mt.meta.actions = tmeta(M.Action) + + function mt.members() + return util.map(function(f) return f.name end, mt.meta.fields) + end + + function mt.validate() + for _, f in ipairs(_members(Field)) do + if not f.compute then f:validate_saved() end + end + end + + if self.has_permission then + function mt.has_permission(user, permission) + return self:has_permission(user, permission) + end + end +end + + +return M diff --git a/acf2/model/net.lua b/acf2/model/net.lua new file mode 100644 index 0000000..27ab262 --- /dev/null +++ b/acf2/model/net.lua @@ -0,0 +1,96 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local raise = require('acf2.error').raise +local Union = require('acf2.model.combination').Union + +local fld = require('acf2.model.field') +local String = fld.String + +local object = require('acf2.object') +local class = object.class +local super = object.super + +local update = require('acf2.util').update + + +local stringy = require('stringy') + + +M.IPv4Address = class(String) + +function M.IPv4Address:validate(context, value) + super(self, M.IPv4Address):validate(context, value) + local function test(...) + if #{...} ~= 4 then return true end + for _, octet in ipairs{...} do + if tonumber(octet) > 255 then return true end + end + end + if test(value:match('^(%d+)%.(%d+)%.(%d+)%.(%d+)$')) then + raise(context.path, 'Invalid IPv4 address') + end +end + + +M.IPv6Address = class(String) + +function M.IPv6Address:validate(context, value) + super(self, M.IPv6Address):validate(context, value) + + local function invalid() raise(context.path, 'Invalid IPv6 address') end + + if value == '' then invalid() end + + local comps = stringy.split(value, ':') + if #comps < 3 then invalid() end + + local function collapse(i, ofs) + if comps[i] > '' then return end + if comps[i + ofs] > '' then invalid() end + table.remove(comps, i) + end + collapse(1, 1) + collapse(#comps, -1) + if #comps > 8 then invalid() end + + local short = false + for _, comp in ipairs(comps) do + if comp == '' then + if short then invalid() end + short = true + elseif not comp:match('^%x%x?%x?%x?$') then invalid() end + end + if ( + short and #comps == 3 and comps[2] == '' + ) or (not short and #comps < 8) then + invalid() + end +end + + +M.IPAddress = class(Union) + +function M.IPAddress:init(params) + super(self, M.IPAddress):init( + update( + params, + {types={M.IPv4Address, M.IPv6Address}, error='Invalid IP address'} + ) + ) +end + + +M.Port = class(fld.Integer) + +function M.Port:validate(context, value) + super(self, M.Port):validate(context, value) + if value < 0 or value > 65535 then raise(context.path, 'Invalid port') end +end + + +return M diff --git a/acf2/model/node.lua b/acf2/model/node.lua new file mode 100644 index 0000000..85fc416 --- /dev/null +++ b/acf2/model/node.lua @@ -0,0 +1,275 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local raise = require('acf2.error').raise + +local object = require('acf2.object') +local class = object.class +local isinstance = object.isinstance +local super = object.super + +local pth = require('acf2.path') +local util = require('acf2.util') + + +M.BoundMember = class() + +function M.BoundMember:init(parent, name, field) + local pmt = getmetatable(parent) + local mt = {} + + function mt.__index(t, k) + local member = field[k] + if type(member) ~= 'function' then return member end + return function(self, ...) + return member( + field, + { + txn=pmt.txn, + parent=parent, + path=pth.join(pmt.path, name), + addr=pth.to_absolute( + field.addr or pth.escape(name), pmt.addr + ) + }, + ... + ) + end + end + + setmetatable(self, mt) +end + + +M.TreeNode = class() + +function M.TreeNode:init(context) + local mt = getmetatable(self) + util.update(mt, context) + + mt.dereference = true + mt.meta = {} + function mt.get(k, create) return mt.load(k, {create=create}) end + + if not mt.txn then return end + + if mt.parent then + mt.meta['ui-name'] = getmetatable(mt.parent).mmeta( + pth.name(mt.path) + )['ui-name'] + end + + function mt.save(k, v) rawset(self, k, v) end + function mt.__index(t, k) return mt.get(k) end + function mt.__newindex(t, k, v) mt.save(k, v) end + + function mt.has_permission(user, permission) + local p = permission..mt.path + if mt.txn:fetch('/auth/permissions')[p] then + return user:check_permission(p) + end + + if ({create=true, delete=true})[permission] then + permission = 'modify' + end + return M.has_permission(mt.parent, user, permission) + end + + mt.txn.validable[mt.path] = mt.addr +end + +function M.TreeNode:fetch(path, create) + if type(path) == 'string' then path = pth.split(path) end + + if #path == 0 then return self end + + local mt = getmetatable(self) + local name = path[1] + if not mt.member(name) then + raise(mt.path, 'Member does not exist: '..name) + end + + local next = mt.get(name, create) + if next == nil and (not create or #path > 1) then + raise(mt.path, 'Subordinate does not exist: '..name) + end + + table.remove(path, 1) + if #path > 0 and type(next) ~= 'table' then + raise(pth.join(mt.path, name), 'Is a primitive value') + end + + return M.TreeNode.fetch(next, path, create) +end + +function M.TreeNode:search_refs(path) + if type(path) == 'string' then path = pth.split(path) end + + if #path == 0 then return {} end + + local mt = getmetatable(self) + local name = path[1] + table.remove(path, 1) + + local function collect(name) + local next = mt.load(name) + if not next then return {} end + + local member = mt.member(name) + if member.deleted then return {member} end + + return isinstance(next, M.TreeNode) and M.TreeNode.search_refs( + next, path + ) or {} + end + + if name == pth.wildcard then + local res = {} + for _, member in ipairs(mt.members()) do + util.extend(res, collect(member)) + end + return res + end + + return collect(name) +end + + +M.Collection = class(M.TreeNode) + +function M.Collection:init(context, params) + super(self, M.Collection):init(context) + + self.init = nil + self.fetch = nil + self.search_refs = nil + + local mt = getmetatable(self) + local field = M.BoundMember(self, pth.wildcard, params.field) + + function mt.topology() return field:topology() end + + function mt.member(name) + return M.BoundMember(self, name, params.field) + end + + function mt.load(k, options) return mt.member(k):load(options) end + + if not mt.txn then return end + + mt.meta.type = 'collection' + mt.meta.members = field:meta() + mt.meta['ui-member'] = params.ui_member or mt.meta['ui-name']:gsub('s$', '') + mt.meta.widget = params.layout + + function mt.mmeta(name) + local res = util.copy(mt.meta.members) + if name ~= pth.wildcard then + res['ui-name'] = mt.meta['ui-member']..' '..name + end + return res + end + + function mt.members() return mt.txn:get(mt.addr) or {} end + + function mt.validate() + if #mt.members() > 0 then return end + if params.required then raise(mt.path, 'Collection cannot be empty') end + if params.destroy then + mt.txn:set(mt.addr) + validate(mt.parent) + end + end + + function mt.save(k, v) mt.member(k):save(v) end +end + + +M.List = class(M.Collection) + +function M.List:init(context, params) + super(self, M.List):init(context, params) + + local mt = getmetatable(self) + mt.meta.type = 'list' + + local save = mt.save + function mt.save(k, v) + assert(type(k) == 'number') + if v == nil then + local len = #mt.members() + while k < len do + mt.save(k, mt.load(k + 1, {dereference=false})) + k = k + 1 + end + end + save(k, v) + end + + function mt.insert(v, i) + local len = #mt.members() + if not i then i = len + 1 end + for j = len,i,-1 do mt.save(j + 1, mt.load(j, {dereference=false})) end + mt.save(i, v) + end +end + + +-- experimental +M.Mixed = class(M.Collection) + +function M.Mixed:init(context, params) + super(self, M.Mixed):init(context, params) + + -- TODO dynamic meta: list non-leaf children + local mt = getmetatable(self) + mt.meta = {type='mixed', ['ui-name']=mt.path} + function mt.mmeta(name) + return {type='mixed', ['ui-name']=pth.join(mt.path, name)} + end +end + + +local function meta_func(attr) + return function(node, ...) + local res = getmetatable(node)[attr] + if type(res) == 'function' then return res(...) end + return res + end +end + +for _, mf in ipairs{ + 'addr', 'has_permission', 'insert', 'meta', 'mmeta', 'path', 'topology' +} do M[mf] = meta_func(mf) end + + +function M.pairs(tbl, dereference) + if not isinstance(tbl, M.TreeNode) then return pairs(tbl) end + + local mt = getmetatable(tbl) + if dereference == nil then dereference = mt.dereference end + + local res = {} + for _, member in ipairs(mt.members()) do + res[member] = mt.load(member, {dereference=dereference}) + end + return pairs(res) +end + +local function _ipairs(mt, i) + i = i + 1 + local v = mt.load(i) + if v == nil then return end + return i, v +end +function M.ipairs(tbl) + if not isinstance(tbl, M.TreeNode) then return ipairs(tbl) end + return _ipairs, getmetatable(tbl), 0 +end + + +return M diff --git a/acf2/model/permission.lua b/acf2/model/permission.lua new file mode 100644 index 0000000..71fa1a5 --- /dev/null +++ b/acf2/model/permission.lua @@ -0,0 +1,22 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local insert = require('acf2.model.node').insert +local start_txn = require('acf2.transaction') + +function M.define(path, ...) + local txn = start_txn() + local db = txn:fetch('/auth/permissions') + for _, permission in ipairs{...} do insert(db, permission..path) end + txn:commit() +end + +function M.defaults(path) + M.define(path, 'read', 'create', 'modify', 'delete') +end + +return M diff --git a/acf2/model/root.lua b/acf2/model/root.lua new file mode 100644 index 0000000..61debec --- /dev/null +++ b/acf2/model/root.lua @@ -0,0 +1,91 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local model = require('acf2.model.model') +local node = require('acf2.model.node') +local object = require('acf2.object') +local pth = require('acf2.path') + +local util = require('acf2.util') +local setdefault = util.setdefault + + +M.RootModel = model.new() + +function M.RootModel:init(txn) + object.super(self, M.RootModel):init{txn=txn, path='/', addr='/null/root'} +end + +function M.RootModel:has_permission(user, permission) + return permission == 'read' +end + +function M.RootModel:meta(path) + local obj = self:fetch(path, true) + if object.isinstance(obj, node.TreeNode) then return node.meta(obj) end + return node.mmeta(self:fetch(pth.parent(path), true), pth.name(path)) +end + + +local _topology = {} +local order = 0 + +function M.topology(addr, create) + local top = _topology + if type(addr) == 'table' then addr = util.copy(addr) + else addr = pth.split(addr) end + + local function defaults(top) + return util.setdefaults(top, {members={}, paths={}, referrers={}}) + end + + while #addr > 0 do + if create then + top = setdefault(defaults(top).members, addr[1], {}) + else + top = top.members[addr[1]] or top.members[pth.wildcard] + if not top then return end + end + table.remove(addr, 1) + end + + return defaults(top) +end + +function M.register(name, field, params) + if not params then params = {} end + params.create = true + M.RootModel[name] = model.to_field(field, params) + + local root = M.RootModel() + + for _, record in ipairs(node.topology(root:fetch(name))) do + local top = M.topology(record.addr, true) + + setdefault(top, 'order', order) + order = order + 1 + + local function set(k, v) + setdefault(top, k, v) + assert(top[k] == v) + end + + set('type', record.type) + table.insert(top.paths, record.path) + + if record.scope then + local scope = node.addr( + root:fetch(pth.to_absolute(record.scope, pth.parent(record.path))) + ) + set('scope', scope) + table.insert(M.topology(scope, true).referrers, record.path) + end + end +end + + +return M diff --git a/acf2/model/set.lua b/acf2/model/set.lua new file mode 100644 index 0000000..215056e --- /dev/null +++ b/acf2/model/set.lua @@ -0,0 +1,47 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local TreeNode = require('acf2.model.field').TreeNode +local npairs = require('acf2.model.node').pairs +local object = require('acf2.object') + + +M.Set = object.class(require('acf2.model.node').List) + +function M.Set:init(context, params) + assert(not object.isinstance(params.field, TreeNode)) + object.super(self, M.Set):init(context, params) + + local function find(value) + for i, member in npairs(self) do + if member == value then return i end + end + end + + local mt = getmetatable(self) + mt.dereference = false + mt.meta.type = 'set' + + function mt.get(k, create) + local i = find(k) + if i then return mt.load(i) end + if create then return k end + end + + function mt.__newindex(t, k, v) + assert(v == nil) + local i = find(k) + if not i then return end + mt.save(i, nil) + end + + local insert = mt.insert + function mt.insert(v) if not find(v) then insert(v) end end +end + + +return M diff --git a/acf2/modules/awall.lua b/acf2/modules/awall.lua new file mode 100644 index 0000000..071db57 --- /dev/null +++ b/acf2/modules/awall.lua @@ -0,0 +1,149 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = require('acf2.model') +local object = require('acf2.object') + + +local Direction = object.class(M.String) +function Direction:init(params) + if not params then params = {} end + params.choice = {'in', 'out'} + object.super(self, Direction):init(params) +end + + +-- TODO reference types? + +local IPSet = M.new() +-- TODO choices +IPSet.type = M.String{required=true} +IPSet.family = M.String{required=true, choice={'inet', 'inet6'}} +-- TODO only for bitmaps +IPSet.range = M.Range{type=M.net.IPv4Address} + +local Service = M.new() +Service.proto = M.String{required=true, ui_name='Protocol'} +Service.port = M.Set{type=M.Range{type=M.net.Port}} +Service['icmp-type'] = M.String{ui_name='ICMP type'} +Service['ct-helper'] = M.String{ui_name='Connection tracking helper'} + +-- TODO fw zone + +local Zone = M.new() +Zone.iface = M.Set{type=M.String, ui_name='Interfaces'} +Zone.addr = M.Set{type=M.String, ui_name='Addresses'} +Zone['route-back'] = M.Boolean{default=false} + +local LogClass = M.new() +LogClass.mode = M.String{ + required=true, default='log', choice={'log', 'nflog', 'ulog'} +} +LogClass.every = M.Integer{ui_name='Sampling frequency'} +LogClass.limit = M.Integer +LogClass.prefix = M.String +LogClass.probability = M.Number +LogClass.group = M.Integer +LogClass.range = M.Integer +LogClass.threshold = M.Integer + +local IPSetReference = M.new() +IPSetReference.name = M.Reference{scope='/awall/ipset', required=true} +IPSetReference.args = M.List{ + type=Direction, required=true, ui_name='Arguments' +} + +local Rule = M.new() +Rule['in'] = M.Set{ + type=M.Reference{scope='/awall/zone'}, ui_name='Ingress zones' +} +Rule.out = M.Set{ + type=M.Reference{scope='/awall/zone'}, ui_name='Egress zones' +} +Rule.src = M.Set{type=M.String, ui_name='Sources'} +Rule.dest = M.Set{type=M.String, ui_name='Destinations'} +Rule.ipset = M.Model{model=IPSetReference, ui_name='IP set'} +Rule.ipsec = Direction{ui_name='Require IPsec'} +Rule.service = M.Set{type=M.Reference{scope='/awall/service'}} +Rule.action = M.String{choice={'accept'}} + + +local PacketLogRule = M.new(Rule) +PacketLogRule.log = M.Reference{scope='../../log', ui_name='Log class'} + +-- TODO no service field +local PolicyRule = M.new(PacketLogRule) +PolicyRule.action = M.String{ + required=true, choice={'accept', 'drop', 'reject', 'tarpit'} +} + +local Limit = M.new() +Limit.count = M.Integer +Limit.interval = M.Integer +Limit.log = M.Reference{scope='../../../log'} + +local FilterRule = M.new(PolicyRule) +FilterRule['conn-limit'] = M.Model{model=Limit, ui_name='Connection limit'} +FilterRule['flow-limit'] = M.Model{model=Limit, ui_name='Flow limit'} +FilterRule.dnat = M.net.IPv4Address{ui_name='DNAT target'} +FilterRule['no-track'] = M.Boolean{default=false, ui_name='CT bypass'} +FilterRule.related = M.List{type=Rule, ui_name='Related packet rules'} + +local DivertRule = M.new(Rule) +DivertRule['to-port'] = M.Range{type=M.net.Port, ui_name='Target port'} + +local NATRule = M.new(DivertRule) +NATRule['to-addr'] = M.Range{type=M.net.IPv4Address, ui_name='Target address'} + +local MarkRule = M.new(Rule) +MarkRule.mark = M.Integer{required=true} + +local ClampMSSRule = M.new(Rule) +ClampMSSRule.mss = M.Integer{ui_name='MSS'} + + +local AWall = M.new() +-- TODO differentiate lists? +AWall.service = M.Collection{type=M.List{type=Service}} +AWall.zone = M.Collection{type=Zone} +AWall.log = M.Collection{ + type=LogClass, ui_name='Log classes', ui_member='Log class' +} +AWall.policy = M.List{type=PolicyRule, ui_name='Policies', ui_member='Policy'} +AWall['packet-log'] = M.List{ + type=PacketLogRule, ui_name='Logging', ui_member='Logging rule' +} +AWall.filter = M.List{type=FilterRule} +AWall.dnat = M.List{type=NATRule, ui_name='DNAT', ui_member='DNAT rule'} +AWall.snat = M.List{type=NATRule, ui_name='SNAT', ui_member='SNAT rule'} +AWall.mark = M.List{ + type=MarkRule, ui_name='Packet marking', ui_member='Packet marking rule' +} +AWall['route-track'] = M.List{ + type=MarkRule, ui_name='Route tracking', ui_member='Route tracking rule' +} +AWall.tproxy = M.List{ + type=DivertRule, + ui_name='Transparent proxy', + ui_member='Transparent proxy rule' +} +AWall['clamp-mss'] = M.List{ + type=ClampMSSRule, ui_name='MSS clamping', ui_member='MSS clamping rule' +} +AWall['no-track'] = M.List{ + type=Rule, ui_name='CT bypass', ui_member='Connection tracking bypass rule' +} +AWall.ipset = M.Collection{type=IPSet, ui_name='IP sets', ui_member='IP set'} + +M.register( + 'awall', + AWall, + { + addr='/json'..require('posix').getcwd()..'/config/awall.json', + ui_name='Alpine Wall' + } +) + +M.permission.defaults('/awall') diff --git a/acf2/modules/generic.lua b/acf2/modules/generic.lua new file mode 100644 index 0000000..254add8 --- /dev/null +++ b/acf2/modules/generic.lua @@ -0,0 +1,14 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +-- provided as an example, to be removed from production version + +local M = require('acf2.model') + +M.register('proc', M.Mixed, {addr='/files/proc', ui_name='/proc'}) +M.permission.defaults('/proc') + +M.register('augeas', M.Mixed, {addr='/augeas'}) +M.permission.defaults('/augeas') diff --git a/acf2/modules/net.lua b/acf2/modules/net.lua new file mode 100644 index 0000000..b03ae9d --- /dev/null +++ b/acf2/modules/net.lua @@ -0,0 +1,25 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = require('acf2.model') + +local Host = M.new() +Host.address = M.net.IPAddress{addr='ipaddr'} +Host.canonical = M.String{ui_name='Canonical name'} +Host.alias = M.Set{type=M.String, ui_name='Aliases', ui_member='Alias'} + +local Resolv = M.new() +Resolv.servers = M.List{type=M.net.IPAddress, addr='nameserver'} +Resolv['search-domains'] = M.List{type=M.String, addr='search/domain'} + +local Net = M.new() +Net['host-name'] = M.String{addr='/augeas/etc/hostname/hostname'} +Net.hosts = M.List{type=Host, addr='/augeas/etc/hosts'} +Net.resolver = M.Model{ + model=Resolv, addr='/augeas/etc/resolv.conf', ui_name='DNS resolver' +} + +M.register('net', Net, {ui_name='Network'}) +M.permission.defaults('/net') diff --git a/acf2/object.lua b/acf2/object.lua new file mode 100644 index 0000000..64fbb9c --- /dev/null +++ b/acf2/object.lua @@ -0,0 +1,68 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +function M.class(base) + local cls = {} + + local mt = { + __call=function(self, ...) + local obj = {} + setmetatable(obj, {__index=cls, class=cls}) + obj:init(...) + return obj + end + } + + if not base and M.Object then base = M.Object end + if base then + cls._base = base + mt.__index = base + end + + return setmetatable(cls, mt) +end + + +M.Object = M.class() + +function M.Object:init(...) end + + +function M.super(obj, cls) + assert(M.isinstance(obj, cls)) + + local mt = {} + + function mt.__index(t, k) + local v = cls._base[k] + if type(v) ~= 'function' then return v end + return function(...) + local arg = {...} + arg[1] = obj + return v(unpack(arg)) + end + end + + return setmetatable({}, mt) +end + + +function M.issubclass(cls1, cls2) + if cls1 == cls2 then return true end + if cls1._base then return M.issubclass(cls1._base, cls2) end + return false +end + +function M.isinstance(obj, cls) + if not obj or type(obj) ~= 'table' then return false end + local mt = getmetatable(obj) + if not mt then return false end + return type(obj) == 'table' and mt.class and M.issubclass(mt.class, cls) +end + + +return M diff --git a/acf2/path.lua b/acf2/path.lua new file mode 100644 index 0000000..a31687d --- /dev/null +++ b/acf2/path.lua @@ -0,0 +1,111 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local map = require('acf2.util').map + + +local up = {} +M.wildcard = {} +local special = {['..']=up, ['*']=M.wildcard} + + +function M.is_absolute(path) return path:sub(1, 1) == '/' end + + +function M.escape(comp) + for symbol, item in pairs(special) do + if comp == item then return symbol end + end + if type(comp) == 'number' then return tostring(comp) end + local res = comp:gsub('([\\/])', '\\%1') + return (special[res] or tonumber(res)) and '\\'..res or res +end + + +function M.rawjoin(p1, p2, ...) + if not p2 then return p1 end + if not M.is_absolute(p2) then p2 = '/'..p2 end + return M.rawjoin((p1 == '/' and '' or p1)..p2, ...) +end + +function M.join(parent, ...) + return M.rawjoin(parent, unpack(map(M.escape, {...}))) +end + + +function M.split(path) + local res = {} + local comp = '' + local escaped + + local function merge(s) + if s > '' then + table.insert(res, not escaped and (special[s] or tonumber(s)) or s) + end + end + + while true do + local prefix, sep, suffix = path:match('([^\\/]*)([\\/])(.*)') + if not prefix then + merge(comp..path) + return res + end + + comp = comp..prefix + if sep == '\\' then + comp = comp..suffix:sub(1, 1) + escaped = true + path = suffix:sub(2, -1) + else + merge(comp) + comp = '' + escaped = false + path = suffix + end + end +end + + +function M.is_unique(path) + for _, comp in ipairs(M.split(path)) do + if comp == M.wildcard then return false end + end + return true +end + + +function M.to_absolute(path, base) + if not M.is_absolute(path) then + path = base..(base ~= '/' and '/' or '')..path + end + local comps = M.split(path) + local i = 1 + while i <= #comps do + if comps[i] == up then + if i == 1 then error('Invalid path: '..path) end + table.remove(comps, i - 1) + table.remove(comps, i - 1) + i = i - 1 + else i = i + 1 end + end + return M.join('/', unpack(comps)) +end + + +function M.parent(path) + local comps = M.split(path) + table.remove(comps) + return M.join('/', unpack(comps)) +end + +function M.name(path) + local comps = M.split(path) + return comps[#comps] +end + + +return M diff --git a/acf2/persistence/backends/augeas.lua b/acf2/persistence/backends/augeas.lua new file mode 100644 index 0000000..010a52b --- /dev/null +++ b/acf2/persistence/backends/augeas.lua @@ -0,0 +1,153 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local topology = require('acf2.model.root').topology +local pth = require('acf2.path') + +local util = require('acf2.util') +local copy = util.copy + + +local function aug_path(path) return pth.join('/files', unpack(path)) end + +local function strip_name(name) + return type(name) == 'string' and name:match('^[^][/=)%s]+') or name +end + +local function ipath(path, index) return path..'['..index..']' end + + +local backend = require('acf2.object').class() + +function backend:init() self.aug = require('augeas').init() end + +function backend:find(path, leaf) + util.map( + function(comp) + assert( + comp == strip_name(comp) and ( + type(comp) == 'number' or not comp:match('^%.+$') + ) + ) + end, + path + ) + local res = aug_path(path) + + if #self.aug:match(res) == 0 and #path > 1 and leaf then + local index = path[#path] + if type(index) == 'number' then + local ppath = copy(path) + table.remove(ppath) + ppath = aug_path(ppath) + + if #self.aug:match(ppath) > 0 and #self.aug:match( + ppath..'/*' + ) == 0 then + return ipath(ppath, index), ppath, index + end + end + end + + return res +end + +function backend:get(path, top) + local tpe = top and top.type + local leaf = tpe and tpe ~= 'table' + local apath, mvpath = self:find(path, leaf or not tpe) + + local matches = self.aug:match(apath) + if mvpath then + assert(#matches < 2) + leaf = true + end + + if #matches == 0 then return end + + if #matches > 1 then + assert(not leaf) + local res = {} + path = copy(path) + for i, _ in ipairs(matches) do + table.insert(path, i) + if self:get(path) then table.insert(res, i) end + table.remove(path) + end + return res + end + + local value = self.aug:get(matches[1]) + if value then return tpe == 'table' and {1} or value end + if leaf then return end + + local names = {} + for _, child in ipairs(self.aug:match(apath..'/*')) do + names[strip_name(pth.name(child))] = true + end + return util.keys(names) +end + +function backend:set(mods) + local gcpaths = {} + + for _, mod in ipairs(mods) do + local path, value = unpack(mod) + + local delete = value == nil + self.aug:rm(aug_path(path)..(delete and '' or '/*')) + + local apath, mvpath, index = self:find(path, type(value) ~= 'table') + local mpath = mvpath or apath + + if not delete then + if #self.aug:match(mpath) == 0 then + + local function order(path) + return topology('/augeas'..path).order + end + local ord = order(pth.join('/', unpack(path))) + + for _, sibling in ipairs(self.aug:match(pth.parent(mpath)..'/*')) do + if order(strip_name(sibling):sub(7, -1)) > ord then + self.aug:insert(sibling, pth.name(mpath), true) + break + end + end + end + + if mvpath then + local size = #self.aug:match(mvpath) + while size < index do + self.aug:insert(ipath(mvpath, size), pth.name(mvpath)) + size = size + 1 + end + end + end + + if type(value) == 'table' then value = nil end + if not delete or mvpath then self.aug:set(apath, value) + elseif apath > '/' then apath = pth.parent(apath) end + + if delete or value == '' then gcpaths[mpath] = true end + end + + local function gc(path) + if path == '/' or #self.aug:match(path..'/*') > 0 then return end + if self.aug:rm(path.."[. = '']") > 0 then gc(pth.parent(path)) end + end + for p, _ in pairs(gcpaths) do gc(p) end + + if self.aug:save() ~= 0 then + print('Augeas save failed') + for _, ep in ipairs(self.aug:match('/augeas//error')) do + print(ep, self.aug:get(ep)) + end + assert(false) + end +end + + +return backend diff --git a/acf2/persistence/backends/files.lua b/acf2/persistence/backends/files.lua new file mode 100644 index 0000000..70ff33e --- /dev/null +++ b/acf2/persistence/backends/files.lua @@ -0,0 +1,107 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local topology = require('acf2.model.root').topology +local pth = require('acf2.path') +local util = require('acf2.persistence.util') +local copy = require('acf2.util').copy + +local posix = require('posix') +local stringy = require('stringy') + + +local function get_scope(top) + if not top or top.type ~= 'reference' or not pth.is_unique(top.scope) then + return + end + + return stringy.startswith( + top.scope, '/files/' + ) and top.scope:sub(7, -1) or nil +end + + +local backend = require('acf2.object').class() + +-- TODO cache expiration +function backend:init() self.cache = {} end + +function backend:get(path, top) + local name = pth.join('/', unpack(path)) + + if not self.cache[name] then + local t = posix.stat(name, 'type') + if not t then return end + + if t == 'regular' then + self.cache[name] = util.read_file(name) + + elseif t == 'link' then + -- TODO handle relative symlinks + local target = posix.readlink(name) + assert(target) + + local scope = get_scope(top) + assert(scope) + scope = scope..'/' + + local slen = scope:len() + assert(target:sub(1, slen) == scope) + return target:sub(slen + 1, -1) + + elseif t == 'directory' then + local res = {} + for _, fname in ipairs(posix.dir(name)) do + if not ({['.']=true, ['..']=true})[fname] then + table.insert(res, pth.name(fname)) + end + end + return res + + else error('Unsupported file type: '..name) end + end + + return self.cache[name] +end + +function backend:set(mods) + for _, mod in pairs(mods) do + local path, value = unpack(mod) + local name = pth.join('/', unpack(path)) + + if value == nil then + print('DEL', name) + + local t = posix.stat(name, 'type') + if t == 'directory' then + assert(posix.rmdir(name)) + elseif t then assert(os.remove(name)) end + + self.cache[name] = nil + + elseif type(value) == 'table' then + assert(posix.mkdir(name)) + + else + local scope = get_scope(topology('/files'..name)) + + if scope then + -- TODO use relative symlink + os.remove(name) + assert(posix.link(pth.to_absolute(value, scope), name, true)) + + else + local file = util.open_file(name, 'w') + file:write(tostring(value)) + file:close() + + self.cache[name] = value + end + end + end +end + + +return backend diff --git a/acf2/persistence/backends/json.lua b/acf2/persistence/backends/json.lua new file mode 100644 index 0000000..809d947 --- /dev/null +++ b/acf2/persistence/backends/json.lua @@ -0,0 +1,79 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local pth = require('acf2.path') +local Cache = require('acf2.persistence.backends.volatile') +local util = require('acf2.persistence.util') +local copy = require('acf2.util').copy + +local json = require('cjson') +local posix = require('posix') + + +local backend = require('acf2.object').class() + +function backend:init() + -- TODO cache expiration + self.cache = {} + self.dirty = {} +end + +function backend:split_path(path) + local fpath = copy(path) + local jpath = {} + local res + + while #fpath > 0 do + local fp = pth.join('/', unpack(fpath)) + if self.cache[fp] then return fp, jpath end + table.insert(jpath, 1, fpath[#fpath]) + table.remove(fpath) + end + + fpath = '/' + + while true do + fpath = pth.join(fpath, jpath[1]) + table.remove(jpath, 1) + + local t = posix.stat(fpath, 'type') + if t == 'link' then t = posix.stat(posix.readlink(fpath), 'type') end + if not t or not ({directory=true, regular=true})[t] then + error('File or directory does not exist: '..fpath) + end + + if t == 'regular' then return fpath, jpath end + + assert(#jpath > 0) + end +end + +function backend:get(path, top) + local fpath, jpath = self:split_path(path) + if not self.cache[fpath] then + self.cache[fpath] = Cache(json.decode(util.read_file(fpath))) + end + return self.cache[fpath]:get(jpath, top) +end + +function backend:set(mods) + local dirty = {} + + for _, mod in ipairs(mods) do + local path, value = unpack(mod) + local fpath, jpath = self:split_path(path) + self.cache[fpath]:_set(jpath, value) + dirty[fpath] = true + end + + for path, _ in pairs(dirty) do + local file = util.open_file(path, 'w') + file:write(json.encode(self.cache[path]:_get{})) + file:close() + end +end + + +return backend diff --git a/acf2/persistence/backends/null.lua b/acf2/persistence/backends/null.lua new file mode 100644 index 0000000..118f35b --- /dev/null +++ b/acf2/persistence/backends/null.lua @@ -0,0 +1,10 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local backend = require('acf2.object').class() +function backend:get(path, top) if #path == 0 then return {} end end +function backend:set(mods) end + +return backend diff --git a/acf2/persistence/backends/volatile.lua b/acf2/persistence/backends/volatile.lua new file mode 100644 index 0000000..e33068d --- /dev/null +++ b/acf2/persistence/backends/volatile.lua @@ -0,0 +1,46 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local util = require('acf2.util') + + +local backend = require('acf2.object').class() + +function backend:init(data) self.data = data or {} end + +function backend:_get(path) + local res = self.data + for _, comp in ipairs(path) do + if res == nil then return end + assert(type(res) == 'table') + res = res[comp] + end + return res +end + +function backend:get(path, top) + local res = self:_get(path) + return type(res) == 'table' and util.keys(res) or res +end + +function backend:_set(path, value) + if type(value) == 'table' then value = {} end + + if #path == 0 then self.data = value + + else + local comps = util.copy(path) + local name = comps[#comps] + table.remove(comps) + self:_get(comps)[name] = value + end +end + +function backend:set(mods) + for _, mod in ipairs(mods) do self:_set(unpack(mod)) end +end + + +return backend diff --git a/acf2/persistence/init.lua b/acf2/persistence/init.lua new file mode 100644 index 0000000..1dca61d --- /dev/null +++ b/acf2/persistence/init.lua @@ -0,0 +1,79 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local loadmods = require('acf2.loader') +local topology = require('acf2.model.root').topology +local object = require('acf2.object') +local pth = require('acf2.path') +local util = require('acf2.util') + +local stringy = require('stringy') + + +local DataStore = object.class( + require('acf2.transaction.backend').TransactionBackend +) + +function DataStore:init() + object.super(self, DataStore):init() + self.backends = util.map( + function(m) return m() end, + loadmods('persistence/backends') + ) +end + +function DataStore:split_path(path) + local comps = pth.split(path) + local backend = self.backends[comps[1]] + assert(backend) + table.remove(comps, 1) + return backend, comps +end + +function DataStore:get(path) + local backend, comps = self:split_path(path) + local top = topology(path) + + local res = backend:get(comps, top) + + if top then + local t = top.type + if t and res ~= nil then + local atype = type(res) + + if t == 'table' then assert(atype == 'table') + + else + assert(atype ~= 'table') + + if t == 'string' then res = tostring(res) + elseif t == 'number' then res = tonumber(res) + elseif t == 'boolean' then + res = (res and res ~= 'false') and true or false + elseif t == 'reference' then assert(atype == 'string') + else assert(false) end + end + end + end + + return util.copy(res), self.mod_time[path] or 0 +end + +function DataStore:_set_multiple(mods) + local bms = {} + + for _, mod in ipairs(mods) do + local path, value = unpack(mod) + local backend, comps = self:split_path(path) + table.insert(util.setdefault(bms, backend, {}), {comps, value}) + end + + for backend, bm in pairs(bms) do backend:set(bm) end +end + + +return DataStore() diff --git a/acf2/persistence/util.lua b/acf2/persistence/util.lua new file mode 100644 index 0000000..a657411 --- /dev/null +++ b/acf2/persistence/util.lua @@ -0,0 +1,22 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +function M.open_file(path, mode) + local file = io.open(path, mode) + if not file then error('Cannot open file: '..path) end + return file +end + +function M.read_file(path) + local file = M.open_file(path) + local data = '' + for line in file:lines() do data = data..line end + file:close() + return data +end + +return M diff --git a/acf2/transaction/backend.lua b/acf2/transaction/backend.lua new file mode 100644 index 0000000..79ea83a --- /dev/null +++ b/acf2/transaction/backend.lua @@ -0,0 +1,70 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +local err = require('acf2.error') + +-- TODO each transaction backend (i.e. persistence manager or +-- transaction proper) should be implemented as a thread or have its +-- internal state stored in shared storage (with appropriate locking) + + +local generation = 0 +function M.gen_number() + generation = generation + 1 + return generation +end + + +M.TransactionBackend = require('acf2.object').class() + +function M.TransactionBackend:init() self.mod_time = {} end + +function M.TransactionBackend:get_if_older(path, timestamp) + local value, ts = self:get(path) + if ts > timestamp then err.raise('conflict', path) end + return value, ts +end + +function M.TransactionBackend:set(path, value) + self:set_multiple{{path, value}} +end + +function M.TransactionBackend:set_multiple(mods) + -- TODO delegate to PM backends? + local timestamp = M.gen_number() + local effective = {} + + local function tostr(s) return s ~= nil and tostring(s) or nil end + + for _, mod in ipairs(mods) do + local path, value = unpack(mod) + + if type(value) == 'table' or type( + self:get(path) + ) == 'table' or self:get(path) ~= value then + + table.insert(effective, mod) + self.mod_time[path] = timestamp + end + end + + self:_set_multiple(effective) +end + +-- TODO should be atomic, mutex with set_multiple +function M.TransactionBackend:comp_and_setm(accessed, mods) + local errors = err.ErrorDict() + for path, timestamp in pairs(accessed) do + errors:collect(self.get_if_older, self, path, timestamp) + end + errors:raise() + + self:set_multiple(mods) +end + + +return M diff --git a/acf2/transaction/init.lua b/acf2/transaction/init.lua new file mode 100644 index 0000000..eb316b5 --- /dev/null +++ b/acf2/transaction/init.lua @@ -0,0 +1,210 @@ +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local ErrorDict = require('acf2.error').ErrorDict +local root = require('acf2.model.root') +local object = require('acf2.object') +local pth = require('acf2.path') +local be_mod = require('acf2.transaction.backend') + +local util = require('acf2.util') +local copy = util.copy + + +local function remove_list_value(list, value) + value = tostring(value) + + for i, v in ipairs(list) do + if tostring(v) == value then + table.remove(list, i) + return + end + end + + assert(false) +end + + +local Transaction = object.class(be_mod.TransactionBackend) + +function Transaction:init(backend, validate) + object.super(self, Transaction):init() + + self.backend = backend + + self.started = be_mod.gen_number() + self.access_time = {} + + self.added = {} + self.modified = {} + self.deleted = {} + + self.validate = validate + self.validable = {} + + self.root = root.RootModel(self) +end + +function Transaction:check() + if not self.backend then error('Transaction already committed') end +end + +function Transaction:get(path) + self:check() + + if self.deleted[path] then return nil, self.mod_time[path] end + for _, tbl in ipairs{self.added, self.modified} do + if tbl[path] ~= nil then + return copy(tbl[path]), self.mod_time[path] + end + end + + local value, timestamp = self.backend:get_if_older(path, self.started) + self.access_time[path] = timestamp + return value, timestamp +end + +function Transaction:_set_multiple(mods) + + local function set(path, value, new) + local delete = value == nil + + if self.added[path] == nil and (not new or self.deleted[path]) then + self.modified[path] = value + self.deleted[path] = delete + else self.added[path] = value end + end + + for _, mod in ipairs(mods) do + local path, value = unpack(mod) + + local ppath = pth.parent(path) + local parent = self:get(ppath) + if parent == nil then + parent = {} + self:set(ppath, parent) + end + + local name = pth.name(path) + local old = self:get(path) + + local is_table = type(value) == 'table' + local delete = value == nil + + if delete then + -- assume one-level refs for now + local top = root.topology(ppath) + if top then + local errors = ErrorDict() + for _, refs in ipairs(top.referrers) do + for _, ref in ipairs(self.root:search_refs(refs)) do + errors:collect(ref.deleted, ref, path) + end + end + errors:raise() + end + end + + if type(old) == 'table' then + if delete then + for _, child in ipairs(old) do self:set(pth.join(path, child)) end + elseif is_table then return + elseif #old > 0 then + error('Cannot assign a primitive value to non-leaf node '..path) + end + end + + if is_table then value = {} end + set(path, value, old == nil) + + local function set_parent() + set(ppath, parent) + self.mod_time[ppath] = self.mod_time[path] + end + + if old == nil and not delete then + table.insert(parent, name) + set_parent() + elseif old ~= nil and delete then + remove_list_value(parent, name) + set_parent() + end + end +end + +function Transaction:fetch(path) return self.root:fetch(path) end + +function Transaction:meta(path) return self.root:meta(path) end + +function Transaction:commit() + self:check() + + if self.validate then + local errors = ErrorDict() + for path, addr in pairs(copy(self.validable)) do + if self:get(addr) ~= nil then + errors:collect(getmetatable(self:fetch(path)).validate) + end + end + errors:raise() + end + + local mods = {} + local handled = {} + + local function insert(path, value) + assert(not handled[path]) + table.insert(mods, {path, value}) + handled[path] = true + end + + local function insert_add(path) + if not handled[path] then + local pp = pth.parent(path) + if self.added[pp] then insert_add(pp) end + insert(path, self.added[path]) + end + end + + local function insert_del(path) + if not handled[path] then + local value = self.backend:get(path) + if type(value) == 'table' then + for _, child in ipairs(value) do + local cp = pth.join(path, child) + assert(self.deleted[cp]) + insert_del(cp) + end + end + insert(path) + end + end + + for path, deleted in pairs(self.deleted) do + if deleted then insert_del(path) end + end + + for path, value in pairs(self.modified) do + if type(value) ~= 'table' then insert(path, value) end + end + + for path, _ in pairs(self.added) do insert_add(path) end + + self.backend:comp_and_setm(self.access_time, mods) + + + if not self.validate then + util.update(self.backend.validable, self.validable) + end + + self.backend = nil +end + + +local store = require('acf2.persistence') + +return function(txn, defer_validation) + return Transaction(txn or store, not (txn and defer_validation)) + end diff --git a/acf2/util.lua b/acf2/util.lua new file mode 100644 index 0000000..25e885b --- /dev/null +++ b/acf2/util.lua @@ -0,0 +1,84 @@ +--- ACF2 utility functions. +-- +-- @module acf2.util + +--[[ +Copyright (c) 2012-2013 Kaarle Ritvanen +See LICENSE file for license details +--]] + + +local M = {} + +--- set default value for a key in a table unless it is already set. +-- @param t the table +-- @param k the key +-- @param v the default value +-- @return the value `t[k]` +function M.setdefault(t, k, v) + if t[k] == nil then t[k] = v end + return t[k] +end + +--- merge a table into another. +-- Copy values for all keys from `src` to `dst` and optionally keep existing +-- values. +-- @param dst the destination table +-- @param src the source table +-- @param preserve a boolean. If true then will existing entries in `dst` be +-- kept. +-- @return the destination table, `dst` +function M.update(dst, src, preserve) + for k, v in pairs(src) do + if not preserve or dst[k] == nil then dst[k] = v end + end + return dst +end + +--- copy default vaules from one table to another. +-- Copy all entries in `src` to `dst` but keep any already existing values +-- in `dst`. +-- @param dst the destination table +-- @param src the source table containing the default values +-- @return the destination table, `dst` +function M.setdefaults(dst, src) return M.update(dst, src, true) end + +--- copy a varable. +-- If `var` is a table, then the table is cloned, otherwise return the value +-- of `var`. +-- @param var the variable to copy +-- @return a clone of `var` +function M.copy(var) + return type(var) == 'table' and M.setdefaults({}, var) or var +end + +--- extend an array. +-- inserts all elements in `src` into `dst` +-- @param dst the destination array +-- @param src the source array +-- @return the destination array, `dst` +function M.extend(dst, src) + for _, v in ipairs(src) do table.insert(dst, v) end + return dst +end + +--- extract the keys of a table as an array. +-- @param tbl a table +-- @return an array of keys +function M.keys(tbl) + local res = {} + for k, v in pairs(tbl) do table.insert(res, k) end + return res +end + +--- map a function over a table. +-- @param func a function with one argument +-- @param tbl the table +-- @return the transformed table +function M.map(func, tbl) + local res = {} + for k, v in pairs(tbl) do res[k] = func(v) end + return res +end + +return M diff --git a/server.lua b/server.lua index 85688e0..8d6cfee 100644 --- a/server.lua +++ b/server.lua @@ -3,7 +3,7 @@ Copyright (c) 2012-2013 Kaarle Ritvanen See LICENSE file for license details --]] -local acf = require('acf') +local acf = require('acf2') local mnode = acf.model.node local isinstance = acf.object.isinstance @@ -183,13 +183,13 @@ return function(env) if method == 'POST' then local obj = txn:fetch(path) - if acf.object.isinstance(obj, mnode.List) then + if isinstance(obj, mnode.List) then if not mnode.has_permission(obj, user, 'create') then return 403 end local index - if not acf.object.isinstance(obj, mnode.Set) then + if not isinstance(obj, mnode.Set) then index = data.index data = data.data end -- cgit v1.2.3