[module] kffs
This commit is contained in:
		
							parent
							
								
									38cfe30644
								
							
						
					
					
						commit
						14e5731922
					
				| 
						 | 
				
			
			@ -0,0 +1,363 @@
 | 
			
		|||
// This package allows for grouping embedded filesystems into one multiFS,
 | 
			
		||||
// and provides a simple interface for reading from them
 | 
			
		||||
// and protections for reading from the local game filesystem
 | 
			
		||||
package KFFS
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"embed"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"log"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// TODO: add in some more security features to prevent directory traversal attacks and other security vulnerabilities.
 | 
			
		||||
// this package should be an easy to use viable alternative to the standard os and fs packages, while providing
 | 
			
		||||
// some ease of mind for end user security.
 | 
			
		||||
 | 
			
		||||
var localFS fs.FS
 | 
			
		||||
var localIsValid bool
 | 
			
		||||
 | 
			
		||||
// IsRunningDebug TODO: make sure this is removed before any production builds
 | 
			
		||||
func IsRunningDebug() bool {
 | 
			
		||||
	return os.Getenv("DEBUG_RUN") == "true"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	ex, err := os.Executable()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		localIsValid = false
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// check if there is a game directory at the root of the executable
 | 
			
		||||
	dataDir := filepath.Join(filepath.Dir(ex), "data")
 | 
			
		||||
	if IsRunningDebug() {
 | 
			
		||||
		dataDir, err = filepath.Abs("data")
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			localIsValid = false
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	info, err := os.Stat(dataDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		localIsValid = false
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// check if it is a directory
 | 
			
		||||
	if !info.IsDir() {
 | 
			
		||||
		localIsValid = false
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	localFS = os.DirFS(dataDir)
 | 
			
		||||
 | 
			
		||||
	// DEBUG: REMOVE BELOW
 | 
			
		||||
	// walk the directory to check if it is valid by printing out the files
 | 
			
		||||
	err = fs.WalkDir(localFS, ".", func(path string, d fs.DirEntry, err error) error {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if d.IsDir() {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		log.Println(path)
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		localIsValid = false
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// DEBUG: REMOVE ABOVE
 | 
			
		||||
 | 
			
		||||
	localIsValid = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Configuration is a configuration struct for the multiFS that allows specifying the filesystems to use.
 | 
			
		||||
// new file systems can be added by calling EmbedFS or and embed.FS or a fs.FS
 | 
			
		||||
type Configuration struct {
 | 
			
		||||
	FSNames   []string
 | 
			
		||||
	LocalFS   bool
 | 
			
		||||
	OnlyLocal bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewConfiguration creates a new configuration for the multiFS if local is true is checked first.
 | 
			
		||||
// by leaving fsNames empty all filesystems will be checked in no particular order (it loops through a map[string]fs.FS),
 | 
			
		||||
// with most fuctions except walk.
 | 
			
		||||
// returning the first matching file, Walk will walk all filesystems if it is called by an empty fsNames,
 | 
			
		||||
// or only the specified filesystems if fsNames is not empty.
 | 
			
		||||
// you can also manually set OnlyLocal to true after creating the configuration to only check the local filesystem.
 | 
			
		||||
func NewConfiguration(useLocal bool, fsNames ...string) Configuration {
 | 
			
		||||
	return Configuration{
 | 
			
		||||
		LocalFS: useLocal,
 | 
			
		||||
		FSNames: fsNames,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// multiFS is a list of filesystems.
 | 
			
		||||
type multiFS map[string]fs.FS
 | 
			
		||||
 | 
			
		||||
func (m multiFS) Open(path string, config Configuration) (fs.File, error) {
 | 
			
		||||
	path = filepath.Clean(path)
 | 
			
		||||
	path = strings.ReplaceAll(path, "\\", "/")
 | 
			
		||||
	if (config.LocalFS || config.OnlyLocal) && localIsValid {
 | 
			
		||||
		file, err := localFS.Open(path)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			return file, nil
 | 
			
		||||
		}
 | 
			
		||||
	} else if !localIsValid && config.OnlyLocal {
 | 
			
		||||
		return nil, errors.New("local filesystem is not valid")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !config.OnlyLocal {
 | 
			
		||||
		if len(config.FSNames) > 0 {
 | 
			
		||||
			for _, name := range config.FSNames {
 | 
			
		||||
				// check if the name exists in the filesystems
 | 
			
		||||
				fsys, ok := m[name]
 | 
			
		||||
				if !ok {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				file, err := fsys.Open(path)
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					return file, nil
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			for _, fsys := range m {
 | 
			
		||||
				file, err := fsys.Open(path)
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					return file, nil
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil, os.ErrNotExist
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Stat returns the fileInfo for the first matching file info in the filesystem.
 | 
			
		||||
func (m multiFS) Stat(path string, config Configuration) (fs.FileInfo, error) {
 | 
			
		||||
	path = filepath.Clean(path)
 | 
			
		||||
	path = strings.ReplaceAll(path, "\\", "/")
 | 
			
		||||
	if (config.LocalFS || config.OnlyLocal) && localIsValid {
 | 
			
		||||
		fileInfo, err := fs.Stat(localFS, path)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			return fileInfo, nil
 | 
			
		||||
		}
 | 
			
		||||
	} else if !localIsValid && config.OnlyLocal {
 | 
			
		||||
		return nil, errors.New("local filesystem is not valid")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !config.OnlyLocal {
 | 
			
		||||
		if len(config.FSNames) > 0 {
 | 
			
		||||
			for _, name := range config.FSNames {
 | 
			
		||||
				// check if the name exists in the filesystems
 | 
			
		||||
				fsys, ok := m[name]
 | 
			
		||||
				if !ok {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				file, err := fsys.Open(path)
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					fileInfo, err := file.Stat()
 | 
			
		||||
					if err == nil {
 | 
			
		||||
						return fileInfo, nil
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			for _, fsys := range m {
 | 
			
		||||
				file, err := fsys.Open(path)
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					fileInfo, err := file.Stat()
 | 
			
		||||
					if err == nil {
 | 
			
		||||
						return fileInfo, nil
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil, os.ErrNotExist
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ReadFile reads the first matching file in the filesystems and returns the contents as a byte slice
 | 
			
		||||
func (m multiFS) ReadFile(path string, config Configuration) ([]byte, error) {
 | 
			
		||||
	path = filepath.Clean(path)
 | 
			
		||||
	path = strings.ReplaceAll(path, "\\", "/")
 | 
			
		||||
	if (config.LocalFS || config.OnlyLocal) && localIsValid {
 | 
			
		||||
		data, err := fs.ReadFile(localFS, path)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			return data, nil
 | 
			
		||||
		}
 | 
			
		||||
	} else if !localIsValid && config.OnlyLocal {
 | 
			
		||||
		return nil, errors.New("local filesystem is not valid")
 | 
			
		||||
	}
 | 
			
		||||
	if !config.OnlyLocal {
 | 
			
		||||
		if len(config.FSNames) > 0 {
 | 
			
		||||
			for _, name := range config.FSNames {
 | 
			
		||||
				//Check if the name exists in the filesystems
 | 
			
		||||
				fsys, ok := m[name]
 | 
			
		||||
				if !ok {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				data, err := fs.ReadFile(fsys, path)
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					return data, nil
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			for _, fsys := range m {
 | 
			
		||||
				data, err := fs.ReadFile(fsys, path)
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					return data, nil
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil, os.ErrNotExist
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ReadDir reads the first matching directory in the filesystems and returns a list of directory entries
 | 
			
		||||
func (m multiFS) ReadDir(path string, config Configuration) ([]fs.DirEntry, error) {
 | 
			
		||||
	path = filepath.Clean(path)
 | 
			
		||||
	path = strings.ReplaceAll(path, "\\", "/")
 | 
			
		||||
	if (config.LocalFS || config.OnlyLocal) && localIsValid {
 | 
			
		||||
		data, err := fs.ReadDir(localFS, path)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			return data, nil
 | 
			
		||||
		}
 | 
			
		||||
	} else if !localIsValid && config.OnlyLocal {
 | 
			
		||||
		return nil, errors.New("local filesystem is not valid")
 | 
			
		||||
	}
 | 
			
		||||
	if !config.OnlyLocal {
 | 
			
		||||
		if len(config.FSNames) > 0 {
 | 
			
		||||
			for _, name := range config.FSNames {
 | 
			
		||||
				//Check if the name exists in the filesystems
 | 
			
		||||
				fsys, ok := m[name]
 | 
			
		||||
				if !ok {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				data, err := fs.ReadDir(fsys, path)
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					return data, nil
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			for _, fsys := range m {
 | 
			
		||||
				data, err := fs.ReadDir(fsys, path)
 | 
			
		||||
				if err == nil {
 | 
			
		||||
					return data, nil
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil, os.ErrNotExist
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Walk walks each filesystem in the multiFS performing the walkFn on each file or directory
 | 
			
		||||
func (m multiFS) Walk(path string, walkFn fs.WalkDirFunc, config Configuration) error {
 | 
			
		||||
	path = filepath.Clean(path)
 | 
			
		||||
	path = strings.ReplaceAll(path, "\\", "/")
 | 
			
		||||
	var err error
 | 
			
		||||
	if (config.LocalFS || config.OnlyLocal) && localIsValid {
 | 
			
		||||
		walkErr := fs.WalkDir(localFS, path, walkFn)
 | 
			
		||||
		if walkErr != nil {
 | 
			
		||||
			errors.Join(err, walkErr)
 | 
			
		||||
		}
 | 
			
		||||
	} else if !localIsValid && config.OnlyLocal {
 | 
			
		||||
		errors.Join(err, errors.New("local filesystem is not valid"))
 | 
			
		||||
	}
 | 
			
		||||
	if !config.OnlyLocal {
 | 
			
		||||
		if len(config.FSNames) > 0 {
 | 
			
		||||
			for _, name := range config.FSNames {
 | 
			
		||||
				// check if the name exists in the filesystems
 | 
			
		||||
				fsys, ok := m[name]
 | 
			
		||||
				if !ok {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
				walkErr := fs.WalkDir(fsys, path, walkFn)
 | 
			
		||||
				if walkErr != nil {
 | 
			
		||||
					errors.Join(err, walkErr)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			for _, fsys := range m {
 | 
			
		||||
				walkErr := fs.WalkDir(fsys, path, walkFn)
 | 
			
		||||
				if walkErr != nil {
 | 
			
		||||
					errors.Join(err, walkErr)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var embeddedFS multiFS
 | 
			
		||||
 | 
			
		||||
// EmbedFS sets the embedded filesystem to use for loading files
 | 
			
		||||
// This function can be called multiple times to add multiple embedded filesystems
 | 
			
		||||
// However, please note that unless you use Walk, the functions acting on these added filesystems
 | 
			
		||||
// will only return the first matching result. So make sure your filenames/directory names are unique between the filesystems
 | 
			
		||||
// Or specify the filesystem to use when calling the function
 | 
			
		||||
func EmbedFS(fs embed.FS, names ...string) {
 | 
			
		||||
	//Check if a name is provided otherwise set name to "embedded" plus a number until it is unique
 | 
			
		||||
	name := "embedded"
 | 
			
		||||
	if len(names) > 0 {
 | 
			
		||||
		name = names[0]
 | 
			
		||||
	}
 | 
			
		||||
	for i := 0; embeddedFS[name] != nil; i++ {
 | 
			
		||||
		name = "embedded" + strconv.Itoa(i)
 | 
			
		||||
	}
 | 
			
		||||
	embeddedFS[name] = fs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Walk is an extension of fs.WalkDir for any specified fileSystems
 | 
			
		||||
// that have been embedded with EmbedFS
 | 
			
		||||
// or all of them if no names are specified(It will return the first matching file)
 | 
			
		||||
// and optionally the local game filesystem
 | 
			
		||||
// Configuration can be created with NewConfiguration
 | 
			
		||||
func Walk(dir string, config Configuration, walkFn fs.WalkDirFunc) error {
 | 
			
		||||
	return embeddedFS.Walk(dir, walkFn, config)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ReadFile is an extension of fs.ReadFile for any specified fileSystems
 | 
			
		||||
// that have been embedded with EmbedFS
 | 
			
		||||
// or all of them if no names are specified(It will return the first matching file)
 | 
			
		||||
// and optionally the local game filesystem
 | 
			
		||||
// Configuration can be created with NewConfiguration
 | 
			
		||||
func ReadFile(path string, config Configuration) ([]byte, error) {
 | 
			
		||||
	return embeddedFS.ReadFile(path, config)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ReadDir is an extension of fs.ReadDir for any specified fileSystems
 | 
			
		||||
// that have been embedded with EmbedFS
 | 
			
		||||
// or all of them if no names are specified(It will return the first matching file)
 | 
			
		||||
// and optionally the local game filesystem
 | 
			
		||||
// Configuration can be created with NewConfiguration
 | 
			
		||||
func ReadDir(path string, config Configuration) ([]fs.DirEntry, error) {
 | 
			
		||||
	return embeddedFS.ReadDir(path, config)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Open is an extension of os.Open for any specified fileSystems
 | 
			
		||||
// that have been embedded with EmbedFS
 | 
			
		||||
// or all of them if no names are specified(It will return the first matching file)
 | 
			
		||||
// and optionally the local game filesystem
 | 
			
		||||
// Configuration can be created with NewConfiguration
 | 
			
		||||
func Open(path string, config Configuration) (fs.File, error) {
 | 
			
		||||
	return embeddedFS.Open(path, config)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Stat is an extension of fs.Stat for any specified fileSystems
 | 
			
		||||
// that have been embedded with EmbedFS
 | 
			
		||||
// or all of them if no names are specified(It will return the first matching file)
 | 
			
		||||
// and optionally the local game filesystem
 | 
			
		||||
// Configuration can be created with NewConfiguration
 | 
			
		||||
func Stat(path string, config Configuration) (fs.FileInfo, error) {
 | 
			
		||||
	return embeddedFS.Stat(path, config)
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue