package iovfs import ( "fmt" "io/fs" "strings" "time" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/stringutil" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs/internal" ) type RealpathFS interface { fs.FS Realpath(path string) (string, error) } type WritableFS interface { fs.FS WriteFile(path string, data []byte, perm fs.FileMode) error MkdirAll(path string, perm fs.FileMode) error // Removes `path` and all its contents. Will return the first error it encounters. Remove(path string) error Chtimes(path string, aTime time.Time, mTime time.Time) error } type FsWithSys interface { vfs.FS FSys() fs.FS } // From creates a new FS from an [fs.FS]. // // For paths like `c:/foo/bar`, fsys will be used as though it's rooted at `/` and the path is `/c:/foo/bar`. // // If the provided [fs.FS] implements [RealpathFS], it will be used to implement the Realpath method. // If the provided [fs.FS] implements [WritableFS], it will be used to implement the WriteFile method. // // From does not actually handle case-insensitivity; ensure the passed in [fs.FS] // respects case-insensitive file names if needed. Consider using [vfstest.FromMap] for testing. func From(fsys fs.FS, useCaseSensitiveFileNames bool) FsWithSys { var realpath func(path string) (string, error) if fsys, ok := fsys.(RealpathFS); ok { realpath = func(path string) (string, error) { rest, hadSlash := strings.CutPrefix(path, "/") rp, err := fsys.Realpath(rest) if err != nil { return "", err } if hadSlash { return "/" + rp, nil } return rp, nil } } else { realpath = func(path string) (string, error) { return path, nil } } var writeFile func(path string, content string, writeByteOrderMark bool) error var mkdirAll func(path string) error var remove func(path string) error var chtimes func(path string, aTime time.Time, mTime time.Time) error if fsys, ok := fsys.(WritableFS); ok { writeFile = func(path string, content string, writeByteOrderMark bool) error { rest, _ := strings.CutPrefix(path, "/") if writeByteOrderMark { // Strada uses \uFEFF because NodeJS requires it, but substitutes it with the correct BOM based on the // output encoding. \uFEFF is actually the BOM for big-endian UTF-16. For UTF-8 the actual BOM is // \xEF\xBB\xBF. content = stringutil.AddUTF8ByteOrderMark(content) } return fsys.WriteFile(rest, []byte(content), 0o666) } mkdirAll = func(path string) error { rest, _ := strings.CutPrefix(path, "/") return fsys.MkdirAll(rest, 0o777) } remove = func(path string) error { rest, _ := strings.CutPrefix(path, "/") return fsys.Remove(rest) } chtimes = func(path string, aTime time.Time, mTime time.Time) error { rest, _ := strings.CutPrefix(path, "/") return fsys.Chtimes(rest, aTime, mTime) } } else { writeFile = func(string, string, bool) error { panic("writeFile not supported") } mkdirAll = func(string) error { panic("mkdirAll not supported") } remove = func(string) error { panic("remove not supported") } chtimes = func(string, time.Time, time.Time) error { panic("chtimes not supported") } } return &ioFS{ common: internal.Common{ RootFor: func(root string) fs.FS { if root == "/" { return fsys } p := tspath.RemoveTrailingDirectorySeparator(root) sub, err := fs.Sub(fsys, p) if err != nil { panic(fmt.Sprintf("vfs: failed to create sub file system for %q: %v", p, err)) } return sub }, }, useCaseSensitiveFileNames: useCaseSensitiveFileNames, realpath: realpath, writeFile: writeFile, mkdirAll: mkdirAll, remove: remove, chtimes: chtimes, fsys: fsys, } } type ioFS struct { common internal.Common useCaseSensitiveFileNames bool realpath func(path string) (string, error) writeFile func(path string, content string, writeByteOrderMark bool) error mkdirAll func(path string) error remove func(path string) error chtimes func(path string, aTime time.Time, mTime time.Time) error fsys fs.FS } var _ FsWithSys = (*ioFS)(nil) func (vfs *ioFS) UseCaseSensitiveFileNames() bool { return vfs.useCaseSensitiveFileNames } func (vfs *ioFS) DirectoryExists(path string) bool { return vfs.common.DirectoryExists(path) } func (vfs *ioFS) FileExists(path string) bool { return vfs.common.FileExists(path) } func (vfs *ioFS) GetAccessibleEntries(path string) vfs.Entries { return vfs.common.GetAccessibleEntries(path) } func (vfs *ioFS) Stat(path string) vfs.FileInfo { _ = internal.RootLength(path) // Assert path is rooted return vfs.common.Stat(path) } func (vfs *ioFS) ReadFile(path string) (contents string, ok bool) { return vfs.common.ReadFile(path) } func (vfs *ioFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { return vfs.common.WalkDir(root, walkFn) } func (vfs *ioFS) Remove(path string) error { _ = internal.RootLength(path) // Assert path is rooted return vfs.remove(path) } func (vfs *ioFS) Chtimes(path string, aTime time.Time, mTime time.Time) error { _ = internal.RootLength(path) // Assert path is rooted return vfs.chtimes(path, aTime, mTime) } func (vfs *ioFS) Realpath(path string) string { root, rest := internal.SplitPath(path) // splitPath normalizes the path into parts (e.g. "c:/foo/bar" -> "c:/", "foo/bar") // Put them back together to call realpath. realpath, err := vfs.realpath(root + rest) if err != nil { return path } return realpath } func (vfs *ioFS) WriteFile(path string, content string, writeByteOrderMark bool) error { _ = internal.RootLength(path) // Assert path is rooted if err := vfs.writeFile(path, content, writeByteOrderMark); err == nil { return nil } if err := vfs.mkdirAll(tspath.GetDirectoryPath(tspath.NormalizePath(path))); err != nil { return err } return vfs.writeFile(path, content, writeByteOrderMark) } func (vfs *ioFS) FSys() fs.FS { return vfs.fsys }