Compare commits

..

No commits in common. "74faa76dbddcd06690aefa52ee22b2c77f6adba3" and "e95f0f33700c752c388adff08b41c55ba947284d" have entirely different histories.

7 changed files with 188 additions and 612 deletions

2
.gitignore vendored
View File

@ -23,6 +23,6 @@ luajit/.git
go.work go.work
# Test directories and files # Test directories and files
/*.lua test.lua
test_fs_dir test_fs_dir
public public

View File

@ -4,9 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"net" "net"
"path/filepath"
"runtime" "runtime"
"strings"
"sync" "sync"
"time" "time"
@ -20,8 +18,6 @@ var (
globalStateCreator StateCreator globalStateCreator StateCreator
globalMu sync.RWMutex globalMu sync.RWMutex
serverRunning bool serverRunning bool
staticHandlers = make(map[string]*fasthttp.FS)
staticMu sync.RWMutex
) )
func SetStateCreator(creator StateCreator) { func SetStateCreator(creator StateCreator) {
@ -35,7 +31,6 @@ func GetFunctionList() map[string]luajit.GoFunction {
"http_listen": http_listen, "http_listen": http_listen,
"http_close_server": http_close_server, "http_close_server": http_close_server,
"http_has_servers": http_has_servers, "http_has_servers": http_has_servers,
"http_register_static": http_register_static,
} }
} }
@ -167,32 +162,6 @@ func http_has_servers(s *luajit.State) int {
return 1 return 1
} }
func http_register_static(s *luajit.State) int {
if err := s.CheckMinArgs(2); err != nil {
return s.PushError("http_register_static: %v", err)
}
urlPrefix := s.ToString(1)
rootPath := s.ToString(2)
// Ensure prefix starts with /
if !strings.HasPrefix(urlPrefix, "/") {
urlPrefix = "/" + urlPrefix
}
// Convert to absolute path
absPath, err := filepath.Abs(rootPath)
if err != nil {
s.PushBoolean(false)
s.PushString(fmt.Sprintf("invalid path: %v", err))
return 2
}
RegisterStaticHandler(urlPrefix, absPath)
s.PushBoolean(true)
return 1
}
func HasActiveServers() bool { func HasActiveServers() bool {
globalMu.RLock() globalMu.RLock()
defer globalMu.RUnlock() defer globalMu.RUnlock()
@ -206,22 +175,6 @@ func WaitForServers() {
} }
func handleRequest(ctx *fasthttp.RequestCtx) { func handleRequest(ctx *fasthttp.RequestCtx) {
path := string(ctx.Path())
// Check static handlers first
staticMu.RLock()
for prefix, fs := range staticHandlers {
if strings.HasPrefix(path, prefix) {
staticMu.RUnlock()
// Remove prefix and serve
ctx.Request.URI().SetPath(strings.TrimPrefix(path, prefix))
fs.NewRequestHandler()(ctx)
return
}
}
staticMu.RUnlock()
// Fall back to Lua handling
globalMu.RLock() globalMu.RLock()
pool := globalWorkerPool pool := globalWorkerPool
globalMu.RUnlock() globalMu.RUnlock()
@ -277,19 +230,3 @@ func handleRequest(ctx *fasthttp.RequestCtx) {
ctx.SetBodyString(resp.Body) ctx.SetBodyString(resp.Body)
} }
} }
// RegisterStaticHandler adds a static file handler
func RegisterStaticHandler(urlPrefix, rootPath string) {
staticMu.Lock()
defer staticMu.Unlock()
fs := &fasthttp.FS{
Root: rootPath,
IndexNames: []string{"index.html"},
GenerateIndexPages: false,
Compress: true,
AcceptByteRange: true,
}
staticHandlers[urlPrefix] = fs
}

View File

@ -523,18 +523,48 @@ function http.cors(options)
end end
end end
function http.static(root_path, url_prefix) function http.static(root_path)
url_prefix = url_prefix or "/"
if not _G.__IS_WORKER then
local success, err = moonshark.http_register_static(url_prefix, root_path)
if not success then
error("Failed to register static handler: " .. (err or "unknown error"))
end
end
-- Return no-op middleware
return function(req, res, next) return function(req, res, next)
if req.method ~= "GET" and req.method ~= "HEAD" then
next()
return
end
local file_path = moonshark.path_join(root_path, req.path)
file_path = moonshark.path_clean(file_path)
local abs_root = moonshark.path_abs(root_path)
local abs_file = moonshark.path_abs(file_path)
if not abs_file or not abs_file:find("^" .. abs_root:gsub("([%(%)%.%+%-%*%?%[%]%^%$%%])", "%%%1")) then
next()
return
end
if moonshark.file_exists(file_path) and not moonshark.file_is_dir(file_path) then
local content = moonshark.file_read(file_path)
if content then
local ext = moonshark.path_ext(file_path):lower()
local content_types = {
[".html"] = "text/html",
[".css"] = "text/css",
[".js"] = "application/javascript",
[".json"] = "application/json",
[".png"] = "image/png",
[".jpg"] = "image/jpeg",
[".jpeg"] = "image/jpeg",
[".gif"] = "image/gif",
[".svg"] = "image/svg+xml",
[".webp"] = "image/webp",
[".txt"] = "text/plain",
}
local content_type = content_types[ext] or "application/octet-stream"
res:type(content_type):send(content)
return
end
end
next() next()
end end
end end

45
modules/json/json.go Normal file
View File

@ -0,0 +1,45 @@
package json
import (
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
"github.com/goccy/go-json"
)
func GetFunctionList() map[string]luajit.GoFunction {
return map[string]luajit.GoFunction{
"json_encode": json_encode,
"json_decode": json_decode,
}
}
func json_encode(s *luajit.State) int {
value, err := s.ToValue(1)
if err != nil {
s.PushNil()
s.PushString("failed to read value")
return 2
}
data, err := json.Marshal(value)
if err != nil {
s.PushNil()
s.PushString("encoding failed")
return 2
}
s.PushString(string(data))
return 1
}
func json_decode(s *luajit.State) int {
jsonStr := s.ToString(1)
var result any
if err := json.Unmarshal([]byte(jsonStr), &result); err != nil {
s.PushNil()
s.PushString("invalid JSON")
return 2
}
s.PushValue(result)
return 1
}

View File

@ -1,355 +1,72 @@
-- modules/json.lua - High-performance JSON module -- modules/json.lua - High-performance JSON module using Go functions
local json = {} local json = {}
-- Use the fast Go JSON encoder/decoder
function json.encode(value) function json.encode(value)
local buffer = {} return moonshark.json_encode(value)
local pos = 1
local function encode_string(s)
buffer[pos] = '"'
pos = pos + 1
local start = 1
for i = 1, #s do
local c = s:byte(i)
if c == 34 then -- "
if i > start then
buffer[pos] = s:sub(start, i - 1)
pos = pos + 1
end
buffer[pos] = '\\"'
pos = pos + 1
start = i + 1
elseif c == 92 then -- \
if i > start then
buffer[pos] = s:sub(start, i - 1)
pos = pos + 1
end
buffer[pos] = '\\\\'
pos = pos + 1
start = i + 1
elseif c < 32 then
if i > start then
buffer[pos] = s:sub(start, i - 1)
pos = pos + 1
end
if c == 8 then
buffer[pos] = '\\b'
elseif c == 9 then
buffer[pos] = '\\t'
elseif c == 10 then
buffer[pos] = '\\n'
elseif c == 12 then
buffer[pos] = '\\f'
elseif c == 13 then
buffer[pos] = '\\r'
else
buffer[pos] = ('\\u%04x'):format(c)
end
pos = pos + 1
start = i + 1
end
end
if start <= #s then
buffer[pos] = s:sub(start)
pos = pos + 1
end
buffer[pos] = '"'
pos = pos + 1
end
local function encode_value(v, depth)
local t = type(v)
if t == 'string' then
encode_string(v)
elseif t == 'number' then
if v ~= v then -- NaN
buffer[pos] = 'null'
elseif v == 1/0 or v == -1/0 then -- Infinity
buffer[pos] = 'null'
else
buffer[pos] = tostring(v)
end
pos = pos + 1
elseif t == 'boolean' then
buffer[pos] = v and 'true' or 'false'
pos = pos + 1
elseif t == 'table' then
if depth > 100 then error('circular reference') end
local is_array = true
local max_index = 0
local count = 0
for k, _ in pairs(v) do
count = count + 1
if type(k) ~= 'number' or k <= 0 or k % 1 ~= 0 then
is_array = false
break
end
if k > max_index then max_index = k end
end
if is_array and count == max_index then
buffer[pos] = '['
pos = pos + 1
for i = 1, max_index do
if i > 1 then
buffer[pos] = ','
pos = pos + 1
end
encode_value(v[i], depth + 1)
end
buffer[pos] = ']'
pos = pos + 1
else
buffer[pos] = '{'
pos = pos + 1
local first = true
for k, val in pairs(v) do
if not first then
buffer[pos] = ','
pos = pos + 1
end
first = false
encode_string(tostring(k))
buffer[pos] = ':'
pos = pos + 1
encode_value(val, depth + 1)
end
buffer[pos] = '}'
pos = pos + 1
end
else
buffer[pos] = 'null'
pos = pos + 1
end
end
encode_value(value, 0)
return table.concat(buffer)
end end
function json.decode(str) function json.decode(str)
local pos = 1 local result, err = moonshark.json_decode(str)
local len = #str if result == nil and err then
error("json_decode: " .. err)
local function skip_whitespace()
while pos <= len do
local c = str:byte(pos)
if c ~= 32 and c ~= 9 and c ~= 10 and c ~= 13 then break end
pos = pos + 1
end end
end
local function decode_string()
local start = pos + 1
pos = pos + 1
while pos <= len do
local c = str:byte(pos)
if c == 34 then -- "
local result = str:sub(start, pos - 1)
pos = pos + 1
if result:find('\\') then
result = result:gsub('\\(.)', {
['"'] = '"',
['\\'] = '\\',
['/'] = '/',
['b'] = '\b',
['f'] = '\f',
['n'] = '\n',
['r'] = '\r',
['t'] = '\t'
})
result = result:gsub('\\u(%x%x%x%x)', function(hex)
return string.char(tonumber(hex, 16))
end)
end
return result
elseif c == 92 then -- \
pos = pos + 2
else
pos = pos + 1
end
end
error('unterminated string')
end
local function decode_number()
local start = pos
local c = str:byte(pos)
if c == 45 then pos = pos + 1 end -- -
c = str:byte(pos)
if not c or c < 48 or c > 57 then error('invalid number') end
if c == 48 then
pos = pos + 1
else
while pos <= len do
c = str:byte(pos)
if c < 48 or c > 57 then break end
pos = pos + 1
end
end
if pos <= len and str:byte(pos) == 46 then -- .
pos = pos + 1
local found_digit = false
while pos <= len do
c = str:byte(pos)
if c < 48 or c > 57 then break end
found_digit = true
pos = pos + 1
end
if not found_digit then error('invalid number') end
end
if pos <= len then
c = str:byte(pos)
if c == 101 or c == 69 then -- e or E
pos = pos + 1
if pos <= len then
c = str:byte(pos)
if c == 43 or c == 45 then pos = pos + 1 end -- + or -
end
local found_digit = false
while pos <= len do
c = str:byte(pos)
if c < 48 or c > 57 then break end
found_digit = true
pos = pos + 1
end
if not found_digit then error('invalid number') end
end
end
return tonumber(str:sub(start, pos - 1))
end
local function decode_value()
skip_whitespace()
if pos > len then error('unexpected end') end
local c = str:byte(pos)
if c == 34 then -- "
return decode_string()
elseif c == 123 then -- {
local result = {}
pos = pos + 1
skip_whitespace()
if pos <= len and str:byte(pos) == 125 then -- }
pos = pos + 1
return result
end
while true do
skip_whitespace()
if pos > len or str:byte(pos) ~= 34 then error('expected string key') end
local key = decode_string()
skip_whitespace()
if pos > len or str:byte(pos) ~= 58 then error('expected :') end
pos = pos + 1
result[key] = decode_value()
skip_whitespace()
if pos > len then error('unexpected end') end
c = str:byte(pos)
if c == 125 then -- }
pos = pos + 1
return result
elseif c == 44 then -- ,
pos = pos + 1
else
error('expected , or }')
end
end
elseif c == 91 then -- [
local result = {}
local index = 1
pos = pos + 1
skip_whitespace()
if pos <= len and str:byte(pos) == 93 then -- ]
pos = pos + 1
return result
end
while true do
result[index] = decode_value()
index = index + 1
skip_whitespace()
if pos > len then error('unexpected end') end
c = str:byte(pos)
if c == 93 then -- ]
pos = pos + 1
return result
elseif c == 44 then -- ,
pos = pos + 1
else
error('expected , or ]')
end
end
elseif c == 116 then -- true
if str:sub(pos, pos + 3) == 'true' then
pos = pos + 4
return true
end
error('invalid literal')
elseif c == 102 then -- false
if str:sub(pos, pos + 4) == 'false' then
pos = pos + 5
return false
end
error('invalid literal')
elseif c == 110 then -- null
if str:sub(pos, pos + 3) == 'null' then
pos = pos + 4
return nil
end
error('invalid literal')
elseif (c >= 48 and c <= 57) or c == 45 then -- 0-9 or -
return decode_number()
else
error('unexpected character')
end
end
local result = decode_value()
skip_whitespace()
if pos <= len then error('unexpected content after JSON') end
return result return result
end end
-- Pretty print JSON with indentation
function json.pretty(value, indent)
indent = indent or 2
local encoded = json.encode(value)
local result = {}
local depth = 0
local in_string = false
local escape_next = false
for i = 1, #encoded do
local char = encoded:sub(i, i)
if escape_next then
table.insert(result, char)
escape_next = false
elseif char == "\\" and in_string then
table.insert(result, char)
escape_next = true
elseif char == '"' then
table.insert(result, char)
in_string = not in_string
elseif not in_string then
if char == "{" or char == "[" then
table.insert(result, char)
depth = depth + 1
table.insert(result, "\n" .. string.rep(" ", depth * indent))
elseif char == "}" or char == "]" then
depth = depth - 1
table.insert(result, "\n" .. string.rep(" ", depth * indent))
table.insert(result, char)
elseif char == "," then
table.insert(result, char)
table.insert(result, "\n" .. string.rep(" ", depth * indent))
elseif char == ":" then
table.insert(result, char .. " ")
else
table.insert(result, char)
end
else
table.insert(result, char)
end
end
return table.concat(result)
end
-- Load JSON from file
function json.load_file(filename) function json.load_file(filename)
if not moonshark.file_exists(filename) then
error("File not found: " .. filename)
end
local file = io.open(filename, "r") local file = io.open(filename, "r")
if not file then if not file then
error("Cannot open file: " .. filename) error("Cannot open file: " .. filename)
@ -361,20 +78,28 @@ function json.load_file(filename)
return json.decode(content) return json.decode(content)
end end
function json.save_file(filename, data) -- Save data to JSON file
function json.save_file(filename, data, pretty)
local content
if pretty then
content = json.pretty(data)
else
content = json.encode(data)
end
local file = io.open(filename, "w") local file = io.open(filename, "w")
if not file then if not file then
error("Cannot write to file: " .. filename) error("Cannot write to file: " .. filename)
end end
file:write(json.encode(data)) file:write(content)
file:close() file:close()
end end
-- Merge JSON objects
function json.merge(...) function json.merge(...)
local result = {} local result = {}
local n = select("#", ...) for i = 1, select("#", ...) do
for i = 1, n do
local obj = select(i, ...) local obj = select(i, ...)
if type(obj) == "table" then if type(obj) == "table" then
for k, v in pairs(obj) do for k, v in pairs(obj) do
@ -385,22 +110,20 @@ function json.merge(...)
return result return result
end end
-- Extract values by JSONPath-like syntax (simplified)
function json.extract(data, path) function json.extract(data, path)
local parts = moonshark.string_split(path, ".")
local current = data local current = data
local start = 1
local len = #path
while start <= len do
local dot_pos = path:find(".", start, true)
local part = dot_pos and path:sub(start, dot_pos - 1) or path:sub(start)
for _, part in ipairs(parts) do
if type(current) ~= "table" then if type(current) ~= "table" then
return nil return nil
end end
local bracket_start, bracket_end = part:find("^%[(%d+)%]$") -- Handle array indices [0], [1], etc.
if bracket_start then local array_match = part:match("^%[(%d+)%]$")
local index = tonumber(part:sub(2, -2)) + 1 if array_match then
local index = tonumber(array_match) + 1 -- Lua is 1-indexed
current = current[index] current = current[index]
else else
current = current[part] current = current[part]
@ -409,163 +132,12 @@ function json.extract(data, path)
if current == nil then if current == nil then
return nil return nil
end end
start = dot_pos and dot_pos + 1 or len + 1
end end
return current return current
end end
function json.pretty(value, indent) -- Validate JSON structure against schema (basic)
local buffer = {}
local pos = 1
indent = indent or " "
local function encode_string(s)
buffer[pos] = '"'
pos = pos + 1
local start = 1
for i = 1, #s do
local c = s:byte(i)
if c == 34 then -- "
if i > start then
buffer[pos] = s:sub(start, i - 1)
pos = pos + 1
end
buffer[pos] = '\\"'
pos = pos + 1
start = i + 1
elseif c == 92 then -- \
if i > start then
buffer[pos] = s:sub(start, i - 1)
pos = pos + 1
end
buffer[pos] = '\\\\'
pos = pos + 1
start = i + 1
elseif c < 32 then
if i > start then
buffer[pos] = s:sub(start, i - 1)
pos = pos + 1
end
if c == 8 then
buffer[pos] = '\\b'
elseif c == 9 then
buffer[pos] = '\\t'
elseif c == 10 then
buffer[pos] = '\\n'
elseif c == 12 then
buffer[pos] = '\\f'
elseif c == 13 then
buffer[pos] = '\\r'
else
buffer[pos] = ('\\u%04x'):format(c)
end
pos = pos + 1
start = i + 1
end
end
if start <= #s then
buffer[pos] = s:sub(start)
pos = pos + 1
end
buffer[pos] = '"'
pos = pos + 1
end
local function encode_value(v, depth)
local t = type(v)
local current_indent = string.rep(indent, depth)
local next_indent = string.rep(indent, depth + 1)
if t == 'string' then
encode_string(v)
elseif t == 'number' then
if v ~= v then -- NaN
buffer[pos] = 'null'
elseif v == 1/0 or v == -1/0 then -- Infinity
buffer[pos] = 'null'
else
buffer[pos] = tostring(v)
end
pos = pos + 1
elseif t == 'boolean' then
buffer[pos] = v and 'true' or 'false'
pos = pos + 1
elseif t == 'table' then
if depth > 100 then error('circular reference') end
local is_array = true
local max_index = 0
local count = 0
for k, _ in pairs(v) do
count = count + 1
if type(k) ~= 'number' or k <= 0 or k % 1 ~= 0 then
is_array = false
break
end
if k > max_index then max_index = k end
end
if is_array and count == max_index then
buffer[pos] = '[\n'
pos = pos + 1
for i = 1, max_index do
buffer[pos] = next_indent
pos = pos + 1
encode_value(v[i], depth + 1)
if i < max_index then
buffer[pos] = ','
pos = pos + 1
end
buffer[pos] = '\n'
pos = pos + 1
end
buffer[pos] = current_indent .. ']'
pos = pos + 1
else
buffer[pos] = '{\n'
pos = pos + 1
local keys = {}
for k in pairs(v) do
keys[#keys + 1] = k
end
for i, k in ipairs(keys) do
buffer[pos] = next_indent
pos = pos + 1
encode_string(tostring(k))
buffer[pos] = ': '
pos = pos + 1
encode_value(v[k], depth + 1)
if i < #keys then
buffer[pos] = ','
pos = pos + 1
end
buffer[pos] = '\n'
pos = pos + 1
end
buffer[pos] = current_indent .. '}'
pos = pos + 1
end
else
buffer[pos] = 'null'
pos = pos + 1
end
end
encode_value(value, 0)
return table.concat(buffer)
end
function json.validate(data, schema) function json.validate(data, schema)
local function validate_value(value, schema_value) local function validate_value(value, schema_value)
local value_type = type(value) local value_type = type(value)
@ -576,16 +148,13 @@ function json.validate(data, schema)
end end
if schema_type == "table" and schema_value.properties then if schema_type == "table" and schema_value.properties then
local required = schema_value.required
for prop, prop_schema in pairs(schema_value.properties) do for prop, prop_schema in pairs(schema_value.properties) do
local prop_value = value[prop] if schema_value.required and schema_value.required[prop] and value[prop] == nil then
if required and required[prop] and prop_value == nil then
return false, "Missing required property: " .. prop return false, "Missing required property: " .. prop
end end
if prop_value ~= nil then if value[prop] ~= nil then
local valid, err = validate_value(prop_value, prop_schema) local valid, err = validate_value(value[prop], prop_schema)
if not valid then if not valid then
return false, "Property " .. prop .. ": " .. err return false, "Property " .. prop .. ": " .. err
end end

View File

@ -8,6 +8,7 @@ import (
"Moonshark/modules/crypto" "Moonshark/modules/crypto"
"Moonshark/modules/fs" "Moonshark/modules/fs"
"Moonshark/modules/http" "Moonshark/modules/http"
"Moonshark/modules/json"
"Moonshark/modules/math" "Moonshark/modules/math"
lua_string "Moonshark/modules/string" lua_string "Moonshark/modules/string"
@ -34,6 +35,7 @@ func New() *Registry {
} }
// Load all Go functions // Load all Go functions
maps.Copy(r.goFuncs, json.GetFunctionList())
maps.Copy(r.goFuncs, lua_string.GetFunctionList()) maps.Copy(r.goFuncs, lua_string.GetFunctionList())
maps.Copy(r.goFuncs, math.GetFunctionList()) maps.Copy(r.goFuncs, math.GetFunctionList())
maps.Copy(r.goFuncs, fs.GetFunctionList()) maps.Copy(r.goFuncs, fs.GetFunctionList())

View File

@ -13,16 +13,9 @@ function assert(condition, message, level)
level = level or 2 level = level or 2
local info = debug.getinfo(level, "Sl") local info = debug.getinfo(level, "Sl")
local file = info.source local file = info.source:match("@?(.+)") or "unknown"
-- Extract filename from source or use generic name
if file:sub(1,1) == "@" then
file = file:sub(2) -- Remove @ prefix for files
else
file = "<script>" -- Generic name for inline scripts
end
local line = info.currentline or "unknown" local line = info.currentline or "unknown"
local error_msg = message or "assertion failed" local error_msg = message or "assertion failed"
local full_msg = string.format("%s:%s: %s", file, line, error_msg) local full_msg = string.format("%s:%s: %s", file, line, error_msg)