package routers import ( "encoding/binary" "errors" "hash/fnv" "os" "path/filepath" "strings" "sync" "time" luajit "git.sharkk.net/Sky/LuaJIT-to-Go" "github.com/VictoriaMetrics/fastcache" ) // Maximum number of URL parameters per route const maxParams = 20 // Default cache sizes const ( defaultBytecodeMaxBytes = 32 * 1024 * 1024 // 32MB for bytecode cache defaultRouteMaxBytes = 8 * 1024 * 1024 // 8MB for route match cache ) // Params holds URL parameters with fixed-size arrays to avoid allocations type Params struct { Keys [maxParams]string Values [maxParams]string Count int } // Get returns a parameter value by name func (p *Params) Get(name string) string { for i := range p.Count { if p.Keys[i] == name { return p.Values[i] } } return "" } // LuaRouter is a filesystem-based HTTP router for Lua files type LuaRouter struct { routesDir string // Root directory containing route files routes map[string]*node // Method -> route tree failedRoutes map[string]*RouteError // Track failed routes mu sync.RWMutex // Lock for concurrent access to routes routeCache *fastcache.Cache // Cache for route lookups bytecodeCache *fastcache.Cache // Cache for compiled bytecode // Middleware tracking for path hierarchy middlewareFiles map[string][]string // path -> middleware file paths } // node represents a node in the routing trie type node struct { handler string // Path to Lua file (empty if not an endpoint) indexFile string // Path to index.lua file (catch-all) paramName string // Parameter name (if this is a parameter node) staticChild map[string]*node // Static children by segment name paramChild *node // Parameter/wildcard child err error // Compilation error if any modTime time.Time // Last modification time } // NewLuaRouter creates a new LuaRouter instance func NewLuaRouter(routesDir string) (*LuaRouter, error) { info, err := os.Stat(routesDir) if err != nil { return nil, err } if !info.IsDir() { return nil, errors.New("routes path is not a directory") } r := &LuaRouter{ routesDir: routesDir, routes: make(map[string]*node), failedRoutes: make(map[string]*RouteError), middlewareFiles: make(map[string][]string), routeCache: fastcache.New(defaultRouteMaxBytes), bytecodeCache: fastcache.New(defaultBytecodeMaxBytes), } methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"} for _, method := range methods { r.routes[method] = &node{ staticChild: make(map[string]*node), } } err = r.buildRoutes() if len(r.failedRoutes) > 0 { return r, ErrRoutesCompilationErrors } return r, err } // buildRoutes scans the routes directory and builds the routing tree func (r *LuaRouter) buildRoutes() error { r.failedRoutes = make(map[string]*RouteError) r.middlewareFiles = make(map[string][]string) // First pass: collect all middleware files err := filepath.Walk(r.routesDir, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() || !strings.HasSuffix(info.Name(), ".lua") { return err } if strings.TrimSuffix(info.Name(), ".lua") == "middleware" { relDir, err := filepath.Rel(r.routesDir, filepath.Dir(path)) if err != nil { return err } urlPath := "/" if relDir != "." { urlPath = "/" + strings.ReplaceAll(relDir, "\\", "/") } r.middlewareFiles[urlPath] = append(r.middlewareFiles[urlPath], path) } return nil }) if err != nil { return err } // Second pass: build routes with combined middleware + handler return filepath.Walk(r.routesDir, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() || !strings.HasSuffix(info.Name(), ".lua") { return err } fileName := strings.TrimSuffix(info.Name(), ".lua") // Skip middleware files (already processed) if fileName == "middleware" { return nil } relDir, err := filepath.Rel(r.routesDir, filepath.Dir(path)) if err != nil { return err } urlPath := "/" if relDir != "." { urlPath = "/" + strings.ReplaceAll(relDir, "\\", "/") } // Handle index.lua files if fileName == "index" { for _, method := range []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"} { root := r.routes[method] node := r.findOrCreateNode(root, urlPath) node.indexFile = path node.modTime = info.ModTime() r.compileWithMiddleware(node, urlPath, path) } return nil } // Handle method files method := strings.ToUpper(fileName) root, exists := r.routes[method] if !exists { return nil } r.addRoute(root, urlPath, path, info.ModTime()) return nil }) } // addRoute adds a route to the routing tree func (r *LuaRouter) addRoute(root *node, urlPath, handlerPath string, modTime time.Time) error { segments := strings.Split(strings.Trim(urlPath, "/"), "/") current := root for _, segment := range segments { if segment == "" { continue } if len(segment) >= 2 && segment[0] == '[' && segment[len(segment)-1] == ']' { if current.paramChild == nil { current.paramChild = &node{ paramName: segment[1 : len(segment)-1], staticChild: make(map[string]*node), } } current = current.paramChild } else { child, exists := current.staticChild[segment] if !exists { child = &node{ staticChild: make(map[string]*node), } current.staticChild[segment] = child } current = child } } current.handler = handlerPath current.modTime = modTime return r.compileWithMiddleware(current, urlPath, handlerPath) } // compileWithMiddleware combines middleware and handler source, then compiles func (r *LuaRouter) compileWithMiddleware(n *node, urlPath, scriptPath string) error { if scriptPath == "" { return nil } // Collect middleware for this path (cascading from root) middlewareChain := r.getMiddlewareChain(urlPath) // Read and combine all source files var combinedSource strings.Builder // Add middleware in order for _, mwPath := range middlewareChain { content, err := os.ReadFile(mwPath) if err != nil { n.err = err return err } combinedSource.WriteString("-- Middleware: ") combinedSource.WriteString(mwPath) combinedSource.WriteString("\n") combinedSource.Write(content) combinedSource.WriteString("\n") } // Add main handler content, err := os.ReadFile(scriptPath) if err != nil { n.err = err return err } combinedSource.WriteString("-- Handler: ") combinedSource.WriteString(scriptPath) combinedSource.WriteString("\n") combinedSource.Write(content) // Compile combined source state := luajit.New() if state == nil { compileErr := errors.New("failed to create Lua state") n.err = compileErr return compileErr } defer state.Close() bytecode, err := state.CompileBytecode(combinedSource.String(), scriptPath) if err != nil { n.err = err return err } bytecodeKey := getBytecodeKey(scriptPath) r.bytecodeCache.Set(bytecodeKey, bytecode) n.err = nil return nil } // getMiddlewareChain returns middleware files that apply to the given path func (r *LuaRouter) getMiddlewareChain(urlPath string) []string { var chain []string // Collect middleware from root to specific path pathParts := strings.Split(strings.Trim(urlPath, "/"), "/") if pathParts[0] == "" { pathParts = []string{} } // Add root middleware if mw, exists := r.middlewareFiles["/"]; exists { chain = append(chain, mw...) } // Add middleware from each path level currentPath := "" for _, part := range pathParts { currentPath += "/" + part if mw, exists := r.middlewareFiles[currentPath]; exists { chain = append(chain, mw...) } } return chain } // findOrCreateNode finds or creates a node at the given path func (r *LuaRouter) findOrCreateNode(root *node, urlPath string) *node { segments := strings.Split(strings.Trim(urlPath, "/"), "/") current := root for _, segment := range segments { if segment == "" { continue } if len(segment) >= 2 && segment[0] == '[' && segment[len(segment)-1] == ']' { if current.paramChild == nil { current.paramChild = &node{ paramName: segment[1 : len(segment)-1], staticChild: make(map[string]*node), } } current = current.paramChild } else { child, exists := current.staticChild[segment] if !exists { child = &node{ staticChild: make(map[string]*node), } current.staticChild[segment] = child } current = child } } return current } // getRouteKey generates a unique key for a route func getRouteKey(path, handler string) string { return path + ":" + handler } // hashString generates a hash for a string func hashString(s string) uint64 { h := fnv.New64a() h.Write([]byte(s)) return h.Sum64() } // uint64ToBytes converts a uint64 to bytes for cache key func uint64ToBytes(n uint64) []byte { b := make([]byte, 8) binary.LittleEndian.PutUint64(b, n) return b } // getCacheKey generates a cache key for a method and path func getCacheKey(method, path string) []byte { key := hashString(method + ":" + path) return uint64ToBytes(key) } // getBytecodeKey generates a cache key for a handler path func getBytecodeKey(handlerPath string) []byte { key := hashString(handlerPath) return uint64ToBytes(key) } // Match finds a handler for the given method and path func (r *LuaRouter) Match(method, path string, params *Params) (*node, bool) { params.Count = 0 r.mu.RLock() root, exists := r.routes[method] r.mu.RUnlock() if !exists { return nil, false } segments := strings.Split(strings.Trim(path, "/"), "/") return r.matchPath(root, segments, params, 0) } // matchPath recursively matches a path against the routing tree func (r *LuaRouter) matchPath(current *node, segments []string, params *Params, depth int) (*node, bool) { if len(segments) == 0 { if current.handler != "" { return current, true } if current.indexFile != "" { return current, true } return nil, false } segment := segments[0] remaining := segments[1:] // Try static child first if child, exists := current.staticChild[segment]; exists { if node, found := r.matchPath(child, remaining, params, depth+1); found { return node, true } } // Try parameter child if current.paramChild != nil { if params.Count < maxParams { params.Keys[params.Count] = current.paramChild.paramName params.Values[params.Count] = segment params.Count++ } if node, found := r.matchPath(current.paramChild, remaining, params, depth+1); found { return node, true } params.Count-- } // Fall back to index.lua if current.indexFile != "" { return current, true } return nil, false } // GetRouteInfo returns the combined bytecode, script path, and any error func (r *LuaRouter) GetRouteInfo(method, path string, params *Params) ([]byte, string, error, bool) { routeCacheKey := getCacheKey(method, path) routeCacheData := r.routeCache.Get(nil, routeCacheKey) if len(routeCacheData) > 0 { handlerPath := string(routeCacheData[8:]) bytecodeKey := routeCacheData[:8] bytecode := r.bytecodeCache.Get(nil, bytecodeKey) n, exists := r.nodeForHandler(handlerPath) if !exists { r.routeCache.Del(routeCacheKey) return nil, "", nil, false } if len(bytecode) > 0 { return bytecode, handlerPath, n.err, true } fileInfo, err := os.Stat(handlerPath) if err != nil || fileInfo.ModTime().After(n.modTime) { scriptPath := n.handler if scriptPath == "" { scriptPath = n.indexFile } urlPath := r.getNodeURLPath(n) if err := r.compileWithMiddleware(n, urlPath, scriptPath); err != nil { return nil, handlerPath, n.err, true } newBytecodeKey := getBytecodeKey(handlerPath) bytecode = r.bytecodeCache.Get(nil, newBytecodeKey) newCacheData := make([]byte, 8+len(handlerPath)) copy(newCacheData[:8], newBytecodeKey) copy(newCacheData[8:], handlerPath) r.routeCache.Set(routeCacheKey, newCacheData) return bytecode, handlerPath, n.err, true } return bytecode, handlerPath, n.err, true } node, found := r.Match(method, path, params) if !found { return nil, "", nil, false } scriptPath := node.handler if scriptPath == "" && node.indexFile != "" { scriptPath = node.indexFile } if scriptPath == "" { return nil, "", nil, false } bytecodeKey := getBytecodeKey(scriptPath) bytecode := r.bytecodeCache.Get(nil, bytecodeKey) if len(bytecode) == 0 { urlPath := r.getNodeURLPath(node) if err := r.compileWithMiddleware(node, urlPath, scriptPath); err != nil { return nil, scriptPath, node.err, true } bytecode = r.bytecodeCache.Get(nil, bytecodeKey) } cacheData := make([]byte, 8+len(scriptPath)) copy(cacheData[:8], bytecodeKey) copy(cacheData[8:], scriptPath) r.routeCache.Set(routeCacheKey, cacheData) return bytecode, scriptPath, node.err, true } // getNodeURLPath reconstructs URL path for a node (simplified) func (r *LuaRouter) getNodeURLPath(node *node) string { // This is a simplified implementation - in practice you'd traverse up the tree return "/" } // nodeForHandler finds a node by its handler path func (r *LuaRouter) nodeForHandler(handlerPath string) (*node, bool) { r.mu.RLock() defer r.mu.RUnlock() for _, root := range r.routes { if node := findNodeByHandler(root, handlerPath); node != nil { return node, true } } return nil, false } // findNodeByHandler finds a node by its handler path func findNodeByHandler(current *node, handlerPath string) *node { if current == nil { return nil } if current.handler == handlerPath || current.indexFile == handlerPath { return current } for _, child := range current.staticChild { if node := findNodeByHandler(child, handlerPath); node != nil { return node } } if current.paramChild != nil { if node := findNodeByHandler(current.paramChild, handlerPath); node != nil { return node } } return nil } // Refresh rebuilds the router by rescanning the routes directory func (r *LuaRouter) Refresh() error { r.mu.Lock() defer r.mu.Unlock() for method := range r.routes { r.routes[method] = &node{ staticChild: make(map[string]*node), } } r.failedRoutes = make(map[string]*RouteError) r.middlewareFiles = make(map[string][]string) err := r.buildRoutes() if len(r.failedRoutes) > 0 { return ErrRoutesCompilationErrors } return err } // ReportFailedRoutes returns a list of routes that failed to compile func (r *LuaRouter) ReportFailedRoutes() []*RouteError { r.mu.RLock() defer r.mu.RUnlock() result := make([]*RouteError, 0, len(r.failedRoutes)) for _, re := range r.failedRoutes { result = append(result, re) } return result } // ClearCache clears both the route and bytecode caches func (r *LuaRouter) ClearCache() { r.routeCache.Reset() r.bytecodeCache.Reset() } // GetCacheStats returns statistics about the cache func (r *LuaRouter) GetCacheStats() map[string]any { var routeStats fastcache.Stats var bytecodeStats fastcache.Stats r.routeCache.UpdateStats(&routeStats) r.bytecodeCache.UpdateStats(&bytecodeStats) return map[string]any{ "routeEntries": routeStats.EntriesCount, "routeBytes": routeStats.BytesSize, "routeCollisions": routeStats.Collisions, "bytecodeEntries": bytecodeStats.EntriesCount, "bytecodeBytes": bytecodeStats.BytesSize, "bytecodeCollisions": bytecodeStats.Collisions, } } // GetRouteStats returns statistics about the router func (r *LuaRouter) GetRouteStats() (int, int64) { r.mu.RLock() defer r.mu.RUnlock() routeCount := 0 bytecodeBytes := int64(0) for _, root := range r.routes { count, bytes := countNodesAndBytecode(root) routeCount += count bytecodeBytes += bytes } return routeCount, bytecodeBytes } // countNodesAndBytecode traverses the tree and counts nodes and bytecode size func countNodesAndBytecode(n *node) (count int, bytecodeBytes int64) { if n == nil { return 0, 0 } if n.handler != "" || n.indexFile != "" { count = 1 bytecodeBytes = 2048 } for _, child := range n.staticChild { childCount, childBytes := countNodesAndBytecode(child) count += childCount bytecodeBytes += childBytes } if n.paramChild != nil { childCount, childBytes := countNodesAndBytecode(n.paramChild) count += childCount bytecodeBytes += childBytes } return count, bytecodeBytes } type NodeWithError struct { ScriptPath string Error error }