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