package runner import ( "errors" "fmt" "os" "path/filepath" "strings" "time" "Moonshark/utils/color" "Moonshark/utils/logger" lru "git.sharkk.net/Go/LRU" luajit "git.sharkk.net/Sky/LuaJIT-to-Go" "github.com/golang/snappy" ) // Global filesystem path (set during initialization) var fsBasePath string // Global file cache with compressed data var fileCache *lru.LRUCache // Cache entry info for statistics/debugging type cacheStats struct { hits int64 misses int64 } var stats cacheStats // InitFS initializes the filesystem with the given base path func InitFS(basePath string) error { if basePath == "" { return errors.New("filesystem base path cannot be empty") } // Create the directory if it doesn't exist if err := os.MkdirAll(basePath, 0755); err != nil { return fmt.Errorf("failed to create filesystem directory: %w", err) } // Store the absolute path absPath, err := filepath.Abs(basePath) if err != nil { return fmt.Errorf("failed to get absolute path: %w", err) } fsBasePath = absPath // Initialize file cache with 2000 entries (reasonable for most use cases) fileCache = lru.NewLRUCache(2000) logger.Info("Filesystem is g2g! %s", color.Yellow(fsBasePath)) return nil } // CleanupFS performs any necessary cleanup func CleanupFS() { if fileCache != nil { fileCache.Clear() logger.Info( "File cache cleared - %s hits, %s misses", color.Yellow(fmt.Sprintf("%d", stats.hits)), color.Red(fmt.Sprintf("%d", stats.misses)), ) } } // ResolvePath resolves a given path relative to the filesystem base // Returns the actual path and an error if the path tries to escape the sandbox func ResolvePath(path string) (string, error) { if fsBasePath == "" { return "", errors.New("filesystem not initialized") } // Clean the path to remove any .. or . components cleanPath := filepath.Clean(path) // Replace backslashes with forward slashes for consistent handling cleanPath = strings.ReplaceAll(cleanPath, "\\", "/") // Remove any leading / or drive letter to make it relative cleanPath = strings.TrimPrefix(cleanPath, "/") // Remove drive letter on Windows (e.g. C:) if len(cleanPath) >= 2 && cleanPath[1] == ':' { cleanPath = cleanPath[2:] } // Ensure the path doesn't contain .. to prevent escaping if strings.Contains(cleanPath, "..") { return "", errors.New("path cannot contain .. components") } // Join with the base path fullPath := filepath.Join(fsBasePath, cleanPath) // Verify the path is still within the base directory if !strings.HasPrefix(fullPath, fsBasePath) { return "", errors.New("path escapes the filesystem sandbox") } return fullPath, nil } // getCacheKey creates a cache key from path and modification time func getCacheKey(fullPath string, modTime time.Time) string { return fmt.Sprintf("%s:%d", fullPath, modTime.Unix()) } // fsReadFile reads a file and returns its contents func fsReadFile(state *luajit.State) int { if !state.IsString(1) { state.PushString("fs.read_file: path must be a string") return -1 } path := state.ToString(1) fullPath, err := ResolvePath(path) if err != nil { state.PushString("fs.read_file: " + err.Error()) return -1 } // Get file info for cache key and validation info, err := os.Stat(fullPath) if err != nil { state.PushString("fs.read_file: " + err.Error()) return -1 } // Create cache key with path and modification time cacheKey := getCacheKey(fullPath, info.ModTime()) // Try to get from cache first if fileCache != nil { if cachedData, exists := fileCache.Get(cacheKey); exists { if compressedData, ok := cachedData.([]byte); ok { // Decompress cached data data, err := snappy.Decode(nil, compressedData) if err == nil { stats.hits++ state.PushString(string(data)) return 1 } // Cache corruption - continue to disk read } } } // Cache miss or error - read from disk stats.misses++ data, err := os.ReadFile(fullPath) if err != nil { state.PushString("fs.read_file: " + err.Error()) return -1 } // Compress and cache the data if fileCache != nil { compressedData := snappy.Encode(nil, data) fileCache.Put(cacheKey, compressedData) } state.PushString(string(data)) return 1 } // fsWriteFile writes data to a file func fsWriteFile(state *luajit.State) int { if !state.IsString(1) { state.PushString("fs.write_file: path must be a string") return -1 } path := state.ToString(1) if !state.IsString(2) { state.PushString("fs.write_file: content must be a string") return -1 } content := state.ToString(2) fullPath, err := ResolvePath(path) if err != nil { state.PushString("fs.write_file: " + err.Error()) return -1 } // Ensure the directory exists dir := filepath.Dir(fullPath) if err := os.MkdirAll(dir, 0755); err != nil { state.PushString("fs.write_file: failed to create directory: " + err.Error()) return -1 } err = os.WriteFile(fullPath, []byte(content), 0644) if err != nil { state.PushString("fs.write_file: " + err.Error()) return -1 } // Invalidate cache entries for this file path if fileCache != nil { // We can't easily iterate through cache keys, so we'll let the cache // naturally expire old entries when the file is read again } state.PushBoolean(true) return 1 } // fsAppendFile appends data to a file func fsAppendFile(state *luajit.State) int { if !state.IsString(1) { state.PushString("fs.append_file: path must be a string") return -1 } path := state.ToString(1) if !state.IsString(2) { state.PushString("fs.append_file: content must be a string") return -1 } content := state.ToString(2) fullPath, err := ResolvePath(path) if err != nil { state.PushString("fs.append_file: " + err.Error()) return -1 } // Ensure the directory exists dir := filepath.Dir(fullPath) if err := os.MkdirAll(dir, 0755); err != nil { state.PushString("fs.append_file: failed to create directory: " + err.Error()) return -1 } file, err := os.OpenFile(fullPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { state.PushString("fs.append_file: " + err.Error()) return -1 } defer file.Close() _, err = file.Write([]byte(content)) if err != nil { state.PushString("fs.append_file: " + err.Error()) return -1 } state.PushBoolean(true) return 1 } // fsExists checks if a file or directory exists func fsExists(state *luajit.State) int { if !state.IsString(1) { state.PushString("fs.exists: path must be a string") return -1 } path := state.ToString(1) fullPath, err := ResolvePath(path) if err != nil { state.PushString("fs.exists: " + err.Error()) return -1 } _, err = os.Stat(fullPath) state.PushBoolean(err == nil) return 1 } // fsRemoveFile removes a file func fsRemoveFile(state *luajit.State) int { if !state.IsString(1) { state.PushString("fs.remove_file: path must be a string") return -1 } path := state.ToString(1) fullPath, err := ResolvePath(path) if err != nil { state.PushString("fs.remove_file: " + err.Error()) return -1 } // Check if it's a directory info, err := os.Stat(fullPath) if err != nil { state.PushString("fs.remove_file: " + err.Error()) return -1 } if info.IsDir() { state.PushString("fs.remove_file: cannot remove directory, use remove_dir instead") return -1 } err = os.Remove(fullPath) if err != nil { state.PushString("fs.remove_file: " + err.Error()) return -1 } state.PushBoolean(true) return 1 } // fsGetInfo gets information about a file func fsGetInfo(state *luajit.State) int { if !state.IsString(1) { state.PushString("fs.get_info: path must be a string") return -1 } path := state.ToString(1) fullPath, err := ResolvePath(path) if err != nil { state.PushString("fs.get_info: " + err.Error()) return -1 } info, err := os.Stat(fullPath) if err != nil { state.PushString("fs.get_info: " + err.Error()) return -1 } state.NewTable() state.PushString(info.Name()) state.SetField(-2, "name") state.PushNumber(float64(info.Size())) state.SetField(-2, "size") state.PushNumber(float64(info.Mode())) state.SetField(-2, "mode") state.PushNumber(float64(info.ModTime().Unix())) state.SetField(-2, "mod_time") state.PushBoolean(info.IsDir()) state.SetField(-2, "is_dir") return 1 } // fsMakeDir creates a directory func fsMakeDir(state *luajit.State) int { if !state.IsString(1) { state.PushString("fs.make_dir: path must be a string") return -1 } path := state.ToString(1) perm := os.FileMode(0755) if state.GetTop() >= 2 && state.IsNumber(2) { perm = os.FileMode(state.ToNumber(2)) } fullPath, err := ResolvePath(path) if err != nil { state.PushString("fs.make_dir: " + err.Error()) return -1 } err = os.MkdirAll(fullPath, perm) if err != nil { state.PushString("fs.make_dir: " + err.Error()) return -1 } state.PushBoolean(true) return 1 } // fsListDir lists the contents of a directory func fsListDir(state *luajit.State) int { if !state.IsString(1) { state.PushString("fs.list_dir: path must be a string") return -1 } path := state.ToString(1) fullPath, err := ResolvePath(path) if err != nil { state.PushString("fs.list_dir: " + err.Error()) return -1 } info, err := os.Stat(fullPath) if err != nil { state.PushString("fs.list_dir: " + err.Error()) return -1 } if !info.IsDir() { state.PushString("fs.list_dir: not a directory") return -1 } files, err := os.ReadDir(fullPath) if err != nil { state.PushString("fs.list_dir: " + err.Error()) return -1 } state.NewTable() for i, file := range files { state.PushNumber(float64(i + 1)) state.PushString(file.Name()) state.SetTable(-3) } return 1 } // fsRemoveDir removes a directory func fsRemoveDir(state *luajit.State) int { if !state.IsString(1) { state.PushString("fs.remove_dir: path must be a string") return -1 } path := state.ToString(1) recursive := false if state.GetTop() >= 2 { recursive = state.ToBoolean(2) } fullPath, err := ResolvePath(path) if err != nil { state.PushString("fs.remove_dir: " + err.Error()) return -1 } info, err := os.Stat(fullPath) if err != nil { state.PushString("fs.remove_dir: " + err.Error()) return -1 } if !info.IsDir() { state.PushString("fs.remove_dir: not a directory") return -1 } if recursive { err = os.RemoveAll(fullPath) } else { err = os.Remove(fullPath) } if err != nil { state.PushString("fs.remove_dir: " + err.Error()) return -1 } state.PushBoolean(true) return 1 } // fsJoinPaths joins path components func fsJoinPaths(state *luajit.State) int { nargs := state.GetTop() if nargs < 1 { state.PushString("fs.join_paths: at least one path component required") return -1 } components := make([]string, nargs) for i := 1; i <= nargs; i++ { if !state.IsString(i) { state.PushString("fs.join_paths: all arguments must be strings") return -1 } components[i-1] = state.ToString(i) } result := filepath.Join(components...) result = strings.ReplaceAll(result, "\\", "/") state.PushString(result) return 1 } // fsDirName returns the directory portion of a path func fsDirName(state *luajit.State) int { if !state.IsString(1) { state.PushString("fs.dir_name: path must be a string") return -1 } path := state.ToString(1) dir := filepath.Dir(path) dir = strings.ReplaceAll(dir, "\\", "/") state.PushString(dir) return 1 } // fsBaseName returns the file name portion of a path func fsBaseName(state *luajit.State) int { if !state.IsString(1) { state.PushString("fs.base_name: path must be a string") return -1 } path := state.ToString(1) base := filepath.Base(path) state.PushString(base) return 1 } // fsExtension returns the file extension func fsExtension(state *luajit.State) int { if !state.IsString(1) { state.PushString("fs.extension: path must be a string") return -1 } path := state.ToString(1) ext := filepath.Ext(path) state.PushString(ext) return 1 } // RegisterFSFunctions registers filesystem functions with the Lua state func RegisterFSFunctions(state *luajit.State) error { if err := state.RegisterGoFunction("__fs_read_file", fsReadFile); err != nil { return err } if err := state.RegisterGoFunction("__fs_write_file", fsWriteFile); err != nil { return err } if err := state.RegisterGoFunction("__fs_append_file", fsAppendFile); err != nil { return err } if err := state.RegisterGoFunction("__fs_exists", fsExists); err != nil { return err } if err := state.RegisterGoFunction("__fs_remove_file", fsRemoveFile); err != nil { return err } if err := state.RegisterGoFunction("__fs_get_info", fsGetInfo); err != nil { return err } if err := state.RegisterGoFunction("__fs_make_dir", fsMakeDir); err != nil { return err } if err := state.RegisterGoFunction("__fs_list_dir", fsListDir); err != nil { return err } if err := state.RegisterGoFunction("__fs_remove_dir", fsRemoveDir); err != nil { return err } if err := state.RegisterGoFunction("__fs_join_paths", fsJoinPaths); err != nil { return err } if err := state.RegisterGoFunction("__fs_dir_name", fsDirName); err != nil { return err } if err := state.RegisterGoFunction("__fs_base_name", fsBaseName); err != nil { return err } if err := state.RegisterGoFunction("__fs_extension", fsExtension); err != nil { return err } return nil }