Recursive Read Only Tables

lua-users home
wiki

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

I saw your implementation and thought I would see if I could implement something that got around a few of the catches you were experiencing. Some of the catches in your case break the code in my case. One such case is that each subtable access returns a freshly-created read-only table. I also was afraid of breaking garbage collection since the objects that I want to make read-only are created and destroyed extremely often. I do not have the experience that I would like to working with metatables and these kinds of constructs so any advice or corrections would be much appreciated.

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

And some tests. Note that the table.insert overwrites the read-only copy of the data, but does not corrupt the actual data source. Anyone know a way around this?

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

I am not sure how safe this is from an expert who is given a read-only table, but it seems that since the table is only ever referenced indirectly through a closure, it should be completely safe from damage. Also, as far as I can tell, this will not break garbage collection. Let me know what you think.

-- ZachDwiel?


RecentChanges · preferences
edit · history
Last edited April 19, 2007 7:51 am GMT (diff)