commit fe2850128e724ff02aab2f33fa12fd8036a58f7c
parent dc0a6d19c05bedb1d733de4bcfd4dc753befdacb
Author: luajitos <bbhbb2094@gmail.com>
Date: Sun, 30 Nov 2025 08:50:40 +0000
added system-apps permission, fixed manifest parsing, fixed fs & app proxy, fixed inline html rendering
Diffstat:
11 files changed, 579 insertions(+), 149 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
@@ -0,0 +1 @@
+- if you only change lua then just run ./repack_lua.sh and tell me that you have repacked it, if you have changed C code then rebuild it with ./build.sh
+\ No newline at end of file
diff --git a/iso_includes/apps/com.luajitos.htmltest/src/index.html b/iso_includes/apps/com.luajitos.htmltest/src/index.html
@@ -1,7 +1,10 @@
<!DOCTYPE html>
<html>
<head>
- <title>HTML Test App</title>
+ <title>HTML Test Application</title>
+ <meta name="window-width" content="500">
+ <meta name="window-height" content="600">
+ <!-- meta name="window-height" value="600"> also works -->
</head>
<body>
<h1>HTML App Test</h1>
diff --git a/iso_includes/apps/com.luajitos.moonbrowser/src/render.lua b/iso_includes/apps/com.luajitos.moonbrowser/src/render.lua
@@ -88,6 +88,9 @@ function Renderer.new(app, width, height)
self.interactiveElements = {} -- Store buttons/inputs for click handling
self.inputValues = {} -- Store input field values
self.focusedInput = nil -- Currently focused input
+ self.inlineX = 0 -- Track X position for inline elements
+ self.inlineY = 0 -- Track Y position for current inline row
+ self.inlineHeight = 0 -- Track max height of current inline row
return self
end
@@ -151,6 +154,13 @@ function Renderer:renderElement(elem, gfx, x, y, maxWidth, inheritedFontSize)
-- Get display type
local display = getStyle(elem, "display") or "block"
+ -- Reset inline flow when hitting a block element
+ if display == "block" and self.inlineX > 0 then
+ y = y + self.inlineHeight + 4
+ self.inlineX = 0
+ self.inlineHeight = 0
+ end
+
-- Apply top margin for block elements
local marginTop = getStyle(elem, "marginTop") or 0
if display == "block" and marginTop > 0 then
@@ -280,24 +290,36 @@ function Renderer:renderElement(elem, gfx, x, y, maxWidth, inheritedFontSize)
local btnWidth = #buttonText * 8 + 16
local btnHeight = 20
+ local btnMargin = 4
+
+ -- Use inline flow - check if we need to wrap to new line
+ if self.inlineX > 0 and self.inlineX + btnWidth + btnMargin > maxWidth then
+ -- Wrap to new line
+ y = y + self.inlineHeight + 4
+ self.inlineX = 0
+ self.inlineHeight = 0
+ end
+
+ local drawX = x + self.inlineX
+ local drawY = y
- if y >= -btnHeight and y < self.height then
+ if drawY >= -btnHeight and drawY < self.height then
-- Button background (gradient effect)
- gfx:fillRect(currentX, y, btnWidth, btnHeight, 0xE0E0E0)
+ gfx:fillRect(drawX, drawY, btnWidth, btnHeight, 0xE0E0E0)
-- Top/left highlight
- gfx:fillRect(currentX, y, btnWidth, 1, 0xFFFFFF)
- gfx:fillRect(currentX, y, 1, btnHeight, 0xFFFFFF)
+ gfx:fillRect(drawX, drawY, btnWidth, 1, 0xFFFFFF)
+ gfx:fillRect(drawX, drawY, 1, btnHeight, 0xFFFFFF)
-- Bottom/right shadow
- gfx:fillRect(currentX, y + btnHeight - 1, btnWidth, 1, 0x808080)
- gfx:fillRect(currentX + btnWidth - 1, y, 1, btnHeight, 0x808080)
+ gfx:fillRect(drawX, drawY + btnHeight - 1, btnWidth, 1, 0x808080)
+ gfx:fillRect(drawX + btnWidth - 1, drawY, 1, btnHeight, 0x808080)
-- Button text (centered)
- gfx:drawText(currentX + 8, y + 6, buttonText, 0x000000)
+ gfx:drawText(drawX + 8, drawY + 6, buttonText, 0x000000)
-- Store for click handling
table.insert(self.interactiveElements, {
type = "button",
- x = currentX,
- y = y + self.scrollY, -- Store absolute position
+ x = drawX,
+ y = drawY + self.scrollY, -- Store absolute position
width = btnWidth,
height = btnHeight,
text = buttonText,
@@ -306,7 +328,12 @@ function Renderer:renderElement(elem, gfx, x, y, maxWidth, inheritedFontSize)
})
end
- y = y + btnHeight + 4
+ -- Advance inline position
+ self.inlineX = self.inlineX + btnWidth + btnMargin
+ if btnHeight > self.inlineHeight then
+ self.inlineHeight = btnHeight
+ end
+
return y -- Don't render children for button (already got text)
elseif tag == "input" then
diff --git a/iso_includes/apps/com.luajitos.shell/manifest.lua b/iso_includes/apps/com.luajitos.shell/manifest.lua
@@ -6,7 +6,7 @@ return {
category = "System",
description = "LuajitOS Shell Terminal",
entry = "shell.lua",
- type = "cli",
+ type = "gui",
permissions = {
"filesystem",
"scheduling",
diff --git a/iso_includes/apps/com.luajitos.shell/src/shell.lua b/iso_includes/apps/com.luajitos.shell/src/shell.lua
@@ -9,6 +9,9 @@ local text = "LuajitOS Shell\n> "
local current_line = ""
local i = 0
local scroll_offset = 0 -- Number of lines to scroll up (0 = show bottom)
+
+-- Track error line ranges (pairs of {start_line, end_line} in the wrapped output)
+local error_lines = {} -- List of lines that are errors (by content prefix)
local cwd = "/home" -- Current working directory (local to shell), ~ = /home
-- Get app path for $ expansion (shell's own app path)
@@ -520,7 +523,15 @@ window.onDraw = function(gfx)
local endLine = math.min(totalLines, bottomLine)
for i = startLine, endLine do
- gfx:drawText(10, y, wrappedLines[i], 0x00FF00)
+ local line = wrappedLines[i]
+ local color = 0x00FF00 -- Default green
+
+ -- Check if this line is an error (starts with "Error:" or is continuation of error)
+ if line:match("^Error:") or line:match("^%s+at ") or line:match("^%s+in ") then
+ color = 0xFF4444 -- Red for errors
+ end
+
+ gfx:drawText(10, y, line, color)
y = y + lineHeight
end
end
diff --git a/iso_includes/apps/com.luajitos.taskbar/manifest.lua b/iso_includes/apps/com.luajitos.taskbar/manifest.lua
@@ -13,8 +13,10 @@ return {
"import",
"draw",
"system-all",
+ "system-apps",
"run",
"ramdisk",
+ "system",
"system-hook"
},
allowedPaths = {
diff --git a/iso_includes/apps/com.luajitos.taskbar/src/init.lua b/iso_includes/apps/com.luajitos.taskbar/src/init.lua
@@ -58,8 +58,8 @@ local function getRunningApplications()
local appList = {}
-- Access global applications from sys
- if sys and sys.applications then
- for pid, application in pairs(sys.applications) do
+ if sys and sys.apps then
+ for pid, application in pairs(sys.apps) do
-- Check if app has any visible or minimized non-taskbar windows
local hasWindows = false
if application.windows then
diff --git a/iso_includes/os/init.lua b/iso_includes/os/init.lua
@@ -632,8 +632,12 @@ local function rebuildWindowStack()
if _G.sys and _G.sys.applications then
for pid, app in pairs(_G.sys.applications) do
if app.windows then
- for _, window in ipairs(app.windows) do
- table.insert(_G.window_stack, window)
+ for idx, window in ipairs(app.windows) do
+ if type(window) ~= "table" then
+ osprint("ERROR: app.windows[" .. idx .. "] is a " .. type(window) .. " not a table! pid=" .. tostring(pid) .. " appName=" .. tostring(app.appName) .. "\n")
+ else
+ table.insert(_G.window_stack, window)
+ end
end
end
end
diff --git a/iso_includes/os/libs/Run.lua b/iso_includes/os/libs/Run.lua
@@ -115,29 +115,156 @@ local function deep_copy(obj)
end
-- Utility: Parse manifest (simple Lua code execution)
+-- Parse manifest content as text (no Lua execution for security)
local function parse_manifest(manifest_code)
if not manifest_code then
return {}
end
- -- Try to load and execute manifest code directly
- local manifest_func, err = load(manifest_code, "manifest", "t")
- if not manifest_func then
- if osprint then
- osprint("ERROR: Failed to parse manifest: " .. tostring(err) .. "\n")
+ -- Remove "return" if present at start
+ manifest_code = string.gsub(manifest_code, "^%s*return%s*", "")
+
+ -- Remove outer braces only if both opening and closing exist as a pair
+ local trimmed = string.gsub(manifest_code, "^%s+", "")
+ trimmed = string.gsub(trimmed, "%s+$", "")
+ if string.sub(trimmed, 1, 1) == "{" and string.sub(trimmed, -1) == "}" then
+ manifest_code = string.sub(trimmed, 2, -2)
+ end
+
+ -- Extract [[long strings]] before removing comments (to preserve their content)
+ local long_strings = {}
+ local placeholder_idx = 0
+ manifest_code = string.gsub(manifest_code, "%[%[(.-)%]%]", function(content)
+ placeholder_idx = placeholder_idx + 1
+ local placeholder = "\001LONGSTR" .. placeholder_idx .. "\001"
+ long_strings[placeholder] = content
+ return placeholder
+ end)
+
+ -- Remove single-line comments (-- to end of line)
+ manifest_code = string.gsub(manifest_code, "%-%-[^\n]*", "")
+
+ -- Normalize whitespace: replace newlines/tabs with spaces, collapse multiple spaces
+ manifest_code = string.gsub(manifest_code, "[\r\n\t]+", " ")
+ manifest_code = string.gsub(manifest_code, " +", " ")
+
+ local manifest = {}
+
+ -- Helper to parse array content (handles [[strings]] in arrays)
+ local function parse_array_item(item)
+ item = string.gsub(item, "^%s+", "")
+ item = string.gsub(item, "%s+$", "")
+ -- Check for long string placeholder
+ if long_strings[item] then
+ return long_strings[item]
+ elseif string.match(item, '^".-"$') then
+ return string.sub(item, 2, -2)
+ elseif string.match(item, "^'.-'$") then
+ return string.sub(item, 2, -2)
+ elseif item ~= "" then
+ return item
end
- return {}
+ return nil
end
- local success, manifest = pcall(manifest_func)
- if not success then
- if osprint then
- osprint("ERROR: Failed to execute manifest: " .. tostring(manifest) .. "\n")
+ -- Match key = value pairs (handles arrays with spaces like { "a", "b" })
+ -- Pattern: word = (value until next word= or end)
+ local pos = 1
+ while pos <= #manifest_code do
+ -- Skip whitespace, commas, and semicolons
+ local ws_end = string.match(manifest_code, "^[%s,;]*()", pos)
+ if ws_end then pos = ws_end end
+
+ -- Match key = value
+ local key, value_start = string.match(manifest_code, "^([%w_]+)%s*=%s*()", pos)
+ if not key then break end
+
+ pos = value_start
+
+ -- Determine value type and extract it
+ local value
+ local char = string.sub(manifest_code, pos, pos)
+
+ if char == "{" then
+ -- Array: find matching } by counting brace depth
+ local depth = 1
+ local brace_end = pos + 1
+ while brace_end <= #manifest_code and depth > 0 do
+ local c = string.sub(manifest_code, brace_end, brace_end)
+ if c == "{" then
+ depth = depth + 1
+ elseif c == "}" then
+ depth = depth - 1
+ end
+ brace_end = brace_end + 1
+ end
+ brace_end = brace_end - 1 -- Point to the closing }
+ if depth == 0 then
+ local arr_content = string.sub(manifest_code, pos + 1, brace_end - 1)
+ local arr = {}
+ for item in string.gmatch(arr_content, '[^,;]+') do
+ local parsed = parse_array_item(item)
+ if parsed then
+ table.insert(arr, parsed)
+ end
+ end
+ value = arr
+ pos = brace_end + 1
+ else
+ break
+ end
+ elseif char == "\001" then
+ -- Long string placeholder
+ local placeholder = string.match(manifest_code, "^(\001LONGSTR%d+\001)", pos)
+ if placeholder and long_strings[placeholder] then
+ value = long_strings[placeholder]
+ pos = pos + #placeholder
+ else
+ break
+ end
+ elseif char == '"' then
+ -- Double-quoted string
+ local str_end = string.find(manifest_code, '"', pos + 1, true)
+ if str_end then
+ value = string.sub(manifest_code, pos + 1, str_end - 1)
+ pos = str_end + 1
+ else
+ break
+ end
+ elseif char == "'" then
+ -- Single-quoted string
+ local str_end = string.find(manifest_code, "'", pos + 1, true)
+ if str_end then
+ value = string.sub(manifest_code, pos + 1, str_end - 1)
+ pos = str_end + 1
+ else
+ break
+ end
+ else
+ -- Unquoted value (boolean, number, or identifier)
+ local val_str = string.match(manifest_code, "^([^%s,;}]+)", pos)
+ if val_str then
+ if val_str == "true" then
+ value = true
+ elseif val_str == "false" then
+ value = false
+ elseif tonumber(val_str) then
+ value = tonumber(val_str)
+ else
+ value = val_str
+ end
+ pos = pos + #val_str
+ else
+ break
+ end
+ end
+
+ if key and value ~= nil then
+ manifest[key] = value
end
- return {}
end
- return manifest or {}
+ return manifest
end
-- Utility: Check if directory exists
@@ -272,57 +399,24 @@ local function extract_embedded_manifest(script_content)
return nil
end
- -- Find MANIFEST START and END markers
- local start_marker = "%-%-%-%-%-+ MANIFEST START"
- local end_marker = "%-%-%-%-%-+ MANIFEST END"
+ -- Look for --[[ MANIFEST ... --]] block comment format
+ local manifest_block = string.match(script_content, "%-%-%[%[%s*MANIFEST%s*(.-)%-%-%]%]")
- local start_pos = string.find(script_content, start_marker)
- if not start_pos then
+ if not manifest_block then
return nil
end
- local end_pos = string.find(script_content, end_marker, start_pos)
- if not end_pos then
- return nil
- end
-
- -- Extract the manifest block
- local manifest_block = string.sub(script_content, start_pos, end_pos)
-
- -- Convert comment lines to Lua code
- -- Remove leading "-- " from each line and build a return statement
- local lua_code = "return {\n"
- for line in string.gmatch(manifest_block, "[^\r\n]+") do
- -- Skip the marker lines
- if not string.match(line, "MANIFEST START") and
- not string.match(line, "MANIFEST END") then
- -- Remove leading "-- " or "--"
- local cleaned = string.gsub(line, "^%s*%-%-%s*", "")
- if cleaned ~= "" then
- lua_code = lua_code .. cleaned .. "\n"
- end
- end
- end
- lua_code = lua_code .. "}"
-
- -- Parse the manifest
- local manifest_func, err = load(lua_code, "embedded_manifest", "t")
- if not manifest_func then
- if osprint then
- osprint("Warning: Failed to parse embedded manifest: " .. tostring(err) .. "\n")
- end
- return nil
- end
+ -- Reuse parse_manifest which handles multi-line arrays and text-based parsing
+ local manifest = parse_manifest(manifest_block)
- local success, manifest = pcall(manifest_func)
- if not success then
- if osprint then
- osprint("Warning: Failed to execute embedded manifest: " .. tostring(manifest) .. "\n")
- end
- return nil
+ -- Return nil if manifest is empty
+ local has_keys = false
+ for _ in pairs(manifest) do
+ has_keys = true
+ break
end
- return manifest
+ return has_keys and manifest or nil
end
-- Run an installed app with sandboxing
@@ -363,9 +457,10 @@ function run.execute(app_name, fsRoot)
end
-- Extract embedded manifest
- manifest = extract_embedded_manifest(init_content)
+ local extracted_manifest = extract_embedded_manifest(init_content)
- if manifest then
+ if extracted_manifest then
+ manifest = extracted_manifest
run_print("Found embedded manifest\n")
-- Parse permissions from embedded manifest
@@ -388,6 +483,7 @@ function run.execute(app_name, fsRoot)
end
else
run_print("No embedded manifest found, running with no permissions\n")
+ manifest = {} -- Ensure manifest is an empty table, not nil
end
-- For standalone scripts, use a simple app name based on the file path
@@ -521,13 +617,25 @@ function run.execute(app_name, fsRoot)
}
-- Add optional globals only if they exist (avoid nil values in sandbox)
- if _G.crypto then
- sandbox_env.crypto = _G.crypto
- end
if _G.Dialog then
sandbox_env.Dialog = _G.Dialog
end
+ -- Check for crypto permission and add crypto object
+ local has_crypto = false
+ for _, perm in ipairs(permissions) do
+ if perm == "crypto" then
+ has_crypto = true
+ break
+ end
+ end
+ if has_crypto and _G.crypto then
+ sandbox_env.crypto = _G.crypto
+ if osprint then
+ osprint("Crypto permission granted - crypto object available\n")
+ end
+ end
+
-- Check for system-all permission and add sys object
local has_system_all = false
for _, perm in ipairs(permissions) do
@@ -555,46 +663,69 @@ function run.execute(app_name, fsRoot)
end
end
+ -- Check for system-apps permission
+ local has_system_apps = false
+ for _, perm in ipairs(permissions) do
+ if perm == "system-apps" then
+ has_system_apps = true
+ break
+ end
+ end
+
if has_system_all then
- if has_system_hook then
- -- Apps with system-hook permission get full sys access including hook
- sandbox_env.sys = _G.sys
- if osprint then
- osprint("System-all permission granted - sys object available (with hook)\n")
- end
- else
- -- Apps without system-hook get sys without hook
- sandbox_env.sys = setmetatable({}, {
- __index = function(t, k)
- if k == "hook" then
- error("sys.hook requires 'system-hook' permission")
+ -- Create a proxy for sys that:
+ -- 1. Blocks hook unless system-hook permission
+ -- 2. Exposes applications as 'apps' only if system-apps permission
+ -- 3. Blocks direct access to 'applications'
+ sandbox_env.sys = setmetatable({}, {
+ __index = function(t, k)
+ if k == "hook" and not has_system_hook then
+ error("sys.hook requires 'system-hook' permission")
+ end
+ if k == "applications" then
+ error("sys.applications is not accessible, use sys.apps with 'system-apps' permission")
+ end
+ if k == "apps" then
+ if has_system_apps then
+ return _G.sys.applications
+ else
+ error("sys.apps requires 'system-apps' permission")
end
- return _G.sys[k]
- end,
- __newindex = function(t, k, v)
- if k == "hook" then
- error("sys.hook requires 'system-hook' permission")
+ end
+ return _G.sys[k]
+ end,
+ __newindex = function(t, k, v)
+ if k == "hook" and not has_system_hook then
+ error("sys.hook requires 'system-hook' permission")
+ end
+ if k == "applications" or k == "apps" then
+ error("Cannot modify sys.apps")
+ end
+ _G.sys[k] = v
+ end,
+ __pairs = function(t)
+ return function(tbl, k)
+ local nextKey, nextValue = next(_G.sys, k)
+ -- Skip hook if no permission
+ if nextKey == "hook" and not has_system_hook then
+ nextKey, nextValue = next(_G.sys, nextKey)
end
- _G.sys[k] = v
- end,
- __pairs = function(t)
- return function(tbl, k)
- local nextKey, nextValue = next(_G.sys, k)
- -- Skip hook key
- if nextKey == "hook" then
- nextKey, nextValue = next(_G.sys, nextKey)
- end
- return nextKey, nextValue
- end, t, nil
- end,
- __ipairs = function(t)
- return ipairs(_G.sys)
- end,
- __metatable = false -- Prevent metatable access/modification
- })
- if osprint then
- osprint("System-all permission granted - sys object available (without hook)\n")
- end
+ -- Skip applications (exposed as apps)
+ if nextKey == "applications" then
+ nextKey, nextValue = next(_G.sys, nextKey)
+ end
+ return nextKey, nextValue
+ end, t, nil
+ end,
+ __ipairs = function(t)
+ return ipairs(_G.sys)
+ end,
+ __metatable = false -- Prevent metatable access/modification
+ })
+ if osprint then
+ osprint("System-all permission granted - sys object available\n")
+ if has_system_hook then osprint(" - sys.hook available\n") end
+ if has_system_apps then osprint(" - sys.apps available\n") end
end
end
@@ -619,7 +750,7 @@ function run.execute(app_name, fsRoot)
end
end
-- Enable SafeFS if manifest has allowedPaths defined
- if not has_filesystem and manifest.allowedPaths and #manifest.allowedPaths > 0 then
+ if not has_filesystem and manifest and manifest.allowedPaths and #manifest.allowedPaths > 0 then
has_filesystem = true
end
@@ -826,8 +957,44 @@ function run.execute(app_name, fsRoot)
end
end
+ -- Create a proxy for app that only exposes safe methods
+ -- This prevents apps from directly modifying exports, windows, etc.
+ local safe_app_methods = {
+ "export", "call", "getExport", "listExports", "getInfo",
+ "getExportCount", "getHelp", "printExports",
+ "writeStdout", "getStdout", "clearStdout",
+ "newWindow", "enterFullscreen", "exitFullscreen"
+ }
+ -- Also expose these read-only properties
+ local safe_app_properties = {
+ "appName", "pid", "startTime", "status"
+ }
+ local app_proxy = {}
+ for _, method in ipairs(safe_app_methods) do
+ if app_instance[method] then
+ app_proxy[method] = function(self, ...)
+ return app_instance[method](app_instance, ...)
+ end
+ end
+ end
+ setmetatable(app_proxy, {
+ __index = function(t, k)
+ -- Allow read-only access to safe properties
+ for _, prop in ipairs(safe_app_properties) do
+ if k == prop then
+ return app_instance[k]
+ end
+ end
+ error("app." .. tostring(k) .. " is not accessible", 2)
+ end,
+ __newindex = function(t, k, v)
+ error("Cannot modify app object", 2)
+ end,
+ __metatable = false
+ })
+
-- Add to sandbox environment
- sandbox_env.app = app_instance
+ sandbox_env.app = app_proxy
-- Add Timer API scoped to this app
if _G.Timer then
@@ -1048,7 +1215,7 @@ function run.execute(app_name, fsRoot)
local allowed_paths = {data_dir .. "/*"} -- Always allow data directory
-- Add additional paths from manifest.allowedPaths
- if manifest.allowedPaths and type(manifest.allowedPaths) == "table" then
+ if manifest and manifest.allowedPaths and type(manifest.allowedPaths) == "table" then
for _, path in ipairs(manifest.allowedPaths) do
if type(path) == "string" then
-- Replace $ with app directory
@@ -1132,10 +1299,38 @@ function run.execute(app_name, fsRoot)
end
end
+ -- Create a proxy table that only exposes safe methods
+ -- This prevents apps from accessing internal properties like allowedDirs
+ -- Note: createDiskFSMount and mountAllDiskDrives are NOT exposed - they're internal only
+ local safe_fs_methods = {
+ "resolvePath", "getCWD", "setCWD", "read", "write", "open",
+ "delete", "dirs", "files", "exists", "getType", "mkdir",
+ "fileName", "parentDir", "join", "relativeTo", "copy", "move",
+ "createPseudoFile", "createPseudoDir", "addFileHandler",
+ "removeFileHandler"
+ }
+ local fs_proxy = {}
+ for _, method in ipairs(safe_fs_methods) do
+ if fs_instance[method] then
+ fs_proxy[method] = function(self, ...)
+ return fs_instance[method](fs_instance, ...)
+ end
+ end
+ end
+ setmetatable(fs_proxy, {
+ __index = function(t, k)
+ error("fs." .. tostring(k) .. " is not accessible", 2)
+ end,
+ __newindex = function(t, k, v)
+ error("Cannot modify fs object", 2)
+ end,
+ __metatable = false
+ })
+
-- Add to sandbox environment
- sandbox_env.fs = fs_instance
+ sandbox_env.fs = fs_proxy
if osprint then
- osprint("[RUN] Set sandbox_env.fs for " .. app_name .. ", fs=" .. tostring(fs_instance) .. "\n")
+ osprint("[RUN] Set sandbox_env.fs (proxied) for " .. app_name .. "\n")
end
else
if osprint then
@@ -1182,7 +1377,7 @@ function run.execute(app_name, fsRoot)
if success and SafeHTTP then
-- Get allowed domains from manifest
- local allowed_domains = manifest.allowedDomains or {}
+ local allowed_domains = (manifest and manifest.allowedDomains) or {}
-- If no domains specified but network permission granted, allow all (backwards compat)
if #allowed_domains == 0 then
@@ -1197,7 +1392,7 @@ function run.execute(app_name, fsRoot)
local http_instance = SafeHTTP.new(_G.NetworkStack, allowed_domains, {
timeout = 30,
max_size = 10485760, -- 10MB default
- user_agent = manifest.name .. "/" .. (manifest.version or "1.0")
+ user_agent = (manifest and manifest.name or "Script") .. "/" .. (manifest and manifest.version or "1.0")
})
-- Add to sandbox environment
@@ -1877,19 +2072,34 @@ function run.execute(app_name, fsRoot)
end
else
-- Standard sandbox: prevent access to undefined globals
+ -- Track which keys are allowed (even if nil) vs truly undefined
+ local allowed_keys = {}
+ for k, _ in pairs(sandbox_env) do
+ allowed_keys[k] = true
+ end
+ -- Also allow keys that were explicitly set to nil
+ allowed_keys.fs = true
+ allowed_keys.crypto = true
+ allowed_keys.sys = true
+ allowed_keys.window = true
+
setmetatable(sandbox_env, {
__index = function(t, k)
+ if allowed_keys[k] then
+ return nil -- Key is allowed but not set, return nil
+ end
error("Attempt to access undefined global: " .. tostring(k), 2)
end,
__newindex = function(t, k, v)
- rawset(t, k, v) -- Allow setting new globals within sandbox
+ allowed_keys[k] = true -- Mark as allowed when set
+ rawset(t, k, v)
end,
__metatable = false -- Prevent metatable access/modification
})
end
-- Register sandbox environment with sys (after sandbox is fully set up)
- local app_instance = rawget(sandbox_env, "app")
+ -- Note: app_instance is already defined above, don't redefine it from the proxy
if app_instance and app_instance.pid and _G.sys and _G.sys.registerEnvironment then
if osprint then
osprint("[run] Registering sandbox environment for PID " .. app_instance.pid .. "\n")
@@ -1926,6 +2136,18 @@ function run.execute(app_name, fsRoot)
osprint("[DEBUG] HTML type app detected, creating HTML window\n")
end
+ -- Extract window dimensions from HTML meta tags
+ -- Supports: <meta name="window-width" content="500"> or value="500"
+ local html_width = manifest.width or 800
+ local html_height = manifest.height or 600
+ -- Try content= first, then value=
+ local meta_width = string.match(init_content, '<meta[^>]+name=["\']window%-width["\'][^>]+content=["\'](%d+)["\']')
+ or string.match(init_content, '<meta[^>]+name=["\']window%-width["\'][^>]+value=["\'](%d+)["\']')
+ local meta_height = string.match(init_content, '<meta[^>]+name=["\']window%-height["\'][^>]+content=["\'](%d+)["\']')
+ or string.match(init_content, '<meta[^>]+name=["\']window%-height["\'][^>]+value=["\'](%d+)["\']')
+ if meta_width then html_width = tonumber(meta_width) or html_width end
+ if meta_height then html_height = tonumber(meta_height) or html_height end
+
-- For HTML apps, init_content is HTML, not Lua
local htmlWindow, htmlErr
@@ -1935,8 +2157,8 @@ function run.execute(app_name, fsRoot)
html = init_content,
file = init_file,
fs = rawget(sandbox_env, "fs"), -- Use rawget to avoid metatable errors
- width = manifest.width or 800,
- height = manifest.height or 600,
+ width = html_width,
+ height = html_height,
title = manifest.name or "HTML App",
Dialog = rawget(sandbox_env, "Dialog"),
loadstring = rawget(sandbox_env, "loadstring"),
@@ -1972,14 +2194,14 @@ function run.execute(app_name, fsRoot)
end
-- Attach CLI buffer to app instance for proper output handling
- if sandbox_env.app and sandbox_env.app.attachCLI then
- sandbox_env.app:attachCLI(cli)
+ if app_instance and app_instance.attachCLI then
+ app_instance:attachCLI(cli)
if osprint then
osprint("[run] Attached CLI to HTML app instance\n")
end
end
- return true, sandbox_env.app
+ return true, app_instance
end
-- Load and execute the app in the sandbox (Lua apps)
@@ -2094,11 +2316,11 @@ function run.execute(app_name, fsRoot)
print("Error: App execution failed: " .. result)
-- Mark app as stopped if it exists
- if Application and sandbox_env.app then
- sandbox_env.app:setStatus("stopped", "execution_error: " .. tostring(result))
+ if Application and app_instance then
+ app_instance:setStatus("stopped", "execution_error: " .. tostring(result))
-- Clean up /proc/$PID directory
- local pid = sandbox_env.app.pid
+ local pid = app_instance.pid
if pid and CRamdiskRemove then
local proc_dir = "/proc/" .. pid
CRamdiskRemove(proc_dir .. "/process")
@@ -2129,21 +2351,21 @@ function run.execute(app_name, fsRoot)
-- Check if app should keep running (has windows or callbacks)
local keepRunning = false
- if Application and sandbox_env.app then
- if sandbox_env.app.windows and #sandbox_env.app.windows > 0 then
+ if Application and app_instance then
+ if app_instance.windows and #app_instance.windows > 0 then
keepRunning = true
if osprint then
- osprint("App has " .. #sandbox_env.app.windows .. " windows, keeping it running\n")
+ osprint("App has " .. #app_instance.windows .. " windows, keeping it running\n")
end
end
end
-- Only mark app as stopped and clean up if it has no windows
- if not keepRunning and Application and sandbox_env.app then
- sandbox_env.app:setStatus("stopped", "no_windows")
+ if not keepRunning and Application and app_instance then
+ app_instance:setStatus("stopped", "no_windows")
-- Clean up /proc/$PID directory
- local pid = sandbox_env.app.pid
+ local pid = app_instance.pid
if pid and CRamdiskRemove then
local proc_dir = "/proc/" .. pid
CRamdiskRemove(proc_dir .. "/process")
@@ -2168,8 +2390,8 @@ function run.execute(app_name, fsRoot)
end
-- Attach CLI buffer to app instance for easy access
- if sandbox_env.app and sandbox_env.cli then
- sandbox_env.app.cli = sandbox_env.cli
+ if app_instance and sandbox_env.cli then
+ app_instance.cli = sandbox_env.cli
if osprint then
osprint("[run] Attached CLI to app instance\n")
osprint("[run] CLI has getText: " .. tostring(sandbox_env.cli.getText ~= nil) .. "\n")
@@ -2182,7 +2404,7 @@ function run.execute(app_name, fsRoot)
end
-- Return success and the app instance (so caller can get PID, stdout, etc)
- return true, sandbox_env.app
+ return true, app_instance
end
-- Export to global namespace for easy access
diff --git a/iso_includes/scripts/test.lua b/iso_includes/scripts/test.lua
@@ -1,12 +1,23 @@
---[[MANIFEST
-{
- permissions = {"filesystem", "draw"}
-}
-]]
+--[[ MANIFEST
+ name = "Test Script"
+ permissions = {"filesystem", "crypto"}
+--]]
+
osprint("Test OSPrint")
print("Hi from TEST PROGRAM!")
-
-for k,v in pairs(crypto) do
- print("crypto", k, v)
+function testTable(tab, tabname)
+ if tab then
+ for k,v in pairs(tab) do
+ print(tabname, k, v)
+ end
+ else
+ print("no "..tabname)
+ end
end
+
+testTable(crypto, "crypto")
+testTable(fs, "fs")
+testTable(sys, "sys")
+testTable(app, "app")
+testTable(window, "window")
diff --git a/repack_lua.sh b/repack_lua.sh
@@ -0,0 +1,148 @@
+#!/bin/bash
+
+# Fast Lua-only rebuild script
+# Recompiles Lua files (excluding manifest.lua and /scripts/*), repacks ramdisk, and creates ISO
+# Much faster than full build.sh since it skips C compilation
+
+set -e # Exit on error
+
+echo "=== LuajitOS Fast Lua Repack ==="
+
+# Check if we have the required compiled objects
+if [ ! -f "build/kernel.o" ]; then
+ echo "ERROR: build/kernel.o not found!"
+ echo "Please run ./build.sh first to compile C code."
+ exit 1
+fi
+
+# Configuration for linking
+CROSS_COMPILE=i686-elf-
+LD="${CROSS_COMPILE}ld"
+OBJCOPY="${CROSS_COMPILE}objcopy"
+
+# Check if cross-compiler exists, fallback to native
+if ! command -v ${LD} &> /dev/null; then
+ LD="ld"
+ OBJCOPY="objcopy"
+fi
+
+LDFLAGS="-m elf_i386 -nostdlib -static"
+
+# Step 1: Recompile Lua files (excluding manifest.lua and scripts/*)
+echo "Step 1: Recompiling Lua files..."
+if command -v luajit &> /dev/null; then
+ find iso_includes -name "*.lua" | while read lua_file; do
+ # Skip manifest.lua files
+ if [[ "$lua_file" == */manifest.lua ]]; then
+ continue
+ fi
+ # Skip scripts directory
+ if [[ "$lua_file" == iso_includes/scripts/* ]]; then
+ continue
+ fi
+
+ luac_file="${lua_file}c"
+ # Only recompile if .lua is newer than .luac or .luac doesn't exist
+ if [ ! -f "$luac_file" ] || [ "$lua_file" -nt "$luac_file" ]; then
+ echo " Compiling: $lua_file"
+ luajit -bW "$lua_file" "$luac_file" 2>/dev/null || true
+ fi
+ done
+ echo " Bytecode compilation complete"
+else
+ echo "ERROR: luajit not found!"
+ exit 1
+fi
+
+# Step 2: Generate ramdisk
+echo "Step 2: Generating ramdisk..."
+if [ -d iso_includes ]; then
+ # Remove any previously embedded kernel to prevent recursive embedding
+ rm -rf iso_includes/os/boot
+
+ # If we have a kernel for install, include it
+ if [ -f build/kernel_install.bin ]; then
+ mkdir -p iso_includes/os/boot
+ cp build/kernel_install.bin iso_includes/os/boot/kernel.bin
+ echo " Including install kernel ($(stat -c%s build/kernel_install.bin 2>/dev/null || stat -f%z build/kernel_install.bin 2>/dev/null || echo '?') bytes)"
+ fi
+
+ luajit pack_ramdisk.lua iso_includes packed.bin
+ if [ $? -ne 0 ]; then
+ echo "Error: Failed to generate ramdisk"
+ exit 1
+ fi
+else
+ echo "ERROR: iso_includes directory not found!"
+ exit 1
+fi
+
+# Step 3: Convert packed.bin to object file
+echo "Step 3: Converting packed.bin to object file..."
+${OBJCOPY} -I binary -O elf32-i386 -B i386 \
+ --rename-section .data=.rodata,alloc,load,readonly,data,contents \
+ packed.bin build/packed.o
+echo " Embedded packed.bin ($(stat -c%s packed.bin 2>/dev/null || stat -f%z packed.bin 2>/dev/null || echo '?') bytes)"
+
+# Step 4: Re-link kernel
+echo "Step 4: Linking kernel..."
+
+# Find LuaJIT static library
+LUAJIT_LIB=""
+if [ -f "lib/libluajit-5.1-32bit.a" ]; then
+ LUAJIT_LIB="lib/libluajit-5.1-32bit.a"
+elif [ -f "/usr/lib/i386-linux-gnu/libluajit-5.1.a" ]; then
+ LUAJIT_LIB="/usr/lib/i386-linux-gnu/libluajit-5.1.a"
+else
+ echo "ERROR: Could not find 32-bit LuaJIT static library!"
+ exit 1
+fi
+
+# Find libgcc
+CC="gcc"
+if command -v i686-elf-gcc &> /dev/null; then
+ CC="i686-elf-gcc"
+fi
+LIBGCC_PATH=$(${CC} -m32 -print-libgcc-file-name 2>/dev/null)
+LIBGCC=""
+if [ -f "$LIBGCC_PATH" ]; then
+ LIBGCC="$LIBGCC_PATH"
+fi
+
+# Link all objects together
+${LD} ${LDFLAGS} -T linker.ld -o build/kernel.bin \
+ build/boot.o build/syscall.o build/exceptions.o build/libc.o build/paging.o build/graphics.o build/vesa.o build/mouse.o build/usb_uhci.o build/keyboard.o build/ata.o build/fde.o build/diskfs.o build/partition.o build/fat16.o \
+ build/decoder.o build/decoder_BMP.o build/decoder_PNG.o build/splash.o \
+ build/compression.o build/deflate_impl.o build/Deflate.o build/LZMA.o build/GZip.o build/zlib.o \
+ build/ramdisk.o build/luajit_init.o build/kernel.o \
+ build/crypto_baremetal.o build/CSPRNG.o build/Ed25519.o build/X25519.o build/Curve25519.o \
+ build/AES-256-GCM.o build/AES-128-GCM.o build/ChaCha20-Poly1305.o build/XChaCha20-Poly1305.o \
+ build/Serpent-256-GCM.o build/Twofish-256-GCM.o build/Salsa20-Poly1305.o \
+ build/PBKDF2.o build/Argon2.o build/stubs.o build/RSA.o build/P256.o \
+ build/Kyber.o build/Dilithium.o \
+ build/hash.o build/MD5.o build/SHA1.o build/SHA256.o build/SHA3.o build/BLAKE2.o \
+ build/CSPRNG_Lua.o build/Ed25519_Lua.o build/X25519_Lua.o build/Hash_Lua.o \
+ build/PBKDF2_Lua.o build/RSA_Lua.o build/P256_Lua.o \
+ build/Kyber_Lua.o build/Dilithium_Lua.o \
+ build/crypto.o \
+ build/packed.o ${LUAJIT_LIB} ${LIBGCC}
+
+# Step 5: Create ISO
+echo "Step 5: Creating ISO..."
+cp build/kernel.bin isodir/boot/kernel.bin
+
+if command -v grub-mkrescue &> /dev/null; then
+ grub-mkrescue -o luajitos.iso isodir 2>&1
+elif command -v grub2-mkrescue &> /dev/null; then
+ grub2-mkrescue -o luajitos.iso isodir 2>&1
+else
+ echo "ERROR: grub-mkrescue not found!"
+ exit 1
+fi
+
+echo ""
+echo "=== Repack complete! ==="
+echo "ISO updated: luajitos.iso"
+echo ""
+echo "To test with QEMU, run:"
+echo " qemu-system-x86_64 -cdrom luajitos.iso -m 512 -serial stdio -vga std -display sdl"