递归只读表 |
|
Lua 版本: 5.x
先决条件: 熟悉元方法(参见 MetamethodsTutorial)
秉承 ReadOnlyTables 的精神,我需要一种方法在多用户 Lua 系统中提供访问控制。特别是,要求用户对复杂数据结构具有只读访问权限,并且不得以任何方式修改它们。
目标是即使是精通 Lua 的用户也无法规避此保护。
我提出的(目前尚未完全测试的)解决方案如下:
-- cache the metatables of all existing read-only tables, -- so our functions can get to them, but user code can't local metatable_cache = setmetatable({}, {__mode='k'}) local function make_getter(real_table) local function getter(dummy, key) local ans=real_table[key] if type(ans)=='table' and not metatable_cache[ans] then ans = make_read_only(ans) end return ans end return getter end local function setter(dummy) error("attempt to modify read-only table", 2) end local function make_pairs(real_table) local function pairs() local key, value, real_key = nil, nil, nil local function nexter() -- both args dummy key, value = next(real_table, real_key) real_key = key if type(key)=='table' and not metatable_cache[key] then key = make_read_only(key) end if type(value)=='table' and not metatable_cache[value] then value = make_read_only(value) end return key, value end return nexter -- values 2 and 3 dummy end return pairs end function make_read_only(t) local new={} local mt={ __metatable = "read only table", __index = make_getter(t), __newindex = setter, __pairs = make_pairs(t), __type = "read-only table"} setmetatable(new, mt) metatable_cache[new]=mt return new end function ropairs(t) local mt = metatable_cache[t] if mt==nil then error("bad argument #1 to 'ropairs' (read-only table expected, got " .. type(t) .. ")", 2) end return mt.__pairs() end
__type 和 __pairs 在每个只读表的元表中设置,以支持对标准库的相应扩展。除了元方法之外,此模块仅导出 ropairs(适用于只读表的 pairs 版本,它使用 __pairs)和 make_read_only(只读表的构造函数)。
我倾向于缓存每个只读表的版本,以避免制作冗余副本(并支持只读表的相等性测试),但正如 RiciLake 在 GarbageCollectingWeakTables 中指出的那样,缓存递归数据在 Lua 中是个难题。幸运的是,只读表相当轻量,所以这不像可能的那样是个大问题。
在我的实现中,我可能会修改 make_read_only 中的行 local new={} ,使其生成一个新的 userdata,以正确捕获将只读表视为标准表的尝试(例如,使用 pairs、ipairs、rawset 或 table.insert)。
我提前发布此信息,希望获得一些反馈。我需要解决这个问题,并且发现用纯 Lua 实现比我预期的更容易。但任何改进都将受到欢迎。
以下是示例用法。
保护似乎运行良好
> tab = { one=1, two=2, sub={} }
> tab.sub[{}]={}
> rotab=make_read_only(tab)
> =rotab.two
2
> =rotab.three
nil
> rotab.two='two'
stdin:1: attempt to modify read-only table
stack traceback: ...
> rotab.sub.foo='bar'
stdin:1: attempt to modify read-only table
stack traceback: ...
不幸的是,每次访问子表都会返回一个新创建的只读表。如果一个表是只读表中的键,您无法从中提取它,但如果您可以通过其他方式访问它,您仍然可以使用它作为键。
> key={'Lua!'}
> rot=make_read_only {[key]=12345}
> for k,v in ropairs(rot) do print (k,v) end
table: 003DD990 12345
> for k,v in ropairs(rot) do print (k,v) end
table: 00631568 12345
> =rot[key]
12345
> for k,_ in ropairs(rot) do k[2]='Woot!' end
stdin:1: attempt to modify read-only table
stack traceback: ...
我希望加强这一点,以尊重被包装表的 __index 和 __pairs 元表,并希望能找到一个不破坏垃圾回收的缓存策略。也许有一天我会发布 RecursiveReadOnlyTablesTwo?
-- VeLoSo
我看了你的实现,并想看看我是否能实现一些东西来绕过你遇到的几个陷阱。你的一些陷阱会破坏我的代码。其中一种情况是,每次子表访问都会返回一个新创建的只读表。我也担心破坏垃圾回收,因为我想使其只读的对象创建和销毁得非常频繁。我在处理元表和这类构造方面没有我想要的经验,因此任何建议或更正都将非常感激。
-- recursive read-only definition function readOnly(t) for x, y in pairs(t) do if type(x) == "table" then if type(y) == "table" then t[readOnly(x)] = readOnly[y] else t[readOnly(x)] = y end elseif type(y) == "table" then t[x] = readOnly(y) end end local proxy = {} local mt = { -- hide the actual table being accessed __metatable = "read only table", __index = function(tab, k) return t[k] end, __pairs = function() return pairs(t) end, __newindex = function (t,k,v) error("attempt to update a read-only table", 2) end } setmetatable(proxy, mt) return proxy end local oldpairs = pairs function pairs(t) local mt = getmetatable(t) if mt==nil then return oldpairs(t) elseif type(mt.__pairs) ~= "function" then return oldpairs(t) end return mt.__pairs() end
> local test = {"a", "b", c = 12, {x = 1, y = 2}}
> test = readOnly(test)
> for k, v in pairs(test) do
> print(k, v)
> end
1 a
2 b
3 table: 0x806f34a0
c 12
> =test[1]
a
> -- anyone know how to break this one? The code above by VeLoSo also lets this through
> table.insert(test, "blah")
> =test[1]
blah
> test.new = 3
stdin:1: attempt to modify read-only table
stack traceback: ...
> test[3] = "something"
stdin:1: attempt to modify read-only table
stack traceback: ...
> test[3].new = "something"
stdin:1: attempt to modify read-only table
stack traceback: ...
我不确定这对于一个拥有只读表的专家来说有多安全,但似乎由于该表仅通过闭包间接引用,因此它应该完全不受损坏。此外,据我所知,这不会破坏垃圾回收。请告诉我你的想法。
-- ZachDwiel?