575 lines
13 KiB
Go
575 lines
13 KiB
Go
package runner
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"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.Server("Virtual filesystem initialized at: %s", fsBasePath)
|
|
return nil
|
|
}
|
|
|
|
// CleanupFS performs any necessary cleanup
|
|
func CleanupFS() {
|
|
if fileCache != nil {
|
|
fileCache.Clear()
|
|
logger.Server("File cache cleared - Stats: hits=%d, misses=%d", stats.hits, 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
|
|
}
|