package lualibs 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.Infof("Filesystem is g2g! %s", color.Yellow(fsBasePath)) return nil } // CleanupFS performs any necessary cleanup func CleanupFS() { if fileCache != nil { fileCache.Clear() logger.Infof( "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 err := state.CheckExactArgs(1); err != nil { return state.PushError("fs.read_file: %v", err) } path, err := state.SafeToString(1) if err != nil { return state.PushError("fs.read_file: path must be string") } fullPath, err := ResolvePath(path) if err != nil { return state.PushError("fs.read_file: %v", err) } // Get file info for cache key and validation info, err := os.Stat(fullPath) if err != nil { return state.PushError("fs.read_file: %v", err) } // 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 { return state.PushError("fs.read_file: %v", err) } // 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 err := state.CheckExactArgs(2); err != nil { return state.PushError("fs.write_file: %v", err) } path, err := state.SafeToString(1) if err != nil { return state.PushError("fs.write_file: path must be string") } content, err := state.SafeToString(2) if err != nil { return state.PushError("fs.write_file: content must be string") } fullPath, err := ResolvePath(path) if err != nil { return state.PushError("fs.write_file: %v", err) } // Ensure the directory exists dir := filepath.Dir(fullPath) if err := os.MkdirAll(dir, 0755); err != nil { return state.PushError("fs.write_file: failed to create directory: %v", err) } if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil { return state.PushError("fs.write_file: %v", err) } state.PushBoolean(true) return 1 } // fsAppendFile appends data to a file func fsAppendFile(state *luajit.State) int { if err := state.CheckExactArgs(2); err != nil { return state.PushError("fs.append_file: %v", err) } path, err := state.SafeToString(1) if err != nil { return state.PushError("fs.append_file: path must be string") } content, err := state.SafeToString(2) if err != nil { return state.PushError("fs.append_file: content must be string") } fullPath, err := ResolvePath(path) if err != nil { return state.PushError("fs.append_file: %v", err) } // Ensure the directory exists dir := filepath.Dir(fullPath) if err := os.MkdirAll(dir, 0755); err != nil { return state.PushError("fs.append_file: failed to create directory: %v", err) } file, err := os.OpenFile(fullPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return state.PushError("fs.append_file: %v", err) } defer file.Close() if _, err = file.Write([]byte(content)); err != nil { return state.PushError("fs.append_file: %v", err) } state.PushBoolean(true) return 1 } // fsExists checks if a file or directory exists func fsExists(state *luajit.State) int { if err := state.CheckExactArgs(1); err != nil { return state.PushError("fs.exists: %v", err) } path, err := state.SafeToString(1) if err != nil { return state.PushError("fs.exists: path must be string") } fullPath, err := ResolvePath(path) if err != nil { return state.PushError("fs.exists: %v", err) } _, err = os.Stat(fullPath) state.PushBoolean(err == nil) return 1 } // fsRemoveFile removes a file func fsRemoveFile(state *luajit.State) int { if err := state.CheckExactArgs(1); err != nil { return state.PushError("fs.remove_file: %v", err) } path, err := state.SafeToString(1) if err != nil { return state.PushError("fs.remove_file: path must be string") } fullPath, err := ResolvePath(path) if err != nil { return state.PushError("fs.remove_file: %v", err) } // Check if it's a directory info, err := os.Stat(fullPath) if err != nil { return state.PushError("fs.remove_file: %v", err) } if info.IsDir() { return state.PushError("fs.remove_file: cannot remove directory, use remove_dir instead") } if err := os.Remove(fullPath); err != nil { return state.PushError("fs.remove_file: %v", err) } state.PushBoolean(true) return 1 } // fsGetInfo gets information about a file func fsGetInfo(state *luajit.State) int { if err := state.CheckExactArgs(1); err != nil { return state.PushError("fs.get_info: %v", err) } path, err := state.SafeToString(1) if err != nil { return state.PushError("fs.get_info: path must be string") } fullPath, err := ResolvePath(path) if err != nil { return state.PushError("fs.get_info: %v", err) } info, err := os.Stat(fullPath) if err != nil { return state.PushError("fs.get_info: %v", err) } fileInfo := map[string]any{ "name": info.Name(), "size": info.Size(), "mode": int(info.Mode()), "mod_time": info.ModTime().Unix(), "is_dir": info.IsDir(), } if err := state.PushValue(fileInfo); err != nil { return state.PushError("fs.get_info: %v", err) } return 1 } // fsMakeDir creates a directory func fsMakeDir(state *luajit.State) int { if err := state.CheckMinArgs(1); err != nil { return state.PushError("fs.make_dir: %v", err) } path, err := state.SafeToString(1) if err != nil { return state.PushError("fs.make_dir: path must be string") } perm := os.FileMode(0755) if state.GetTop() >= 2 { if permVal, err := state.SafeToNumber(2); err == nil { perm = os.FileMode(permVal) } } fullPath, err := ResolvePath(path) if err != nil { return state.PushError("fs.make_dir: %v", err) } if err := os.MkdirAll(fullPath, perm); err != nil { return state.PushError("fs.make_dir: %v", err) } state.PushBoolean(true) return 1 } // fsListDir lists the contents of a directory func fsListDir(state *luajit.State) int { if err := state.CheckExactArgs(1); err != nil { return state.PushError("fs.list_dir: %v", err) } path, err := state.SafeToString(1) if err != nil { return state.PushError("fs.list_dir: path must be string") } fullPath, err := ResolvePath(path) if err != nil { return state.PushError("fs.list_dir: %v", err) } info, err := os.Stat(fullPath) if err != nil { return state.PushError("fs.list_dir: %v", err) } if !info.IsDir() { return state.PushError("fs.list_dir: not a directory") } files, err := os.ReadDir(fullPath) if err != nil { return state.PushError("fs.list_dir: %v", err) } // Create array of filenames filenames := make([]string, len(files)) for i, file := range files { filenames[i] = file.Name() } if err := state.PushValue(filenames); err != nil { return state.PushError("fs.list_dir: %v", err) } return 1 } // fsRemoveDir removes a directory func fsRemoveDir(state *luajit.State) int { if err := state.CheckMinArgs(1); err != nil { return state.PushError("fs.remove_dir: %v", err) } path, err := state.SafeToString(1) if err != nil { return state.PushError("fs.remove_dir: path must be string") } recursive := false if state.GetTop() >= 2 { recursive = state.ToBoolean(2) } fullPath, err := ResolvePath(path) if err != nil { return state.PushError("fs.remove_dir: %v", err) } info, err := os.Stat(fullPath) if err != nil { return state.PushError("fs.remove_dir: %v", err) } if !info.IsDir() { return state.PushError("fs.remove_dir: not a directory") } if recursive { err = os.RemoveAll(fullPath) } else { err = os.Remove(fullPath) } if err != nil { return state.PushError("fs.remove_dir: %v", err) } state.PushBoolean(true) return 1 } // fsJoinPaths joins path components func fsJoinPaths(state *luajit.State) int { if err := state.CheckMinArgs(1); err != nil { return state.PushError("fs.join_paths: %v", err) } components := make([]string, state.GetTop()) for i := 1; i <= state.GetTop(); i++ { comp, err := state.SafeToString(i) if err != nil { return state.PushError("fs.join_paths: all arguments must be strings") } components[i-1] = comp } 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 err := state.CheckExactArgs(1); err != nil { return state.PushError("fs.dir_name: %v", err) } path, err := state.SafeToString(1) if err != nil { return state.PushError("fs.dir_name: path must be string") } 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 err := state.CheckExactArgs(1); err != nil { return state.PushError("fs.base_name: %v", err) } path, err := state.SafeToString(1) if err != nil { return state.PushError("fs.base_name: path must be string") } base := filepath.Base(path) state.PushString(base) return 1 } // fsExtension returns the file extension func fsExtension(state *luajit.State) int { if err := state.CheckExactArgs(1); err != nil { return state.PushError("fs.extension: %v", err) } path, err := state.SafeToString(1) if err != nil { return state.PushError("fs.extension: path must be string") } 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 }