[module] kffs

This commit is contained in:
shynd 2024-06-26 05:58:50 +02:00
parent 38cfe30644
commit 14e5731922
No known key found for this signature in database
GPG Key ID: 31DC000B2E21D69D
1 changed files with 363 additions and 0 deletions

363
pkg/KFData/KFFS/KFFS.go Normal file
View File

@ -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)
}