572 lines
14 KiB
Go

package lualibs
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"Moonshark/color"
"Moonshark/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
}