string fixes, static server improvements

This commit is contained in:
Sky Johnson 2025-07-31 19:07:27 -05:00
parent 4d0d5b6757
commit da5281ba0a
5 changed files with 427 additions and 9 deletions

View File

@ -1,7 +1,5 @@
package metadata
const (
Version = "1.0.0"
CommitHash = "placeholder"
BuildDate = "25/07/2025"
Version = "1.0.0"
)

View File

@ -1,6 +1,7 @@
package http
import (
"Moonshark/metadata"
"context"
"fmt"
"net"
@ -49,6 +50,7 @@ func http_create_server(s *luajit.State) int {
}
globalServer = &fasthttp.Server{
Name: "Moonshark/" + metadata.Version,
Handler: handleRequest,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
@ -174,6 +176,7 @@ func http_register_static(s *luajit.State) int {
urlPrefix := s.ToString(1)
rootPath := s.ToString(2)
noCache := s.ToBoolean(3)
// Ensure prefix starts with /
if !strings.HasPrefix(urlPrefix, "/") {
@ -188,7 +191,7 @@ func http_register_static(s *luajit.State) int {
return 2
}
RegisterStaticHandler(urlPrefix, absPath)
RegisterStaticHandler(urlPrefix, absPath, noCache)
s.PushBoolean(true)
return 1
}
@ -293,16 +296,36 @@ func isLikelyStaticFile(path string) bool {
}
// RegisterStaticHandler adds a static file handler
func RegisterStaticHandler(urlPrefix, rootPath string) {
func RegisterStaticHandler(urlPrefix, rootPath string, noCache bool) {
staticMu.Lock()
defer staticMu.Unlock()
var cacheDuration time.Duration
var compress bool
if noCache {
cacheDuration = 0
compress = false
} else {
cacheDuration = 3600 * time.Second
compress = true
}
fs := &fasthttp.FS{
Root: rootPath,
CompressRoot: rootPath + "/.cache",
IndexNames: []string{"index.html"},
GenerateIndexPages: false,
Compress: true,
Compress: compress,
CompressBrotli: compress,
CompressZstd: compress,
CacheDuration: cacheDuration,
AcceptByteRange: true,
PathNotFound: func(ctx *fasthttp.RequestCtx) {
path := ctx.Path()
fmt.Printf("404 not found: %s\n", path)
ctx.SetStatusCode(fasthttp.StatusNotFound)
ctx.SetBodyString("404 not found")
},
}
staticHandlers[urlPrefix] = fs

View File

@ -920,14 +920,16 @@ function Server:close()
return _G.__IS_WORKER or moonshark.http_close_server()
end
function Server:static(root_path, url_prefix)
function Server:static(root_path, url_prefix, no_cache)
if not no_cache or no_cache ~= true then no_cache = false end
url_prefix = url_prefix or "/"
if not string.starts_with(url_prefix, "/") then
url_prefix = "/" .. url_prefix
end
if not _G.__IS_WORKER then
local success, err = moonshark.http_register_static(url_prefix, root_path)
local success, err = moonshark.http_register_static(url_prefix, root_path, no_cache)
if not success then
error("Failed to register static handler: " .. (err or "unknown error"))
end

View File

@ -670,3 +670,398 @@ function string.random(length, charset)
end
return result
end
-- Template processing with code execution
function string.render(template_str, env)
local function is_control_structure(code)
-- Check if code is a control structure that doesn't produce output
local trimmed = code:match("^%s*(.-)%s*$")
return trimmed == "else" or
trimmed == "end" or
trimmed:match("^if%s") or
trimmed:match("^elseif%s") or
trimmed:match("^for%s") or
trimmed:match("^while%s") or
trimmed:match("^repeat%s*$") or
trimmed:match("^until%s") or
trimmed:match("^do%s*$") or
trimmed:match("^local%s") or
trimmed:match("^function%s") or
trimmed:match(".*=%s*function%s*%(") or
trimmed:match(".*then%s*$") or
trimmed:match(".*do%s*$")
end
local pos, chunks = 1, {}
while pos <= #template_str do
local unescaped_start = template_str:find("{{{", pos, true)
local escaped_start = template_str:find("{{", pos, true)
local start, tag_type, open_len
if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then
start, tag_type, open_len = unescaped_start, "-", 3
elseif escaped_start then
start, tag_type, open_len = escaped_start, "=", 2
else
table.insert(chunks, template_str:sub(pos))
break
end
if start > pos then
table.insert(chunks, template_str:sub(pos, start-1))
end
pos = start + open_len
local close_tag = tag_type == "-" and "}}}" or "}}"
local close_start, close_stop = template_str:find(close_tag, pos, true)
if not close_start then
error("Failed to find closing tag at position " .. pos)
end
local code = template_str:sub(pos, close_start-1):match("^%s*(.-)%s*$")
local is_control = is_control_structure(code)
table.insert(chunks, {tag_type, code, pos, is_control})
pos = close_stop + 1
end
local buffer = {"local _tostring, _escape, _b, _b_i = ...\n"}
for _, chunk in ipairs(chunks) do
local t = type(chunk)
if t == "string" then
table.insert(buffer, "_b_i = _b_i + 1\n")
table.insert(buffer, "_b[_b_i] = " .. string.format("%q", chunk) .. "\n")
else
local tag_type, code, pos, is_control = chunk[1], chunk[2], chunk[3], chunk[4]
if is_control then
-- Control structure - just insert as raw Lua code
table.insert(buffer, "--[[" .. pos .. "]] " .. code .. "\n")
elseif tag_type == "=" then
-- Simple variable check
if code:match("^[%w_]+$") then
table.insert(buffer, "_b_i = _b_i + 1\n")
table.insert(buffer, "--[[" .. pos .. "]] _b[_b_i] = _escape(_tostring(" .. code .. "))\n")
else
-- Expression output with escaping
table.insert(buffer, "_b_i = _b_i + 1\n")
table.insert(buffer, "--[[" .. pos .. "]] _b[_b_i] = _escape(_tostring(" .. code .. "))\n")
end
elseif tag_type == "-" then
-- Unescaped output
table.insert(buffer, "_b_i = _b_i + 1\n")
table.insert(buffer, "--[[" .. pos .. "]] _b[_b_i] = _tostring(" .. code .. ")\n")
end
end
end
table.insert(buffer, "return _b")
local generated_code = table.concat(buffer)
-- DEBUG: Uncomment to see generated code
-- print("Generated Lua code:")
-- print(generated_code)
-- print("---")
local fn, err = loadstring(generated_code)
if not fn then
print("Generated code that failed to compile:")
print(generated_code)
error(err)
end
env = env or {}
local runtime_env = setmetatable({}, {__index = function(_, k) return env[k] or _G[k] end})
setfenv(fn, runtime_env)
local output_buffer = {}
fn(tostring, html_special_chars, output_buffer, 0)
return table.concat(output_buffer)
end
-- Named placeholder processing
function string.parse(template_str, env)
local pos, output = 1, {}
env = env or {}
while pos <= #template_str do
local unescaped_start, unescaped_end, unescaped_name = template_str:find("{{{%s*([%w_]+)%s*}}}", pos)
local escaped_start, escaped_end, escaped_name = template_str:find("{{%s*([%w_]+)%s*}}", pos)
local next_pos, placeholder_end, name, escaped
if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then
next_pos, placeholder_end, name, escaped = unescaped_start, unescaped_end, unescaped_name, false
elseif escaped_start then
next_pos, placeholder_end, name, escaped = escaped_start, escaped_end, escaped_name, true
else
local text = template_str:sub(pos)
if text and #text > 0 then
table.insert(output, text)
end
break
end
local text = template_str:sub(pos, next_pos - 1)
if text and #text > 0 then
table.insert(output, text)
end
local value = env[name]
local str = tostring(value or "")
if escaped then
str:html_special_chars()
end
table.insert(output, str)
pos = placeholder_end + 1
end
return table.concat(output)
end
-- Indexed placeholder processing
function string.iparse(template_str, values)
local pos, output, value_index = 1, {}, 1
values = values or {}
while pos <= #template_str do
local unescaped_start, unescaped_end = template_str:find("{{{}}}", pos, true)
local escaped_start, escaped_end = template_str:find("{{}}", pos, true)
local next_pos, placeholder_end, escaped
if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then
next_pos, placeholder_end, escaped = unescaped_start, unescaped_end, false
elseif escaped_start then
next_pos, placeholder_end, escaped = escaped_start, escaped_end, true
else
local text = template_str:sub(pos)
if text and #text > 0 then
table.insert(output, text)
end
break
end
local text = template_str:sub(pos, next_pos - 1)
if text and #text > 0 then
table.insert(output, text)
end
local value = values[value_index]
local str = tostring(value or "")
if escaped then
str:html_special_chars()
end
table.insert(output, str)
pos = placeholder_end + 1
value_index = value_index + 1
end
return table.concat(output)
end
string.special_chars_pattern = '[&<>"\']'
string.entity_decode_pattern = '&[#%w]+;'
string.special_encode_map = {
['&'] = '&amp;',
['<'] = '&lt;',
['>'] = '&gt;',
['"'] = '&quot;',
["'"] = '&#039;'
}
string.special_decode_map = {
['&amp;'] = '&',
['&lt;'] = '<',
['&gt;'] = '>',
['&quot;'] = '"',
['&#039;'] = "'",
['&apos;'] = "'"
}
string.extended_encode_map = {
-- Special chars
['&'] = '&amp;',
['<'] = '&lt;',
['>'] = '&gt;',
['"'] = '&quot;',
["'"] = '&#039;',
-- Extended characters
[' '] = '&nbsp;',
['¡'] = '&iexcl;',
['¢'] = '&cent;',
['£'] = '&pound;',
['¤'] = '&curren;',
['¥'] = '&yen;',
['¦'] = '&brvbar;',
['§'] = '&sect;',
['¨'] = '&uml;',
['©'] = '&copy;',
['ª'] = '&ordf;',
['«'] = '&laquo;',
['¬'] = '&not;',
['®'] = '&reg;',
['¯'] = '&macr;',
['°'] = '&deg;',
['±'] = '&plusmn;',
['²'] = '&sup2;',
['³'] = '&sup3;',
['´'] = '&acute;',
['µ'] = '&micro;',
[''] = '&para;',
['·'] = '&middot;',
['¸'] = '&cedil;',
['¹'] = '&sup1;',
['º'] = '&ordm;',
['»'] = '&raquo;',
['¼'] = '&frac14;',
['½'] = '&frac12;',
['¾'] = '&frac34;',
['¿'] = '&iquest;',
['À'] = '&Agrave;',
['Á'] = '&Aacute;',
['Â'] = '&Acirc;',
['Ã'] = '&Atilde;',
['Ä'] = '&Auml;',
['Å'] = '&Aring;',
['Æ'] = '&AElig;',
['Ç'] = '&Ccedil;',
['È'] = '&Egrave;',
['É'] = '&Eacute;',
['Ê'] = '&Ecirc;',
['Ë'] = '&Euml;',
['Ì'] = '&Igrave;',
['Í'] = '&Iacute;',
['Î'] = '&Icirc;',
['Ï'] = '&Iuml;',
['Ð'] = '&ETH;',
['Ñ'] = '&Ntilde;',
['Ò'] = '&Ograve;',
['Ó'] = '&Oacute;',
['Ô'] = '&Ocirc;',
['Õ'] = '&Otilde;',
['Ö'] = '&Ouml;',
['×'] = '&times;',
['Ø'] = '&Oslash;',
['Ù'] = '&Ugrave;',
['Ú'] = '&Uacute;',
['Û'] = '&Ucirc;',
['Ü'] = '&Uuml;',
['Ý'] = '&Yacute;',
['Þ'] = '&THORN;',
['ß'] = '&szlig;',
['à'] = '&agrave;',
['á'] = '&aacute;',
['â'] = '&acirc;',
['ã'] = '&atilde;',
['ä'] = '&auml;',
['å'] = '&aring;',
['æ'] = '&aelig;',
['ç'] = '&ccedil;',
['è'] = '&egrave;',
['é'] = '&eacute;',
['ê'] = '&ecirc;',
['ë'] = '&euml;',
['ì'] = '&igrave;',
['í'] = '&iacute;',
['î'] = '&icirc;',
['ï'] = '&iuml;',
['ð'] = '&eth;',
['ñ'] = '&ntilde;',
['ò'] = '&ograve;',
['ó'] = '&oacute;',
['ô'] = '&ocirc;',
['õ'] = '&otilde;',
['ö'] = '&ouml;',
['÷'] = '&divide;',
['ø'] = '&oslash;',
['ù'] = '&ugrave;',
['ú'] = '&uacute;',
['û'] = '&ucirc;',
['ü'] = '&uuml;',
['ý'] = '&yacute;',
['þ'] = '&thorn;',
['ÿ'] = '&yuml;'
}
string.extended_decode_map = {}
for char, entity in pairs(string.extended_encode_map) do
string.extended_decode_map[entity] = char
end
-- Add common named entities not in extended_encode_map
string.extended_decode_map['&apos;'] = "'"
string.extended_decode_map['&nbsp;'] = ' '
-- Converts HTML special characters (&, <, >, ", ') to entities
function string.html_special_chars(str)
if not str then return nil end
if not str:find(string.special_chars_pattern) then return str end
return (str:gsub(string.special_chars_pattern, string.special_encode_map))
end
getmetatable("").__index.html_special_chars = string.html_special_chars
-- Decodes HTML special character entities back to characters
function string.html_special_chars_decode(str)
if not str then return nil end
return (str:gsub('&[lg]t;', string.special_decode_map)
:gsub('&quot;', '"')
:gsub('&#039;', "'")
:gsub('&apos;', "'")
:gsub('&amp;', '&')) -- Must be last to avoid double-decoding
end
getmetatable("").__index.html_special_chars_decode = string.html_special_chars_decode
-- More comprehensive HTML entity encoding
-- Handles special chars plus extended Latin-1 characters
function string.html_entities(str)
if not str then return nil end
local result = {}
local result_len = 0
for i = 1, #str do
local char = str:sub(i, i)
local entity = string.extended_encode_map[char]
if entity then
result_len = result_len + 1
result[result_len] = entity
else
local byte = string.byte(char)
if byte > 127 then
-- Encode high-bit characters as numeric entities
result_len = result_len + 1
result[result_len] = '&#' .. byte .. ';'
else
result_len = result_len + 1
result[result_len] = char
end
end
end
return table.concat(result)
end
getmetatable("").__index.html_entities = string.html_entities
function string.html_entities_decode(str)
if not str then return nil end
-- Handle numeric entities first
local result = str:gsub('&#(%d+);', function(num)
local n = tonumber(num)
if n and n >= 0 and n <= 255 then
return string.char(n)
end
return '&#' .. num .. ';' -- Return unchanged if invalid
end)
-- Handle named entities
result = result:gsub(string.entity_decode_pattern, function(entity)
return string.extended_decode_map[entity] or entity
end)
return result
end
getmetatable("").__index.html_entities_decode = string.html_entities_decode

View File

@ -16,7 +16,7 @@ import (
var (
watchFlag = flag.Bool("watch", false, "Watch script files for changes and restart")
wFlag = flag.Bool("w", false, "Watch script files for changes and restart (short)")
wFlag = flag.Bool("w", false, "Watch script files for changes and restart")
)
func main() {