Created
October 8, 2014 02:57
-
-
Save nefftd/1bb88ded0875e27964c7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- Problem: it appears to be impossible to protect a metatable from being unset | |
-- while also allowing Lua-side code to read/write metamethods. The solution is | |
-- to use a proxy. | |
-- The idea is, we use __metatable so that getmetatable() returns a proxy | |
-- object which inherits from the real metatable. User code can read/write | |
-- fields to the real metatable through this proxy. | |
-- Note: Keep __metatable intact on the proxy to (1) protect user code from | |
-- changing the behavior of the proxy and (2) protect user code from getting | |
-- access to the real metatable via getmetatable(proxy).__index | |
-- READ ONLY FORM | |
do | |
local function throw_write_error(self,k) | |
k = tostring(k) | |
error("attempt to alter protected metatable (with key '"..k.."')",2) | |
end | |
function setprotectedmt(tbl,meta) | |
local meta_proxy = setmetatable({},{ | |
__index = meta, | |
__newindex = throw_write_error, | |
__metatable = false, | |
}) | |
meta.__metatable = meta_proxy | |
return setmetatable(tbl,meta) | |
end | |
end | |
-- Test | |
local test = {} | |
local mt = { __newindex = print } | |
setprotectedmt(test,mt) | |
assert(getmetatable(test).__newindex == print) | |
assert(not pcall(function() getmetatable(test).__newindex = function() end end)) | |
assert(not pcall(setmetatable,test,nil)) | |
test.a = 1 -- prints <table: n>, 1 | |
-- WRITE NEW ONLY FORM | |
-- This form protects any fields present at the time the metatable was set. All | |
-- other fields can be written to. | |
do | |
local function throw_write_error(k) | |
k = tostring(k) | |
error("attempt to alter protected metamethod '"..k.."'",3) | |
end | |
function setprotectedmt(tbl,meta) | |
if type(meta) ~= 'table' then | |
error("bad argument #1 to 'setprotectedmt' (table expected)",2) | |
end | |
local protected = {} | |
for k in next,meta do | |
protected[k] = true | |
end | |
local meta_proxy = setmetatable({},{ | |
__index = meta, | |
__newindex = function(self,k,v) | |
if protected[k] then throw_write_error(k) end | |
meta[k] = v | |
end, | |
__metatable = false, | |
}) | |
meta.__metatable = meta_proxy | |
return setmetatable(tbl,meta) | |
end | |
end | |
-- Test | |
local test = {} | |
local mt = { __newindex = print } | |
setprotectedmt(test,mt) | |
assert(getmetatable(test).__newindex == print) | |
assert(not pcall(function() getmetatable(test).__newindex = function() end end)) | |
assert(pcall(function() getmetatable(test).__custom = true end)) -- can write custom field? | |
assert(not pcall(setmetatable,test,nil)) | |
test.a = 1 -- prints <table: n>, 1 | |
-- __protected FORM | |
-- This form expects a custom __protected on the *real* metatable. If present, | |
-- any field in that table will be protected from writes. All other fields can | |
-- be safely written. | |
do | |
local function throw_write_error(k) | |
k = tostring(k) | |
error("attempt to alter protected metamethod '"..k.."'",3) | |
end | |
function setprotectedmt(tbl,meta) | |
local meta_proxy = setmetatable({},{ | |
__index = meta, | |
__newindex = function(self,k,v) | |
if k == '__protected' or (meta.__protected and meta.__protected[k]) then | |
throw_write_error(k) | |
end | |
meta[k] = v | |
end, | |
__metatable = false, | |
}) | |
meta.__metatable = meta_proxy | |
return setmetatable(tbl,meta) | |
end | |
end | |
-- Test | |
local test = {} | |
local mt = { __newindex = print, __protected = { __newindex = true } } | |
setprotectedmt(test,mt) | |
assert(getmetatable(test).__newindex == print) | |
assert(not pcall(function() getmetatable(test).__newindex = function() end end)) | |
assert(pcall(function() getmetatable(test).__custom = true end)) -- can write custom field? | |
assert(not pcall(setmetatable,test,nil)) | |
test.a = 1 -- prints <table: n>, 1 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment