first commit

This commit is contained in:
Finn
2026-01-16 21:47:04 +01:00
commit 02d39e2303
40 changed files with 2658 additions and 0 deletions

View File

@@ -0,0 +1,232 @@
package org.openautonomousconnection.luascript.dom.jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.openautonomousconnection.luascript.hosts.DomHost;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
/**
* DomHost implementation backed by a jsoup Document.
*
* <p>Element identity is the HTML attribute {@code id}. This implementation auto-assigns ids to all
* elements that do not have one, making them addressable.</p>
*/
public final class JsoupDomHost implements DomHost {
private final Document document;
private final AtomicLong autoIdSeq = new AtomicLong(1);
public JsoupDomHost(Document document) {
this.document = Objects.requireNonNull(document, "document");
ensureAllElementsHaveId();
}
public Document document() {
return document;
}
public void ensureAllElementsHaveId() {
Elements all = document.getAllElements();
for (Element el : all) {
if (el == document) continue;
if (!hasUsableId(el)) {
el.attr("id", generateUniqueId());
}
}
}
@Override
public List<String> getAllElementIds() {
Elements all = document.getAllElements();
List<String> ids = new ArrayList<>(all.size());
for (Element el : all) {
if (el == document) continue;
String id = el.id();
if (id != null && !id.isBlank()) ids.add(id);
}
return ids;
}
@Override
public Map<String, String> getAttributes(String elementId) {
Element el = byId(elementId);
Map<String, String> out = new LinkedHashMap<>();
el.attributes().forEach(a -> out.put(a.getKey(), a.getValue()));
return out;
}
@Override
public String getTagName(String elementId) {
return byId(elementId).tagName().toLowerCase(Locale.ROOT);
}
@Override
public String getTextContent(String elementId) {
return byId(elementId).text();
}
@Override
public void setTextContent(String elementId, String text) {
byId(elementId).text(text == null ? "" : text);
}
@Override
public String getAttribute(String elementId, String name) {
Element el = byId(elementId);
String n = normalizeAttr(name);
if (!el.hasAttr(n)) return null;
return el.attr(n);
}
@Override
public void setAttribute(String elementId, String name, String value) {
Element el = byId(elementId);
el.attr(normalizeAttr(name), value == null ? "" : value);
}
@Override
public void removeAttribute(String elementId, String name) {
byId(elementId).removeAttr(normalizeAttr(name));
}
@Override
public String getParentId(String elementId) {
Element el = byId(elementId);
Element p = el.parent();
if (p == null || p == document) return null;
if (!hasUsableId(p)) p.attr("id", generateUniqueId());
return p.id();
}
@Override
public List<String> getChildrenIds(String elementId) {
Element el = byId(elementId);
List<String> out = new ArrayList<>();
for (Element child : el.children()) {
if (!hasUsableId(child)) child.attr("id", generateUniqueId());
out.add(child.id());
}
return out;
}
@Override
public String createElement(String tagName, String requestedId) {
String tag = normalizeTag(tagName);
String id = normalizeOrGenerateId(requestedId);
if (exists(id)) throw new IllegalArgumentException("Element id already exists: " + id);
Element el = document.createElement(tag);
el.attr("id", id);
return id;
}
@Override
public void removeElement(String elementId) {
byId(elementId).remove();
}
@Override
public void appendChild(String parentId, String childId) {
Element parent = byId(parentId);
Element child = byId(childId);
child.remove();
parent.appendChild(child);
}
@Override
public void insertBefore(String parentId, String childId, String beforeChildId) {
Element parent = byId(parentId);
Element child = byId(childId);
Element before = byId(beforeChildId);
if (before.parent() == null || !Objects.equals(before.parent(), parent)) {
throw new IllegalArgumentException("beforeChildId is not a direct child of parentId: " + beforeChildId);
}
child.remove();
before.before(child);
}
@Override
public boolean exists(String id) {
if (id == null || id.isBlank()) return false;
return document.getElementById(id) != null;
}
@Override
public List<String> queryByTag(String tagName) {
String tag = normalizeTag(tagName);
Elements els = document.getElementsByTag(tag);
List<String> out = new ArrayList<>(els.size());
for (Element el : els) {
if (!hasUsableId(el)) el.attr("id", generateUniqueId());
out.add(el.id());
}
return out;
}
@Override
public List<String> queryByClass(String className) {
String cls = normalizeCssIdent(className);
Elements els = document.getElementsByClass(cls);
List<String> out = new ArrayList<>(els.size());
for (Element el : els) {
if (!hasUsableId(el)) el.attr("id", generateUniqueId());
out.add(el.id());
}
return out;
}
private Element byId(String id) {
if (id == null || id.isBlank()) throw new IllegalArgumentException("elementId is blank");
Element el = document.getElementById(id);
if (el == null) throw new IllegalArgumentException("Unknown element id: " + id);
return el;
}
private boolean hasUsableId(Element el) {
String id = el.id();
return id != null && !id.isBlank();
}
private String generateUniqueId() {
while (true) {
String id = "__auto_" + autoIdSeq.getAndIncrement();
if (!exists(id)) return id;
}
}
private String normalizeTag(String tagName) {
if (tagName == null) throw new IllegalArgumentException("tagName is null");
String t = tagName.trim().toLowerCase(Locale.ROOT);
if (t.isEmpty()) throw new IllegalArgumentException("tagName is empty");
return t;
}
private String normalizeAttr(String name) {
if (name == null) throw new IllegalArgumentException("attribute name is null");
String n = name.trim();
if (n.isEmpty()) throw new IllegalArgumentException("attribute name is empty");
return n;
}
private String normalizeCssIdent(String s) {
if (s == null) throw new IllegalArgumentException("identifier is null");
String v = s.trim();
if (v.isEmpty()) throw new IllegalArgumentException("identifier is empty");
return v;
}
private String normalizeOrGenerateId(String requestedId) {
if (requestedId != null) {
String id = requestedId.trim();
if (!id.isEmpty()) return id;
}
return generateUniqueId();
}
}

View File

@@ -0,0 +1,47 @@
package org.openautonomousconnection.luascript.events;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import java.util.List;
import java.util.Map;
/**
* Coerces common Java values into Lua values.
*/
public final class JavaToLua {
private JavaToLua() { }
public static LuaValue coerce(Object v) {
if (v == null) return LuaValue.NIL;
if (v instanceof LuaValue lv) return lv;
if (v instanceof String s) return LuaValue.valueOf(s);
if (v instanceof Boolean b) return LuaValue.valueOf(b);
if (v instanceof Integer i) return LuaValue.valueOf(i);
if (v instanceof Long l) return LuaValue.valueOf(l);
if (v instanceof Float f) return LuaValue.valueOf(f);
if (v instanceof Double d) return LuaValue.valueOf(d);
if (v instanceof Map<?, ?> m) {
LuaTable t = new LuaTable();
for (Map.Entry<?, ?> e : m.entrySet()) {
Object k = e.getKey();
if (k == null) continue;
t.set(String.valueOf(k), coerce(e.getValue()));
}
return t;
}
if (v instanceof List<?> list) {
LuaTable t = new LuaTable();
int i = 1;
for (Object o : list) {
t.set(i++, coerce(o));
}
return t;
}
return LuaValue.valueOf(String.valueOf(v));
}
}

View File

@@ -0,0 +1,100 @@
package org.openautonomousconnection.luascript.events;
import org.luaj.vm2.Globals;
import org.luaj.vm2.LuaError;
import org.luaj.vm2.LuaFunction;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.Varargs;
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
import org.openautonomousconnection.luascript.security.LuaSecurityManager;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* Stores bindings (elementId,eventName -> handlerPath) and dispatches events into Lua.
*/
public final class LuaEventDispatcher {
private final Globals globals;
private final LuaSecurityManager securityManager;
private final LuaExecutionPolicy policy;
private final ConcurrentHashMap<String, String> bindings = new ConcurrentHashMap<>();
public LuaEventDispatcher(Globals globals, LuaSecurityManager securityManager, LuaExecutionPolicy policy) {
this.globals = Objects.requireNonNull(globals, "globals");
this.securityManager = Objects.requireNonNull(securityManager, "securityManager");
this.policy = Objects.requireNonNull(policy, "policy");
}
public void bind(String elementId, String eventName, String handlerPath) {
Objects.requireNonNull(elementId, "elementId");
Objects.requireNonNull(eventName, "eventName");
Objects.requireNonNull(handlerPath, "handlerPath");
String k = key(elementId, UiEventRegistry.normalize(eventName));
bindings.put(k, handlerPath);
}
public void unbind(String elementId, String eventName) {
Objects.requireNonNull(elementId, "elementId");
Objects.requireNonNull(eventName, "eventName");
bindings.remove(key(elementId, UiEventRegistry.normalize(eventName)));
}
public boolean hasBinding(String elementId, String eventName) {
return bindings.containsKey(key(elementId, UiEventRegistry.normalize(eventName)));
}
public boolean dispatch(UiEvent event) {
Objects.requireNonNull(event, "event");
String k = key(event.targetId(), UiEventRegistry.normalize(event.type()));
String handlerPath = bindings.get(k);
if (handlerPath == null) return false;
LuaValue fn = resolvePath(handlerPath);
if (fn.isnil() || !fn.isfunction()) {
throw new LuaError("Handler is not a function: " + handlerPath);
}
LuaValue eventTable = toLuaEvent(event);
Varargs args = LuaValue.varargsOf(new LuaValue[]{eventTable});
securityManager.callGuarded(globals, (LuaFunction) fn, args, policy);
return true;
}
private LuaValue resolvePath(String path) {
String[] parts = path.split("\\.");
LuaValue cur = globals;
for (String p : parts) {
String key = p.trim();
if (key.isEmpty()) throw new LuaError("Invalid handler path: " + path);
cur = cur.get(key);
if (cur.isnil()) return LuaValue.NIL;
}
return cur;
}
private LuaValue toLuaEvent(UiEvent event) {
LuaTable t = new LuaTable();
t.set("target", LuaValue.valueOf(event.targetId()));
t.set("type", LuaValue.valueOf(event.type()));
LuaTable d = new LuaTable();
for (Map.Entry<String, Object> e : event.data().entrySet()) {
String k = e.getKey();
if (k == null) continue;
d.set(k, JavaToLua.coerce(e.getValue()));
}
t.set("data", d);
return t;
}
private static String key(String elementId, String event) {
return elementId + "\n" + event;
}
}

View File

@@ -0,0 +1,33 @@
package org.openautonomousconnection.luascript.events;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
/**
* Event passed from host into Lua.
*/
public final class UiEvent {
private final String targetId;
private final String type;
private final Map<String, Object> data;
public UiEvent(String targetId, String type, Map<String, Object> data) {
this.targetId = Objects.requireNonNull(targetId, "targetId");
this.type = Objects.requireNonNull(type, "type");
this.data = (data == null) ? Collections.emptyMap() : Collections.unmodifiableMap(data);
}
public String targetId() {
return targetId;
}
public String type() {
return type;
}
public Map<String, Object> data() {
return data;
}
}

View File

@@ -0,0 +1,18 @@
package org.openautonomousconnection.luascript.events;
import java.util.Locale;
/**
* Normalizes event names.
*/
public final class UiEventRegistry {
private UiEventRegistry() { }
public static String normalize(String eventName) {
if (eventName == null) throw new IllegalArgumentException("eventName is null");
String e = eventName.trim().toLowerCase(Locale.ROOT);
if (e.isEmpty()) throw new IllegalArgumentException("eventName is empty");
return e;
}
}

View File

@@ -0,0 +1,21 @@
package org.openautonomousconnection.luascript.hosts;
/**
* Host capability for console logging.
*/
public interface ConsoleHost {
/** @param message message */
void info(String message);
/** @param message message */
void log(String message);
/** @param message message */
void warn(String message);
/** @param message message */
void error(String message);
/** @param message message */
void exception(String message);
}

View File

@@ -0,0 +1,150 @@
package org.openautonomousconnection.luascript.hosts;
import java.util.List;
import java.util.Map;
/**
* Host capability that exposes a DOM-like API.
*
* <p>Element identity is the (stable) element id.</p>
*/
public interface DomHost {
/**
* Returns all element ids known to the renderer (must be stable).
*
* @return list of element ids
*/
List<String> getAllElementIds();
/**
* Returns all attributes for the given element id.
*
* @param elementId element id
* @return attributes map (attributeName -> attributeValue)
*/
Map<String, String> getAttributes(String elementId);
/**
* Returns the tag name of the element (lowercase recommended), e.g. "script", "button".
*
* @param elementId element id
* @return tag name
*/
String getTagName(String elementId);
/**
* Returns the text content of an element (used for inline <script>...).
*
* @param elementId element id
* @return text content (never null)
*/
String getTextContent(String elementId);
/**
* Sets the text content of an element.
*
* @param elementId element id
* @param text text
*/
void setTextContent(String elementId, String text);
/**
* Gets a single attribute or null if missing.
*
* @param elementId element id
* @param name attribute name
* @return value or null
*/
String getAttribute(String elementId, String name);
/**
* Sets an attribute (creates it if missing).
*
* @param elementId element id
* @param name attribute name
* @param value attribute value
*/
void setAttribute(String elementId, String name, String value);
/**
* Removes an attribute.
*
* @param elementId element id
* @param name attribute name
*/
void removeAttribute(String elementId, String name);
/**
* Returns parent id or null.
*
* @param elementId element id
* @return parent id or null
*/
String getParentId(String elementId);
/**
* Returns direct children ids.
*
* @param elementId element id
* @return children ids
*/
List<String> getChildrenIds(String elementId);
/**
* Creates a new element (detached) and returns its id.
*
* @param tagName tag name
* @param requestedId optional requested id, may be null/blank for auto id
* @return created element id
*/
String createElement(String tagName, String requestedId);
/**
* Removes an element from the DOM.
*
* @param elementId element id
*/
void removeElement(String elementId);
/**
* Moves/appends child under parent.
*
* @param parentId parent id
* @param childId child id
*/
void appendChild(String parentId, String childId);
/**
* Inserts child before an existing direct child.
*
* @param parentId parent id
* @param childId child id
* @param beforeChildId existing child id
*/
void insertBefore(String parentId, String childId, String beforeChildId);
/**
* Checks if an element id exists.
*
* @param id element id
* @return true if exists
*/
boolean exists(String id);
/**
* Returns element ids by tag.
*
* @param tagName tag
* @return ids
*/
List<String> queryByTag(String tagName);
/**
* Returns element ids by class.
*
* @param className class
* @return ids
*/
List<String> queryByClass(String className);
}

View File

@@ -0,0 +1,37 @@
package org.openautonomousconnection.luascript.hosts;
/**
* Event subscription abstraction (implemented by the client UI layer).
*/
public interface EventHost {
/**
* Subscribes to an element event.
*
* @param elementId element id
* @param eventName event name (e.g. click)
*/
void addListener(String elementId, String eventName);
/**
* Unsubscribes from an element event.
*
* @param elementId element id
* @param eventName event name
*/
void removeListener(String elementId, String eventName);
/**
* Subscribes to a global event (app/window scope).
*
* @param eventName event name
*/
void addGlobalListener(String eventName);
/**
* Unsubscribes from a global event.
*
* @param eventName event name
*/
void removeGlobalListener(String eventName);
}

View File

@@ -0,0 +1,121 @@
package org.openautonomousconnection.luascript.hosts;
import java.util.Objects;
import java.util.Optional;
/**
* Service container holding optional host capabilities.
*
* <p>This avoids one huge "bridge" interface.</p>
*/
public final class HostServices {
private final ConsoleHost console;
private final DomHost dom;
private final EventHost events;
private final ResourceHost resources;
private final UiHost ui;
private HostServices(Builder b) {
this.console = b.console;
this.ui = b.ui;
this.dom = b.dom;
this.events = b.events;
this.resources = b.resources;
}
/**
* @return optional DomHost capability
*/
public Optional<DomHost> dom() {
return Optional.ofNullable(dom);
}
/**
* @return optional EventHost capability
*/
public Optional<EventHost> events() {
return Optional.ofNullable(events);
}
/**
* @return optional ResourceHost capability
*/
public Optional<ResourceHost> resources() {
return Optional.ofNullable(resources);
}
/** @return optional console host */
public Optional<ConsoleHost> console() {
return Optional.ofNullable(console);
}
/** @return optional ui host */
public Optional<UiHost> ui() {
return Optional.ofNullable(ui);
}
/** @return builder */
public static Builder builder() {
return new Builder();
}
/**
* Builder for HostServices.
*/
public static final class Builder {
private ConsoleHost console;
private DomHost dom;
private EventHost events;
private ResourceHost resources;
private UiHost ui;
public Builder console(ConsoleHost console) {
this.console = Objects.requireNonNull(console, "console");
return this;
}
public Builder ui(UiHost ui) {
this.ui = Objects.requireNonNull(ui, "ui");
return this;
}
/**
* Provides dom capability.
*
* @param dom dom host
* @return this
*/
public Builder dom(DomHost dom) {
this.dom = Objects.requireNonNull(dom, "dom");
return this;
}
/**
* Provides event subscription capability.
*
* @param events event host
* @return this
*/
public Builder events(EventHost events) {
this.events = Objects.requireNonNull(events, "events");
return this;
}
/**
* Provides resource loading capability.
*
* @param resources resource host
* @return this
*/
public Builder resources(ResourceHost resources) {
this.resources = Objects.requireNonNull(resources, "resources");
return this;
}
public HostServices build() {
return new HostServices(this);
}
}
}

View File

@@ -0,0 +1,16 @@
package org.openautonomousconnection.luascript.hosts;
/**
* Resource loading abstraction for LuaScript (e.g. script src).
*/
public interface ResourceHost {
/**
* Reads text from a script source (file/url/virtual path).
*
* @param src source identifier
* @return text content
* @throws Exception on load failures
*/
String readText(String src) throws Exception;
}

View File

@@ -0,0 +1,59 @@
package org.openautonomousconnection.luascript.hosts;
/**
* Host capability for UI operations.
*/
public interface UiHost {
void alert(String message);
boolean confirm(String message);
String prompt(String message, String defaultValue);
void setText(String elementId, String text);
String getText(String elementId);
void setHtml(String elementId, String html);
String getHtml(String elementId);
void setValue(String elementId, String value);
String getValue(String elementId);
void setEnabled(String elementId, boolean enabled);
void setVisible(String elementId, boolean visible);
void addClass(String elementId, String className);
void removeClass(String elementId, String className);
boolean toggleClass(String elementId, String className);
boolean hasClass(String elementId, String className);
void setStyle(String elementId, String property, String value);
String getStyle(String elementId, String property);
void setAttribute(String elementId, String name, String value);
String getAttribute(String elementId, String name);
void removeAttribute(String elementId, String name);
void focus(String elementId);
void blur(String elementId);
void scrollIntoView(String elementId);
int viewportWidth();
int viewportHeight();
long nowMillis();
}

View File

@@ -0,0 +1,52 @@
package org.openautonomousconnection.luascript.runtime;
import org.openautonomousconnection.luascript.events.LuaEventDispatcher;
import org.openautonomousconnection.luascript.events.UiEventRegistry;
import org.openautonomousconnection.luascript.hosts.DomHost;
import org.openautonomousconnection.luascript.hosts.EventHost;
import java.util.Map;
import java.util.Objects;
/**
* Scans the DOM for attributes in variant B form: on:EVENT="path.to.handler"
* and binds them automatically via LuaEventDispatcher + EventHost subscriptions.
*/
public final class LuaDomBinder {
private static final String ATTR_PREFIX = "on:";
private final DomHost dom;
private final EventHost eventHost;
private final LuaEventDispatcher dispatcher;
public LuaDomBinder(DomHost dom, EventHost eventHost, LuaEventDispatcher dispatcher) {
this.dom = Objects.requireNonNull(dom, "dom");
this.eventHost = Objects.requireNonNull(eventHost, "eventHost");
this.dispatcher = Objects.requireNonNull(dispatcher, "dispatcher");
}
public void bindAll() {
for (String elementId : dom.getAllElementIds()) {
Map<String, String> attrs = dom.getAttributes(elementId);
if (attrs == null || attrs.isEmpty()) continue;
for (Map.Entry<String, String> e : attrs.entrySet()) {
String attr = e.getKey();
if (attr == null) continue;
String a = attr.trim().toLowerCase();
if (!a.startsWith(ATTR_PREFIX)) continue;
String eventName = UiEventRegistry.normalize(a.substring(ATTR_PREFIX.length()));
String handlerPath = (e.getValue() == null) ? "" : e.getValue().trim();
if (handlerPath.isEmpty()) {
throw new IllegalStateException("Empty handler for attribute '" + attr + "' on element '" + elementId + "'");
}
dispatcher.bind(elementId, eventName, handlerPath);
eventHost.addListener(elementId, eventName);
}
}
}
}

View File

@@ -0,0 +1,30 @@
package org.openautonomousconnection.luascript.runtime;
import org.openautonomousconnection.luascript.events.LuaEventDispatcher;
import org.openautonomousconnection.luascript.events.UiEvent;
import org.openautonomousconnection.luascript.events.UiEventRegistry;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
/**
* Single entry point for the host to forward events into Lua.
*/
public final class LuaEventRouter {
private final LuaEventDispatcher dispatcher;
public LuaEventRouter(LuaEventDispatcher dispatcher) {
this.dispatcher = Objects.requireNonNull(dispatcher, "dispatcher");
}
public boolean emit(String elementId, String eventName, Map<String, Object> data) {
Objects.requireNonNull(elementId, "elementId");
Objects.requireNonNull(eventName, "eventName");
String evt = UiEventRegistry.normalize(eventName);
Map<String, Object> payload = (data == null) ? Collections.emptyMap() : data;
return dispatcher.dispatch(new UiEvent(elementId, evt, payload));
}
}

View File

@@ -0,0 +1,64 @@
package org.openautonomousconnection.luascript.runtime;
import org.luaj.vm2.Globals;
import org.openautonomousconnection.luascript.events.LuaEventDispatcher;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
import org.openautonomousconnection.luascript.security.LuaSecurityManager;
import org.openautonomousconnection.luascript.tables.console.ConsoleTable;
import org.openautonomousconnection.luascript.tables.DomTable;
import org.openautonomousconnection.luascript.tables.EventsTable;
import org.openautonomousconnection.luascript.tables.UiTable;
import java.util.Objects;
/**
* High-level entry point for wiring Lua tables + bootstrap + event routing.
*/
public final class LuaRuntime implements AutoCloseable {
private final Globals globals;
private final HostServices services;
private final LuaSecurityManager securityManager;
private final LuaEventDispatcher dispatcher;
private final LuaEventRouter eventRouter;
public LuaRuntime(Globals globals, HostServices services) {
this(globals, services, LuaExecutionPolicy.uiDefault());
}
public LuaRuntime(Globals globals, HostServices services, LuaExecutionPolicy policy) {
this.globals = Objects.requireNonNull(globals, "globals");
this.services = Objects.requireNonNull(services, "services");
Objects.requireNonNull(policy, "policy");
this.securityManager = new LuaSecurityManager();
this.dispatcher = new LuaEventDispatcher(globals, securityManager, policy);
this.eventRouter = new LuaEventRouter(dispatcher);
}
public Globals globals() {
return globals;
}
public LuaEventRouter eventRouter() {
return eventRouter;
}
public void installStdTables(boolean overwrite) {
new UiTable().inject(globals, services, overwrite);
new ConsoleTable().inject(globals, services, overwrite);
new EventsTable(dispatcher).inject(globals, services, overwrite);
new DomTable().inject(globals, services, overwrite);
}
public void bootstrapFromDom() {
LuaScriptBootstrap.bootstrap(globals, services, dispatcher);
}
@Override
public void close() {
securityManager.close();
}
}

View File

@@ -0,0 +1,69 @@
package org.openautonomousconnection.luascript.runtime;
import org.luaj.vm2.Globals;
import org.openautonomousconnection.luascript.events.LuaEventDispatcher;
import org.openautonomousconnection.luascript.hosts.DomHost;
import org.openautonomousconnection.luascript.hosts.EventHost;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.hosts.ResourceHost;
import java.util.Map;
import java.util.Objects;
/**
* Bootstrap that:
* 1) Executes all Lua scripts found in DOM (<script type="lua"> and <script type="lua" src="...">)
* 2) Scans DOM for on:* handlers and binds them automatically.
*/
public final class LuaScriptBootstrap {
private LuaScriptBootstrap() { }
public static void bootstrap(Globals globals, HostServices services, LuaEventDispatcher dispatcher) {
Objects.requireNonNull(globals, "globals");
Objects.requireNonNull(services, "services");
Objects.requireNonNull(dispatcher, "dispatcher");
DomHost dom = services.dom().orElseThrow(() -> new IllegalStateException("DomHost not provided"));
EventHost eventHost = services.events().orElseThrow(() -> new IllegalStateException("EventHost not provided"));
ResourceHost resources = services.resources().orElseThrow(() -> new IllegalStateException("ResourceHost not provided"));
executeAllScripts(globals, dom, resources);
new LuaDomBinder(dom, eventHost, dispatcher).bindAll();
}
private static void executeAllScripts(Globals globals, DomHost dom, ResourceHost resources) {
for (String elementId : dom.getAllElementIds()) {
String tag = safeLower(dom.getTagName(elementId));
if (!"script".equals(tag)) continue;
Map<String, String> attrs = dom.getAttributes(elementId);
if (attrs == null) continue;
String type = safeLower(attrs.getOrDefault("type", ""));
String src = attrs.get("src");
boolean isLuaByType = "lua".equals(type) || "text/lua".equals(type) || "application/lua".equals(type);
if (!isLuaByType) continue; // IMPORTANT: only run scripts explicitly marked as Lua
boolean hasSrc = src != null && !src.trim().isEmpty();
if (hasSrc) {
String path = src.trim();
try {
String code = resources.readText(path);
LuaScriptExecutor.execute(globals, code, path);
} catch (Exception ex) {
throw new IllegalStateException("Failed to load lua script src='" + path + "': " + ex.getMessage(), ex);
}
} else {
String inline = dom.getTextContent(elementId);
if (inline == null) inline = "";
LuaScriptExecutor.execute(globals, inline, "inline:" + elementId);
}
}
}
private static String safeLower(String s) {
return s == null ? "" : s.trim().toLowerCase();
}
}

View File

@@ -0,0 +1,28 @@
package org.openautonomousconnection.luascript.runtime;
import org.luaj.vm2.Globals;
import org.luaj.vm2.LuaError;
import org.luaj.vm2.LuaValue;
import java.util.Objects;
/**
* Loads and executes Lua source code in a given Globals environment.
*/
public final class LuaScriptExecutor {
private LuaScriptExecutor() { }
public static void execute(Globals globals, String source, String chunkName) {
Objects.requireNonNull(globals, "globals");
Objects.requireNonNull(source, "source");
Objects.requireNonNull(chunkName, "chunkName");
try {
LuaValue chunk = globals.load(source, chunkName);
chunk.call();
} catch (LuaError e) {
throw new LuaError("Lua error in '" + chunkName + "': " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,29 @@
package org.openautonomousconnection.luascript.security;
import java.time.Duration;
/**
* Execution policy for guarded Lua execution.
*
* @param timeout max wall-clock time
* @param instructionLimit max VM instruction budget (approximation via debug hook)
* @param hookStep number of VM instructions between hook ticks
*/
public record LuaExecutionPolicy(Duration timeout, long instructionLimit, int hookStep) {
public LuaExecutionPolicy {
if (timeout == null) throw new IllegalArgumentException("timeout must not be null");
if (timeout.isZero() || timeout.isNegative()) throw new IllegalArgumentException("timeout must be > 0");
if (instructionLimit <= 0) throw new IllegalArgumentException("instructionLimit must be > 0");
if (hookStep <= 0) throw new IllegalArgumentException("hookStep must be > 0");
}
/**
* Default policy for UI event handlers.
*
* @return policy
*/
public static LuaExecutionPolicy uiDefault() {
return new LuaExecutionPolicy(Duration.ofMillis(50), 200_000, 5_000);
}
}

View File

@@ -0,0 +1,143 @@
package org.openautonomousconnection.luascript.security;
import org.luaj.vm2.*;
import org.luaj.vm2.lib.DebugLib;
import org.luaj.vm2.lib.VarArgFunction;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
/**
* Executes Lua functions on a dedicated thread under a debug hook (LuaJ).
*
* <p>LuaJ cannot be hard-killed reliably; the debug hook is the enforcement mechanism.</p>
*/
public final class LuaSecurityManager implements AutoCloseable {
private final ExecutorService executor;
/**
* Creates a new LuaSecurityManager that runs all Lua code on a single dedicated daemon thread.
*/
public LuaSecurityManager() {
this.executor = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "oac-lua-thread");
t.setDaemon(true);
return t;
});
}
/**
* Executes a Lua function guarded by the provided policy.
*
* @param globals globals/environment
* @param function function to execute
* @param args arguments
* @param policy execution policy
* @return results (Lua varargs)
*/
public Varargs callGuarded(Globals globals, LuaFunction function, Varargs args, LuaExecutionPolicy policy) {
Objects.requireNonNull(globals, "globals");
Objects.requireNonNull(function, "function");
Objects.requireNonNull(args, "args");
Objects.requireNonNull(policy, "policy");
Future<Varargs> f = executor.submit(() -> callWithHook(globals, function, args, policy));
try {
return f.get(policy.timeout().toMillis(), TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
f.cancel(true);
throw new LuaError("Lua execution timed out after " + policy.timeout().toMillis() + "ms");
} catch (ExecutionException e) {
Throwable c = e.getCause();
if (c instanceof LuaError le) throw le;
if (c instanceof RuntimeException re) throw re;
throw new RuntimeException(c);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted while waiting for Lua execution", e);
}
}
private static Varargs callWithHook(Globals globals, LuaFunction function, Varargs args, LuaExecutionPolicy policy) {
final LuaValue sethook = resolveAndHideDebugSetHook(globals);
final long deadlineMillis = System.currentTimeMillis() + policy.timeout().toMillis();
final AtomicLong ticks = new AtomicLong(0);
final LuaValue hookFn = new VarArgFunction() {
@Override
public Varargs invoke(Varargs ignored) {
long used = ticks.addAndGet(policy.hookStep());
if (used > policy.instructionLimit()) {
throw new LuaError("Lua instruction limit exceeded: " + policy.instructionLimit());
}
if (System.currentTimeMillis() > deadlineMillis) {
throw new LuaError("Lua execution deadline exceeded");
}
return LuaValue.NIL;
}
};
final LuaThread thread = new LuaThread(globals, function);
try {
// debug.sethook(thread, hookFn, mask, count)
sethook.invoke(LuaValue.varargsOf(new LuaValue[]{
thread,
hookFn,
LuaValue.valueOf(""),
LuaValue.valueOf(policy.hookStep())
}));
Varargs resumed = thread.resume(args);
return unwrapCoroutineResume(resumed);
} finally {
try {
sethook.invoke(LuaValue.varargsOf(new LuaValue[]{thread}));
} catch (Exception ignored) {
// Best-effort cleanup.
}
}
}
private static Varargs unwrapCoroutineResume(Varargs resumed) {
if (resumed == null || resumed.narg() == 0) return LuaValue.NIL;
LuaValue ok = resumed.arg1();
if (ok.isboolean() && !ok.toboolean()) {
LuaValue err = resumed.narg() >= 2 ? resumed.arg(2) : LuaValue.valueOf("Unknown Lua error");
throw new LuaError(err.tojstring());
}
if (ok.isboolean()) return resumed.subargs(2);
return resumed;
}
private static LuaValue resolveAndHideDebugSetHook(Globals globals) {
// Ensure DebugLib exists for hooks.
globals.load(new DebugLib());
LuaValue debugTable = globals.get("debug");
if (debugTable.isnil() || !debugTable.istable()) {
throw new IllegalStateException("Debug library not available (debug table missing)");
}
LuaValue sethook = debugTable.get("sethook");
if (sethook.isnil() || !sethook.isfunction()) {
throw new IllegalStateException("debug.sethook not available");
}
// Hide debug from scripts.
globals.set("debug", LuaValue.NIL);
return sethook;
}
@Override
public void close() {
executor.shutdownNow();
}
}

View File

@@ -0,0 +1,179 @@
package org.openautonomousconnection.luascript.tables;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.*;
import org.openautonomousconnection.luascript.utils.ScriptTable;
import org.openautonomousconnection.luascript.hosts.DomHost;
import org.openautonomousconnection.luascript.hosts.HostServices;
import java.util.List;
import java.util.Map;
/**
* Lua namespace "dom" for DOM querying and mutation.
*
* <p>All element references are by element id (string).</p>
*/
public final class DomTable extends ScriptTable {
public DomTable() {
super("dom");
}
@Override
protected void define(HostServices services) {
DomHost dom = services.dom().orElseThrow(() -> new IllegalStateException("DomHost not provided"));
table().set("allIds", new ZeroArgFunction() {
@Override
public LuaValue call() {
return toLuaArray(dom.getAllElementIds());
}
});
table().set("exists", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
return LuaValue.valueOf(dom.exists(id.checkjstring()));
}
});
table().set("tag", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
return LuaValue.valueOf(dom.getTagName(id.checkjstring()));
}
});
table().set("text", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
return LuaValue.valueOf(dom.getTextContent(id.checkjstring()));
}
});
table().set("setText", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue text) {
dom.setTextContent(id.checkjstring(), text.isnil() ? "" : text.tojstring());
return LuaValue.NIL;
}
});
table().set("attrs", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
Map<String, String> attrs = dom.getAttributes(id.checkjstring());
LuaTable t = new LuaTable();
if (attrs != null) {
for (Map.Entry<String, String> e : attrs.entrySet()) {
if (e.getKey() == null) continue;
t.set(e.getKey(), e.getValue() == null ? LuaValue.NIL : LuaValue.valueOf(e.getValue()));
}
}
return t;
}
});
table().set("getAttr", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue name) {
String v = dom.getAttribute(id.checkjstring(), name.checkjstring());
return v == null ? LuaValue.NIL : LuaValue.valueOf(v);
}
});
table().set("setAttr", new ThreeArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue name, LuaValue value) {
dom.setAttribute(id.checkjstring(), name.checkjstring(), value.isnil() ? "" : value.tojstring());
return LuaValue.NIL;
}
});
table().set("removeAttr", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue name) {
dom.removeAttribute(id.checkjstring(), name.checkjstring());
return LuaValue.NIL;
}
});
table().set("parent", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
String p = dom.getParentId(id.checkjstring());
return p == null ? LuaValue.NIL : LuaValue.valueOf(p);
}
});
table().set("children", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
List<String> children = dom.getChildrenIds(id.checkjstring());
return toLuaArray(children);
}
});
table().set("byTag", new OneArgFunction() {
@Override
public LuaValue call(LuaValue tag) {
return toLuaArray(dom.queryByTag(tag.checkjstring()));
}
});
table().set("byClass", new OneArgFunction() {
@Override
public LuaValue call(LuaValue cls) {
return toLuaArray(dom.queryByClass(cls.checkjstring()));
}
});
table().set("create", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue tagName, LuaValue requestedId) {
String id = dom.createElement(
tagName.checkjstring(),
requestedId.isnil() ? null : requestedId.tojstring()
);
return LuaValue.valueOf(id);
}
});
table().set("remove", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
dom.removeElement(id.checkjstring());
return LuaValue.NIL;
}
});
table().set("appendChild", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue parentId, LuaValue childId) {
dom.appendChild(parentId.checkjstring(), childId.checkjstring());
return LuaValue.NIL;
}
});
table().set("insertBefore", new ThreeArgFunction() {
@Override
public LuaValue call(LuaValue parentId, LuaValue childId, LuaValue beforeChildId) {
dom.insertBefore(parentId.checkjstring(), childId.checkjstring(), beforeChildId.checkjstring());
return LuaValue.NIL;
}
});
}
private static LuaValue toLuaArray(List<String> values) {
LuaTable t = new LuaTable();
if (values == null || values.isEmpty()) return t;
int i = 1;
for (String v : values) {
t.set(i++, v == null ? LuaValue.NIL : LuaValue.valueOf(v));
}
return t;
}
}

View File

@@ -0,0 +1,87 @@
package org.openautonomousconnection.luascript.tables;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.OneArgFunction;
import org.luaj.vm2.lib.TwoArgFunction;
import org.openautonomousconnection.luascript.utils.ScriptTable;
import org.openautonomousconnection.luascript.events.LuaEventDispatcher;
import org.openautonomousconnection.luascript.events.UiEventRegistry;
import org.openautonomousconnection.luascript.hosts.EventHost;
import org.openautonomousconnection.luascript.hosts.HostServices;
/**
* events namespace for manual binding/unbinding.
*
* <p>Auto-binding via on:* is handled by LuaDomBinder. This table is for explicit scripting.</p>
*/
public final class EventsTable extends ScriptTable {
private final LuaEventDispatcher dispatcher;
public EventsTable(LuaEventDispatcher dispatcher) {
super("events");
this.dispatcher = dispatcher;
}
@Override
protected void define(HostServices services) {
EventHost host = services.events().orElseThrow(() -> new IllegalStateException("EventHost not provided"));
table().set("on", new org.luaj.vm2.lib.ThreeArgFunction() {
@Override
public LuaValue call(LuaValue elementId, LuaValue eventName, LuaValue handlerPath) {
String id = elementId.checkjstring();
String evt = UiEventRegistry.normalize(eventName.checkjstring());
String hp = handlerPath.checkjstring();
dispatcher.bind(id, evt, hp);
host.addListener(id, evt);
return LuaValue.NIL;
}
});
table().set("off", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue elementId, LuaValue eventName) {
String id = elementId.checkjstring();
String evt = UiEventRegistry.normalize(eventName.checkjstring());
dispatcher.unbind(id, evt);
host.removeListener(id, evt);
return LuaValue.NIL;
}
});
table().set("has", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue elementId, LuaValue eventName) {
String id = elementId.checkjstring();
String evt = UiEventRegistry.normalize(eventName.checkjstring());
return LuaValue.valueOf(dispatcher.hasBinding(id, evt));
}
});
table().set("onGlobal", new TwoArgFunction() {
@Override
public LuaValue call(LuaValue eventName, LuaValue handlerPath) {
String evt = UiEventRegistry.normalize(eventName.checkjstring());
String hp = handlerPath.checkjstring();
dispatcher.bind("__global__", evt, hp);
host.addGlobalListener(evt);
return LuaValue.NIL;
}
});
table().set("offGlobal", new OneArgFunction() {
@Override
public LuaValue call(LuaValue eventName) {
String evt = UiEventRegistry.normalize(eventName.checkjstring());
dispatcher.unbind("__global__", evt);
host.removeGlobalListener(evt);
return LuaValue.NIL;
}
});
}
}

View File

@@ -0,0 +1,225 @@
package org.openautonomousconnection.luascript.tables;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.OneArgFunction;
import org.openautonomousconnection.luascript.utils.ScriptTable;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.hosts.UiHost;
/**
* ui namespace for common UI operations.
*/
public final class UiTable extends ScriptTable {
public UiTable() {
super("ui");
}
@Override
protected void define(HostServices services) {
UiHost ui = services.ui().orElseThrow(() -> new IllegalStateException("UiHost not provided"));
table().set("alert", new OneArgFunction() {
@Override
public LuaValue call(LuaValue msg) {
ui.alert(msg.isnil() ? "" : msg.tojstring());
return LuaValue.NIL;
}
});
table().set("confirm", new OneArgFunction() {
@Override
public LuaValue call(LuaValue msg) {
return LuaValue.valueOf(ui.confirm(msg.isnil() ? "" : msg.tojstring()));
}
});
table().set("prompt", new org.luaj.vm2.lib.TwoArgFunction() {
@Override
public LuaValue call(LuaValue msg, LuaValue def) {
String out = ui.prompt(
msg.isnil() ? "" : msg.tojstring(),
def.isnil() ? null : def.tojstring()
);
return out == null ? LuaValue.NIL : LuaValue.valueOf(out);
}
});
table().set("setText", new org.luaj.vm2.lib.TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue text) {
ui.setText(id.checkjstring(), text.isnil() ? "" : text.tojstring());
return LuaValue.NIL;
}
});
table().set("getText", new OneArgFunction() {
@Override public LuaValue call(LuaValue id) {
String v = ui.getText(id.checkjstring());
return v == null ? LuaValue.NIL : LuaValue.valueOf(v);
}
});
table().set("setHtml", new org.luaj.vm2.lib.TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue html) {
ui.setHtml(id.checkjstring(), html.isnil() ? "" : html.tojstring());
return LuaValue.NIL;
}
});
table().set("getHtml", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
return LuaValue.valueOf(ui.getHtml(id.checkjstring()));
}
});
table().set("setValue", new org.luaj.vm2.lib.TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue value) {
ui.setValue(id.checkjstring(), value.isnil() ? "" : value.tojstring());
return LuaValue.NIL;
}
});
table().set("getValue", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
return LuaValue.valueOf(ui.getValue(id.checkjstring()));
}
});
table().set("setEnabled", new org.luaj.vm2.lib.TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue enabled) {
ui.setEnabled(id.checkjstring(), enabled.toboolean());
return LuaValue.NIL;
}
});
table().set("setVisible", new org.luaj.vm2.lib.TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue visible) {
ui.setVisible(id.checkjstring(), visible.toboolean());
return LuaValue.NIL;
}
});
table().set("addClass", new org.luaj.vm2.lib.TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue cls) {
ui.addClass(id.checkjstring(), cls.checkjstring());
return LuaValue.NIL;
}
});
table().set("removeClass", new org.luaj.vm2.lib.TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue cls) {
ui.removeClass(id.checkjstring(), cls.checkjstring());
return LuaValue.NIL;
}
});
table().set("toggleClass", new org.luaj.vm2.lib.TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue cls) {
return LuaValue.valueOf(ui.toggleClass(id.checkjstring(), cls.checkjstring()));
}
});
table().set("hasClass", new org.luaj.vm2.lib.TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue cls) {
return LuaValue.valueOf(ui.hasClass(id.checkjstring(), cls.checkjstring()));
}
});
table().set("setStyle", new org.luaj.vm2.lib.ThreeArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue prop, LuaValue value) {
ui.setStyle(id.checkjstring(), prop.checkjstring(), value.isnil() ? "" : value.tojstring());
return LuaValue.NIL;
}
});
table().set("getStyle", new org.luaj.vm2.lib.TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue prop) {
return LuaValue.valueOf(ui.getStyle(id.checkjstring(), prop.checkjstring()));
}
});
table().set("setAttr", new org.luaj.vm2.lib.ThreeArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue name, LuaValue value) {
ui.setAttribute(id.checkjstring(), name.checkjstring(), value.isnil() ? "" : value.tojstring());
return LuaValue.NIL;
}
});
table().set("getAttr", new org.luaj.vm2.lib.TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue name) {
String v = ui.getAttribute(id.checkjstring(), name.checkjstring());
return v == null ? LuaValue.NIL : LuaValue.valueOf(v);
}
});
table().set("removeAttr", new org.luaj.vm2.lib.TwoArgFunction() {
@Override
public LuaValue call(LuaValue id, LuaValue name) {
ui.removeAttribute(id.checkjstring(), name.checkjstring());
return LuaValue.NIL;
}
});
table().set("focus", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
ui.focus(id.checkjstring());
return LuaValue.NIL;
}
});
table().set("blur", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
ui.blur(id.checkjstring());
return LuaValue.NIL;
}
});
table().set("scrollIntoView", new OneArgFunction() {
@Override
public LuaValue call(LuaValue id) {
ui.scrollIntoView(id.checkjstring());
return LuaValue.NIL;
}
});
LuaTable viewport = new LuaTable();
viewport.set("width", new org.luaj.vm2.lib.ZeroArgFunction() {
@Override
public LuaValue call() {
return LuaValue.valueOf(ui.viewportWidth());
}
});
viewport.set("height", new org.luaj.vm2.lib.ZeroArgFunction() {
@Override
public LuaValue call() {
return LuaValue.valueOf(ui.viewportHeight());
}
});
table().set("viewport", viewport);
table().set("now", new org.luaj.vm2.lib.ZeroArgFunction() {
@Override
public LuaValue call() {
return LuaValue.valueOf(ui.nowMillis());
}
});
}
}

View File

@@ -0,0 +1,42 @@
package org.openautonomousconnection.luascript.tables.console;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.OneArgFunction;
import org.openautonomousconnection.luascript.utils.ScriptTable;
import org.openautonomousconnection.luascript.hosts.HostServices;
public class ConsoleLogTable extends ScriptTable {
/**
* Creates a new script table with the given global name.
*/
public ConsoleLogTable() {
super("log");
}
@Override
protected void define(HostServices services) {
table().set("info", new OneArgFunction() {
@Override
public LuaValue call(LuaValue arg) {
services.console().get().info(arg.isnil() ? "nil" : arg.tojstring());
return LuaValue.NIL;
}
});
table().set("log", new OneArgFunction() {
@Override
public LuaValue call(LuaValue arg) {
services.console().get().log(arg.isnil() ? "nil" : arg.tojstring());
return LuaValue.NIL;
}
});
table().set("warn", new OneArgFunction() {
@Override
public LuaValue call(LuaValue arg) {
services.console().get().warn(arg.isnil() ? "nil" : arg.tojstring());
return LuaValue.NIL;
}
});
}
}

View File

@@ -0,0 +1,34 @@
package org.openautonomousconnection.luascript.tables.console;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.OneArgFunction;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.utils.ScriptTable;
public class ConsoleStacktraceTable extends ScriptTable {
/**
* Creates a new script table with the given global name.
*/
public ConsoleStacktraceTable() {
super("stacktrace");
}
@Override
protected void define(HostServices services) {
table().set("print", new OneArgFunction() {
@Override
public LuaValue call(LuaValue arg) {
services.console().get().error(arg.isnil() ? "nil" : arg.tojstring());
return LuaValue.NIL;
}
});
table().set("exception", new OneArgFunction() {
@Override
public LuaValue call(LuaValue arg) {
services.console().get().exception(arg.isnil() ? "nil" : arg.tojstring());
return LuaValue.NIL;
}
});
}
}

View File

@@ -0,0 +1,19 @@
package org.openautonomousconnection.luascript.tables.console;
import org.openautonomousconnection.luascript.hosts.HostServices;
import org.openautonomousconnection.luascript.utils.ScriptTable;
public class ConsoleTable extends ScriptTable {
/**
* Creates a new script table with the given global name.
*/
public ConsoleTable() {
super("console");
}
@Override
protected void define(HostServices services) {
injectChild(new ConsoleLogTable(), services, true);
injectChild(new ConsoleStacktraceTable(), services, true);
}
}

View File

@@ -0,0 +1,137 @@
package org.openautonomousconnection.luascript.utils;
import org.luaj.vm2.Globals;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.DebugLib;
import org.luaj.vm2.lib.jse.JsePlatform;
import java.util.Objects;
import java.util.function.Consumer;
/**
* Factory for creating configured LuaJ {@link Globals} instances.
*
* <p>Design goals:</p>
* <ul>
* <li>No global singletons (safe for multi-view/multi-session usage)</li>
* <li>Optional debug library</li>
* <li>Optional sandbox hardening</li>
* <li>Extension point for host APIs</li>
* </ul>
*/
public final class LuaGlobalsFactory {
private LuaGlobalsFactory() { }
/**
* Configuration options for creating a {@link Globals} instance.
*/
public static final class Options {
private boolean enableDebug;
private boolean sandbox;
private Consumer<Globals> hostApiConfigurer;
/**
* Enables or disables Lua debug library exposure.
*
* @param enableDebug true to enable debug lib, false otherwise
* @return this options instance
*/
public Options enableDebug(boolean enableDebug) {
this.enableDebug = enableDebug;
return this;
}
/**
* Enables or disables sandbox hardening (disables os/io/debug/luajava/package/require/loadfile/dofile).
*
* @param sandbox true to harden, false otherwise
* @return this options instance
*/
public Options sandbox(boolean sandbox) {
this.sandbox = sandbox;
return this;
}
/**
* Adds a hook to register host APIs (e.g. {@code host.ui.alert}).
*
* @param hostApiConfigurer configurer callback
* @return this options instance
*/
public Options hostApiConfigurer(Consumer<Globals> hostApiConfigurer) {
this.hostApiConfigurer = hostApiConfigurer;
return this;
}
}
/**
* Creates a new {@link Globals} based on {@link JsePlatform#standardGlobals()} and applies options.
*
* @param options options for creation
* @return configured globals instance
*/
public static Globals create(Options options) {
Objects.requireNonNull(options, "options");
Globals g = JsePlatform.standardGlobals();
if (options.enableDebug) {
// Ensure debug functions are available (depending on defaults).
g.load(new DebugLib());
} else {
// If sandbox is not enabled but you still want no debug, explicitly remove it.
g.set("debug", LuaValue.NIL);
}
if (options.sandbox) {
hardenSandbox(g);
}
if (options.hostApiConfigurer != null) {
options.hostApiConfigurer.accept(g);
}
return g;
}
/**
* Applies basic sandbox hardening to the given globals.
*
* @param g globals
*/
public static void hardenSandbox(Globals g) {
Objects.requireNonNull(g, "g");
g.set("os", LuaValue.NIL);
g.set("io", LuaValue.NIL);
g.set("debug", LuaValue.NIL);
g.set("luajava", LuaValue.NIL);
// Prevent module loading / file loading
g.set("package", LuaValue.NIL);
g.set("require", LuaValue.NIL);
g.set("loadfile", LuaValue.NIL);
g.set("dofile", LuaValue.NIL);
}
/**
* Convenience method to register a root 'host' table if missing and return it.
*
* @param g globals
* @return host table
*/
public static LuaTable ensureHostTable(Globals g) {
Objects.requireNonNull(g, "g");
LuaValue existing = g.get("host");
if (existing.istable()) {
return (LuaTable) existing;
}
LuaTable host = new LuaTable();
g.set("host", host);
return host;
}
}

View File

@@ -0,0 +1,77 @@
package org.openautonomousconnection.luascript.utils;
import org.luaj.vm2.Globals;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import org.openautonomousconnection.luascript.hosts.HostServices;
import java.util.Objects;
/**
* Base class for exposing a Lua table (namespace) into globals.
*/
public abstract class ScriptTable {
private final LuaTable table;
private final String tableName;
protected ScriptTable(String tableName) {
this.tableName = Objects.requireNonNull(tableName, "tableName");
this.table = new LuaTable();
}
public final LuaTable table() {
return table;
}
public final String tableName() {
return tableName;
}
/**
* Defines functions/fields for this table. Called exactly once before injection.
*
* @param services host services
*/
protected abstract void define(HostServices services);
/**
* Injects this table into globals as a global namespace (e.g. "ui", "console", "events").
*
* @param globals globals
* @param services services
* @param overwriteExisting overwrite existing global
*/
public final void inject(Globals globals, HostServices services, boolean overwriteExisting) {
Objects.requireNonNull(globals, "globals");
Objects.requireNonNull(services, "services");
LuaValue existing = globals.get(tableName);
if (!overwriteExisting && !existing.isnil()) {
throw new IllegalStateException("Lua global already exists: " + tableName);
}
define(services);
globals.set(tableName, table);
}
/**
* Injects a child table as a nested namespace (e.g. ui.modal.*).
*
* @param child child table
* @param services host services
* @param overwriteExisting overwrite if key exists
*/
public final void injectChild(ScriptTable child, HostServices services, boolean overwriteExisting) {
Objects.requireNonNull(child, "child");
Objects.requireNonNull(services, "services");
LuaValue existing = table.get(child.tableName);
if (!overwriteExisting && !existing.isnil()) {
throw new IllegalStateException("Lua key already exists: " + tableName + "." + child.tableName);
}
child.define(services);
table.set(child.tableName, child.table);
}
}