Recursive Read Only Tables

lua-users home
wiki

Showing revision 4
By: VeLoSo

Lua Version: 5.x

Prerequisites: Familiarity with metamethods (see MetamethodsTutorial)

In the spirit of ReadOnlyTables, I needed a way to provide access control in a multi-user Lua system. Notably, it is a requirement that users have read-only access to complex data structures, and should not be able to modify them in any way.

It is a goal that even savvy Lua users should not be able to circumvent the protection.

The (as yet mostly untested) solution I've come up with follows:

-- 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 and __pairs are set in each read-only table's metatable to support the cooresponding extensions to the standard library. Other than the metamethods, this module only exports ropairs, a version of pairs for read-only tables (which uses __pairs), and make_read_only, a constructor of read-only tables.

I would prefer to cache the read-only version of each table so as to avoid making redundant copies (and support equality testing of read-only tables) but as RiciLake points out at GarbageCollectingWeakTables, cacheing recursive data is problematic in Lua. Happily, read-only tables are rather lightweight, so this isn't as big as a problem as it could be.

In my implementation, I will probably modify the line local new={} in make_read_only to generate a new userdata instead, to properly catch attempts to treat a read-only table like a standard table (using, for instance, pairs or ipairs or rawset or table.insert.

I'm posting this early in hopes of getting some feedback. I need a solution to this problem, and this turned out to be easier to code in pure Lua than I had hoped. But any improvements would be welcomed.

A sample usage follows.

Protection seems to work fine:

> 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: ...

Unfortunately, each access of a subtable returns a freshly-created read-only table. If a table is a key in a read-only table, you can't pull it out of the read-only table, but you can still use it as a key if you have access to it by other means.

> 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: ...

I want to strengthen this to honor the wrapped table's __index and __pairs metatables, and hopefully come up with a caching strategy that doesn't break garbage collection. Maybe I'll post RecursiveReadOnlyTablesTwo? one day.

-- VeLoSo


RecentChanges · preferences
edit · history · current revision
Edited January 6, 2007 4:17 pm GMT (diff)