feather-ui 0.4.0

Feather UI library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
-- SPDX-License-Identifier: Apache-2.0
-- SPDX-FileCopyrightText: 2025 Fundament Software SPC <https://fundament.software>

function sandbox_impl(use_weaktables)
	local proxy_origins, proxy_owners, proxy_refs, proxy_key
	if use_weaktables then
		local weak = { __mode = "k" }
		proxy_origins, proxy_owners, proxy_refs = setmetatable({}, weak), setmetatable({}, weak), setmetatable({}, weak)
	else
		proxy_key = {}
	end

	local error_kill_flag = {}

	local proxy_get

	local root_module

	local module_proxy_of_mt = { __mode = "v" }

	if use_weaktables then
		root_module = { proxy_of = setmetatable({}, module_proxy_of_mt) }
	else
		-- can't use proxy_of without weak references
		root_module = {}
	end

	local proxy_get_origin
	if use_weaktables then
		function proxy_get_origin(obj) return proxy_origins[obj] end
	else
		function proxy_get_origin(obj)
			local info = rawget(obj, proxy_key)
			return info and info.origin
		end
	end

	local proxy_get_target
	if use_weaktables then
		function proxy_get_target(obj) return proxy_refs[obj] end
	else
		function proxy_get_target(obj)
			local info = rawget(obj, proxy_key)
			return info and info.target
		end
	end

	local proxy_get_owner
	if use_weaktables then
		function proxy_get_owner(obj) return proxy_owners[obj] end
	else
		function proxy_get_owner(obj)
			local info = rawget(obj, proxy_key)
			return info and info.owner
		end
	end

	local function translate(obj, module_src, module_dst) -- translate values across a module boundary to maintain sandboxing
		local t = type(obj)
		if t == "string" or t == "number" or t == "boolean" or t == "nil" or t == "userdata" then
			return obj -- immutable primitives and string may be passed directly
		elseif t == "table" then
			local origin = proxy_get_origin(obj)
			if origin then
				if origin == module_dst then
					return proxy_get_target(obj)
				else
					return proxy_get(proxy_get_target(obj), origin, module_dst)
				end
			end
			local mt = getmetatable(t)
			return proxy_get(obj, module_src, module_dst)
		elseif t == "function" then
			return proxy_get(obj, module_src, module_dst)
		else
			error("NYI unsupported translation between modules for " .. t .. ", this needs to be expanded")
		end
	end

	local function translate_args_inner(module_src, module_dst, count, arg, ...)
		if count > 1 then
			return translate(arg, module_src, module_dst), translate_args_inner(module_src, module_dst, count - 1, ...)
		else
			return translate(arg, module_src, module_dst)
		end
	end
	local function translate_args(module_src, module_dst, ...) -- convenience function for translating a list of args all at once
		local count = select("#", ...)
		if count == 0 then return end
		return translate_args_inner(module_src, module_dst, count, ...)
	end

	local function cloneTab(tab)
		if tab == nil then return nil end
		local clone = {}
		for k, v in pairs(tab) do
			clone[k] = v
		end
		return clone
	end

	local function sandboxed_getmetatable(obj)
		if type(obj) == "string" then --strings have a shared metatable, so this forbids the global mutable state
			return "string"
		else
			return getmetatable(obj)
		end
	end

	local proxy_mt = { -- metatable for proxies
		__metatable = "proxy",
		__index = function(self, k)
			local origin = proxy_get_origin(self)
			local owner = proxy_get_owner(self)
			return translate(proxy_get_target(self)[translate(k, owner, origin)], origin, owner)
		end,
		__newindex = function(self, k, v)
			local origin = proxy_get_origin(self)
			local owner = proxy_get_owner(self)
			proxy_get_target(self)[translate(k, owner, origin)] = translate(v, owner, origin)
		end,
		__add = function(self, other)
			local origin = proxy_get_origin(self)
			local owner = proxy_get_owner(self)
			return translate(proxy_get_target(self) + translate(other, owner, origin), origin, owner)
		end,
		__sub = function(self, other)
			local origin = proxy_get_origin(self)
			local owner = proxy_get_owner(self)
			return translate(proxy_get_target(self) - translate(other, owner, origin), origin, owner)
		end,
		__mul = function(self, other)
			local origin = proxy_get_origin(self)
			local owner = proxy_get_owner(self)
			return translate(proxy_get_target(self) * translate(other, owner, origin), origin, owner)
		end,
		__div = function(self, other)
			local origin = proxy_get_origin(self)
			local owner = proxy_get_owner(self)
			return translate(proxy_get_target(self) / translate(other, owner, origin), origin, owner)
		end,
		__mod = function(self, other)
			local origin = proxy_get_origin(self)
			local owner = proxy_get_owner(self)
			return translate(proxy_get_target(self) % translate(other, owner, origin), origin, owner)
		end,
		__pow = function(self, other)
			local origin = proxy_get_origin(self)
			local owner = proxy_get_owner(self)
			return translate(proxy_get_target(self) ^ translate(other, owner, origin), origin, owner)
		end,
		__unm = function(self)
			local origin = proxy_get_origin(self)
			local owner = proxy_get_owner(self)
			return translate(-proxy_get_target(self), origin, owner)
		end,
		__call = function(self, ...)
			local origin = proxy_get_origin(self)
			local owner = proxy_get_owner(self)
			return translate_args(origin, owner, proxy_get_target(self)(translate_args(owner, origin, ...)))
		end,
		__pairs = function(self)
			local origin = proxy_get_origin(self)
			local owner = proxy_get_owner(self)
			return translate_args(origin, owner, pairs(proxy_get_target(self)))
		end,
		__ipairs = function(self)
			local origin = proxy_get_origin(self)
			local owner = proxy_get_owner(self)
			return translate_args(origin, owner, ipairs(proxy_get_target(self)))
		end,
		__len = function(self)
			local origin = proxy_get_origin(self)
			local owner = proxy_get_owner(self)
			return translate(#proxy_get_target(self), origin, owner)
		end,

		__tostring = function(self) return tostring(proxy_get_target(self)) end,
		__eq = function(self, other)
			return rawequal(proxy_get_origin(self), proxy_get_origin(other))
				and proxy_get_target(self) == proxy_get_target(other)
		end,
		--[[__uncall = function(self)
    return uncall(proxy_origins[self])
  end,]]
	}
	local proxy_private_mt = { -- metatable for proxies that hide their state
		__metatable = "proxy",
		__index = function(self, k)
			local origin = proxy_get_origin(self)
			local owner = proxy_get_owner(self)
			return translate(getmetatable(proxy_get_target(self)).__index[translate(k, owner, origin)], origin, owner)
		end,
		__newindex = function(self, k, v) error("tried to set a field on a protected object") end,
		__add = proxy_mt.__add,
		__sub = proxy_mt.__sub,
		__mul = proxy_mt.__mul,
		__div = proxy_mt.__div,
		__mod = proxy_mt.__mod,
		__pow = proxy_mt.__pow,
		__unm = proxy_mt.__unm,
		__call = proxy_mt.__call,
		__pairs = proxy_mt.__pairs,
		__len = proxy_mt.__len,
		__tostring = proxy_mt.__tostring,
		__eq = proxy_mt.__eq,
		__uncall = proxy_mt.__uncall,
	}
	local proxy_opaque_mt = { -- metatable for proxies that block access from external modules
		__metatable = "proxy",
		__index = function(self, k) error("tried to get a field on a protected object") end,
		__newindex = function(self, k, v) error("tried to set a field on a protected object") end,
		-- __call = function(self, ...) error "tried to call a protected object" end,
		-- __tostring = proxy_mt.__tostring,
		__eq = proxy_mt.__eq,
		-- __uncall = proxy_mt.__uncall,
	}

	local sandboxed_pairs
	local sandboxed_next
	if use_weaktables then
		sandboxed_next = next
	else
		function sandboxed_next(tab, k)
			local newk, v = next(tab, k)
			if rawequal(newk, proxy_key) then
				return next(tab, newk)
			else
				return newk, v
			end
		end
	end

	do
		local _, tab, _ = pairs(setmetatable({ custom = false }, {
			__pairs = function(_) return next, { custom = true }, nil end,
		}))
		if tab.custom then
			if not use_weaktables then
				-- local function __pairs(self)
				--   return sandboxed_next, self, nil
				-- end
				-- proxy_mt.__pairs = __pairs
				-- proxy_private_mt.__pairs = __pairs
				-- proxy_opaque_mt.__pairs = __pairs
			end
			sandboxed_pairs = pairs
		else
			if use_weaktables then
				sandboxed_pairs = pairs
			else
				function sandboxed_pairs(obj) return sandboxed_next, obj, nil end
			end
		end
	end

	if use_weaktables then
		function proxy_get(object, module_src, module_dst) -- proxy an object from the source module to the dest, reusing a proxy if possible
			if module_dst.proxy_of[object] then return module_dst.proxy_of[object] end
			local proxy
			local ot = type(object)
			if ot == "function" then
				proxy = function(...)
					return translate_args(module_src, module_dst, object(translate_args(module_dst, module_src, ...)))
				end
			else
				local omt = getmetatable(object)
				local mt = proxy_mt
				if omt then
					if omt.__proxy_private == true then mt = proxy_private_mt end
					if omt.__proxy_opaque == true then mt = proxy_opaque_mt end
				end
				proxy = setmetatable({}, mt)
			end
			proxy_origins[proxy] = module_src
			proxy_owners[proxy] = module_dst
			proxy_refs[proxy] = object
			module_dst.proxy_of[object] = proxy
			return proxy
		end
	else
		function proxy_get(object, module_src, module_dst) -- proxy an object from the source module to the dest, but without weak tables it can't reuse proxies
			local omt = getmetatable(object)
			local mt = proxy_mt
			if omt then
				if omt.__proxy_private == true then mt = proxy_private_mt end
				if omt.__proxy_opaque == true then mt = proxy_opaque_mt end
			end
			local proxy =
				setmetatable({ [proxy_key] = { origin = module_src, owner = module_dst, target = object } }, mt)
			return proxy
		end
	end

	local function pcall_handler(ok, err, ...) -- Automatically propagate kill codes through error handling to prevent using any protected mode to avoid a kill signal.
		if not ok and rawequal(err, error_kill_flag) then error(err) end
		return ok, err, ...
	end

	local sandboxed_pcall = function(func, ...) return pcall_handler(pcall(func, ...)) end

	local sandboxed_xpcall = function(func, msgh, ...)
		local function wrapped_handler(err)
			if rawequal(err, error_kill_flag) then
				return err
			else
				return msgh(err)
			end
		end
		return pcall_handler(xpcall(func, wrapped_handler, ...))
	end

	local function env_create(module)
		local env
		env = {
			assert = assert,
			-- collectgarbage is forbidden to prevent messing with memory tracking
			-- dofile is forbidden because there is no default filesystem access
			error = error, -- this will probably need to be sandboxed in the future to allow errors with nonstring values
			-- _G added later
			getmetatable = sandboxed_getmetatable,
			ipairs = ipairs,
			load = function(ld, source, mode, subenv)
				if not source then source = "=(load)" end
				if not subenv then subenv = env end
				mode = "t"
				return load(ld, source, mode, subenv)
			end,
			-- loadfile is forbidden because there is no default filesystem access
			next = sandboxed_next,
			pairs = sandboxed_pairs,
			pcall = sandboxed_pcall,
			print = print,
			rawequal = rawequal,
			rawget = rawget,
			rawlen = rawlen,
			rawset = rawset,
			select = select,
			setmetatable = setmetatable,
			tonumber = tonumber,
			tostring = tostring,
			type = type,
			_VERSION = _VERSION,
			xpcall = sandboxed_xpcall,
			io = { write = io.write, flush = io.flush },

			coroutine = {
				create = coroutine.create,
				-- polyglot, so include this even if it's on the wrong version since it will just be empty
				---@diagnostic disable-next-line:deprecated
				isyieldable = coroutine.isyieldable,
				resume = function(co, ...)
					return translate_args(
						root_module,
						module,
						coroutine.resume(co, translate_args(module, root_module, ...))
					)
				end,
				running = coroutine.running,
				status = coroutine.status,
				wrap = function(f)
					local function wrapped_f(...)
						return translate_args(module, root_module, f(translate_args(root_module, module, ...)))
					end
					local co = coroutine.create(wrapped_f)
					return function(...)
						return translate_args(
							root_module,
							module,
							coroutine.resume(translate_args(module, root_module, ...) --[[@as thread]])
						)
					end
				end,
				yield = function(...)
					return translate_args(
						root_module,
						module,
						coroutine.yield(translate_args(module, root_module, ...))
					)
				end,
			},

			-- require must be set up to permit loading whitelisted packages from source and retrieving injected dependencies on other modules
			-- package configuration table is forbidden because it of filesystem and mutable global state

			string = cloneTab(string), -- string library is safe
			---@diagnostic disable-next-line:undefined-global
			utf8 = cloneTab(utf8), -- if utf8 lib is available, it is fine
			table = cloneTab(table),
			bit = cloneTab(bit),
			math = cloneTab(math),
			-- io is forbidden
			-- os is forbidden
			-- debug is forbidden pending a sandboxed version
			lpeg = cloneTab(lpeg), -- lpeg can't accept proxies of functions so it has to be inside the security boundary
		}
		env._G = env
		return env
	end

	---Create a new module from specified source
	---@param code string
	---@param source string
	---@param ... unknown
	---@return function|nil module the loaded module
	---@return nil|string err the resulting error
	local function module_create(code, source, injected_deps)
		local module = { proxy_of = setmetatable({}, module_proxy_of_mt) }
		local env = env_create(module)
		for k, v in pairs(injected_deps) do
			env[k] = v
		end
		local fn, err = env.load(code, source or "=(module_create)")
		return translate_args(module, root_module, fn, err)
	end

	return module_create
end