github.com/avfs/avfs@v0.33.1-0.20240303173310-c6ba67c33eb7/vfs.go (about)

     1  //
     2  //  Copyright 2021 The AVFS authors
     3  //
     4  //  Licensed under the Apache License, Version 2.0 (the "License");
     5  //  you may not use this file except in compliance with the License.
     6  //  You may obtain a copy of the License at
     7  //
     8  //  	http://www.apache.org/licenses/LICENSE-2.0
     9  //
    10  //  Unless required by applicable law or agreed to in writing, software
    11  //  distributed under the License is distributed on an "AS IS" BASIS,
    12  //  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  //  See the License for the specific language governing permissions and
    14  //  limitations under the License.
    15  //
    16  
    17  package avfs
    18  
    19  import (
    20  	"errors"
    21  	"io"
    22  	"io/fs"
    23  	"os"
    24  	"path/filepath"
    25  	"sort"
    26  	"strings"
    27  	_ "unsafe" // for go:linkname only.
    28  )
    29  
    30  // cleanGlobPath prepares path for glob matching.
    31  func cleanGlobPath[T VFSBase](vfs T, path string) string {
    32  	pathSeparator := vfs.PathSeparator()
    33  
    34  	switch path {
    35  	case "":
    36  		return "."
    37  	case string(pathSeparator):
    38  		// do nothing to the path
    39  		return path
    40  	default:
    41  		return path[0 : len(path)-1] // chop off trailing separator
    42  	}
    43  }
    44  
    45  // cleanGlobPathWindows is Windows version of cleanGlobPath.
    46  func cleanGlobPathWindows[T VFSBase](vfs T, path string) (prefixLen int, cleaned string) {
    47  	vollen := VolumeNameLen(vfs, path)
    48  
    49  	switch {
    50  	case path == "":
    51  		return 0, "."
    52  	case vollen+1 == len(path) && IsPathSeparator(vfs, path[len(path)-1]): // /, \, C:\ and C:/
    53  		// do nothing to the path
    54  		return vollen + 1, path
    55  	case vollen == len(path) && len(path) == 2: // C:
    56  		return vollen, path + "." // convert C: into C:.
    57  	default:
    58  		if vollen >= len(path) {
    59  			vollen = len(path) - 1
    60  		}
    61  
    62  		return vollen, path[0 : len(path)-1] // chop off trailing separator
    63  	}
    64  }
    65  
    66  // Create creates or truncates the named file. If the file already exists,
    67  // it is truncated. If the file does not exist, it is created with mode 0666
    68  // (before umask). If successful, methods on the returned DummyFile can
    69  // be used for I/O; the associated file descriptor has mode O_RDWR.
    70  // If there is an error, it will be of type *PathError.
    71  func Create[T VFSBase](vfs T, name string) (File, error) {
    72  	return vfs.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, DefaultFilePerm)
    73  }
    74  
    75  // CreateTemp creates a new temporary file in the directory dir,
    76  // opens the file for reading and writing, and returns the resulting file.
    77  // The filename is generated by taking pattern and adding a random string to the end.
    78  // If pattern includes a "*", the random string replaces the last "*".
    79  // If dir is the empty string, CreateTemp uses the default directory for temporary files, as returned by TempDir.
    80  // Multiple programs or goroutines calling CreateTemp simultaneously will not choose the same file.
    81  // The caller can use the file's Name method to find the pathname of the file.
    82  // It is the caller's responsibility to remove the file when it is no longer needed.
    83  func CreateTemp[T VFSBase](vfs T, dir, pattern string) (File, error) {
    84  	const op = "createtemp"
    85  
    86  	if dir == "" {
    87  		dir = TempDir(vfs)
    88  	}
    89  
    90  	prefix, suffix, err := prefixAndSuffix(vfs, pattern)
    91  	if err != nil {
    92  		return nil, &fs.PathError{Op: op, Path: pattern, Err: err}
    93  	}
    94  
    95  	prefix = joinPath(vfs, dir, prefix)
    96  
    97  	try := 0
    98  
    99  	for {
   100  		name := prefix + nextRandom() + suffix
   101  
   102  		f, err := vfs.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o600)
   103  		if IsExist(err) {
   104  			try++
   105  			if try < 10000 {
   106  				continue
   107  			}
   108  
   109  			return nil, &fs.PathError{Op: op, Path: dir + string(vfs.PathSeparator()) + prefix + "*" + suffix, Err: fs.ErrExist}
   110  		}
   111  
   112  		return f, err
   113  	}
   114  }
   115  
   116  // FromUnixPath returns valid path for Unix or Windows from a unix path.
   117  // For Windows systems, absolute paths are prefixed with the default volume
   118  // and relative paths are preserved.
   119  func FromUnixPath[T VFSBase](vfs T, path string) string {
   120  	if vfs.OSType() != OsWindows {
   121  		return path
   122  	}
   123  
   124  	if path[0] != '/' {
   125  		return FromSlash(vfs, path)
   126  	}
   127  
   128  	return Join(vfs, DefaultVolume, FromSlash(vfs, path))
   129  }
   130  
   131  // Glob returns the names of all files matching pattern or nil
   132  // if there is no matching file. The syntax of patterns is the same
   133  // as in Match. The pattern may describe hierarchical names such as
   134  // /usr/*/bin/ed (assuming the Separator is '/').
   135  //
   136  // Glob ignores file system errors such as I/O errors reading directories.
   137  // The only possible returned error is ErrBadPattern, when pattern
   138  // is malformed.
   139  func Glob[T VFSBase](vfs T, pattern string) (matches []string, err error) {
   140  	// Check pattern is well-formed.
   141  	if _, err = Match(vfs, pattern, ""); err != nil {
   142  		return nil, err
   143  	}
   144  
   145  	if !hasMeta(vfs, pattern) {
   146  		if _, err = vfs.Lstat(pattern); err != nil {
   147  			return nil, nil
   148  		}
   149  
   150  		return []string{pattern}, nil
   151  	}
   152  
   153  	dir, file := Split(vfs, pattern)
   154  	volumeLen := 0
   155  
   156  	if vfs.OSType() == OsWindows {
   157  		volumeLen, dir = cleanGlobPathWindows(vfs, dir)
   158  	} else {
   159  		dir = cleanGlobPath(vfs, dir)
   160  	}
   161  
   162  	if !hasMeta(vfs, dir[volumeLen:]) {
   163  		return glob(vfs, dir, file, nil)
   164  	}
   165  
   166  	// Prevent infinite recursion. See issue 15879.
   167  	if dir == pattern {
   168  		return nil, filepath.ErrBadPattern
   169  	}
   170  
   171  	var m []string
   172  
   173  	m, err = Glob(vfs, dir)
   174  	if err != nil {
   175  		return
   176  	}
   177  
   178  	for _, d := range m {
   179  		matches, err = glob(vfs, d, file, matches)
   180  		if err != nil {
   181  			return
   182  		}
   183  	}
   184  
   185  	return //nolint:nakedret // Adapted from standard library.
   186  }
   187  
   188  // glob searches for files matching pattern in the directory dir
   189  // and appends them to matches. If the directory cannot be
   190  // opened, it returns the existing matches. New matches are
   191  // added in lexicographical order.
   192  func glob[T VFSBase](vfs T, dir, pattern string, matches []string) (m []string, e error) {
   193  	m = matches
   194  
   195  	fi, err := vfs.Stat(dir)
   196  	if err != nil {
   197  		return // ignore I/O error
   198  	}
   199  
   200  	if !fi.IsDir() {
   201  		return // ignore I/O error
   202  	}
   203  
   204  	d, err := vfs.OpenFile(dir, os.O_RDONLY, 0)
   205  	if err != nil {
   206  		return // ignore I/O error
   207  	}
   208  
   209  	defer d.Close()
   210  
   211  	names, _ := d.Readdirnames(-1)
   212  	sort.Strings(names)
   213  
   214  	for _, n := range names {
   215  		matched, err := Match(vfs, pattern, n)
   216  		if err != nil {
   217  			return m, err
   218  		}
   219  
   220  		if matched {
   221  			m = append(m, Join(vfs, dir, n))
   222  		}
   223  	}
   224  
   225  	return //nolint:nakedret // Adapted from standard library.
   226  }
   227  
   228  // hasMeta reports whether path contains any of the magic characters
   229  // recognized by Match.
   230  func hasMeta[T VFSBase](vfs T, path string) bool {
   231  	magicChars := `*?[`
   232  
   233  	if vfs.OSType() != OsWindows {
   234  		magicChars = `*?[\`
   235  	}
   236  
   237  	return strings.ContainsAny(path, magicChars)
   238  }
   239  
   240  // HomeDir returns the home directory of the file system.
   241  func HomeDir[T VFSBase](vfs T, basePath string) string {
   242  	switch vfs.OSType() {
   243  	case OsWindows:
   244  		return Join(vfs, basePath, `\Users`)
   245  	default:
   246  		return Join(vfs, "/home")
   247  	}
   248  }
   249  
   250  // HomeDirUser returns the home directory of the user.
   251  // If the file system does not have an identity manager, the root directory is returned.
   252  func HomeDirUser[T VFSBase](vfs T, basePath string, u UserReader) string {
   253  	name := u.Name()
   254  	if vfs.OSType() == OsWindows {
   255  		return Join(vfs, HomeDir(vfs, basePath), name)
   256  	}
   257  
   258  	if name == AdminUserName(vfs.OSType()) {
   259  		return "/root"
   260  	}
   261  
   262  	return Join(vfs, HomeDir(vfs, basePath), name)
   263  }
   264  
   265  // HomeDirPerm return the default permission for home directories.
   266  func HomeDirPerm() fs.FileMode {
   267  	return 0o700
   268  }
   269  
   270  // IsExist returns a boolean indicating whether the error is known to report
   271  // that a file or directory already exists. It is satisfied by ErrExist as
   272  // well as some syscall errors.
   273  //
   274  // This function predates errors.Is. It only supports errors returned by
   275  // the os package. New code should use errors.Is(err, fs.ErrExist).
   276  func IsExist(err error) bool {
   277  	return errors.Is(err, fs.ErrExist)
   278  }
   279  
   280  // IsNotExist returns a boolean indicating whether the error is known to
   281  // report that a file or directory does not exist. It is satisfied by
   282  // ErrNotExist as well as some syscall errors.
   283  //
   284  // This function predates errors.Is. It only supports errors returned by
   285  // the os package. New code should use errors.Is(err, fs.ErrNotExist).
   286  func IsNotExist(err error) bool {
   287  	return errors.Is(err, fs.ErrNotExist)
   288  }
   289  
   290  func joinPath[T VFSBase](vfs T, dir, name string) string {
   291  	if dir != "" && IsPathSeparator(vfs, dir[len(dir)-1]) {
   292  		return dir + name
   293  	}
   294  
   295  	return dir + string(vfs.PathSeparator()) + name
   296  }
   297  
   298  // MkdirTemp creates a new temporary directory in the directory dir
   299  // and returns the pathname of the new directory.
   300  // The new directory's name is generated by adding a random string to the end of pattern.
   301  // If pattern includes a "*", the random string replaces the last "*" instead.
   302  // If dir is the empty string, MkdirTemp uses the default directory for temporary files, as returned by TempDir.
   303  // Multiple programs or goroutines calling MkdirTemp simultaneously will not choose the same directory.
   304  // It is the caller's responsibility to remove the directory when it is no longer needed.
   305  func MkdirTemp[T VFSBase](vfs T, dir, pattern string) (string, error) {
   306  	const op = "mkdirtemp"
   307  
   308  	if dir == "" {
   309  		dir = vfs.TempDir()
   310  	}
   311  
   312  	prefix, suffix, err := prefixAndSuffix(vfs, pattern)
   313  	if err != nil {
   314  		return "", &fs.PathError{Op: op, Path: pattern, Err: err}
   315  	}
   316  
   317  	prefix = joinPath(vfs, dir, prefix)
   318  	try := 0
   319  
   320  	for {
   321  		name := prefix + nextRandom() + suffix
   322  
   323  		err := vfs.Mkdir(name, 0o700)
   324  		if err == nil {
   325  			return name, nil
   326  		}
   327  
   328  		if IsExist(err) {
   329  			try++
   330  			if try < 10000 {
   331  				continue
   332  			}
   333  
   334  			return "", &fs.PathError{Op: op, Path: dir + string(vfs.PathSeparator()) + prefix + "*" + suffix, Err: fs.ErrExist}
   335  		}
   336  
   337  		if IsNotExist(err) {
   338  			_, err := vfs.Stat(dir) //nolint:govet // declaration of "err" shadows declaration
   339  			if IsNotExist(err) {
   340  				return "", err
   341  			}
   342  		}
   343  
   344  		return "", err
   345  	}
   346  }
   347  
   348  // MkHomeDir creates and returns the home directory of a user.
   349  // If there is an error, it will be of type *PathError.
   350  func MkHomeDir[T VFSBase](vfs T, basePath string, u UserReader) (string, error) {
   351  	userDir := HomeDirUser(vfs, basePath, u)
   352  
   353  	err := vfs.Mkdir(userDir, HomeDirPerm())
   354  	if err != nil {
   355  		return "", err
   356  	}
   357  
   358  	switch vfs.OSType() {
   359  	case OsWindows:
   360  		err = vfs.MkdirAll(TempDirUser(vfs, basePath, u.Name()), DefaultDirPerm)
   361  	default:
   362  		err = vfs.Chown(userDir, u.Uid(), u.Gid())
   363  	}
   364  
   365  	if err != nil {
   366  		return "", err
   367  	}
   368  
   369  	return userDir, nil
   370  }
   371  
   372  // MkSystemDirs creates the system directories of a file system.
   373  func MkSystemDirs[T VFSBase](vfs T, dirs []DirInfo) error {
   374  	for _, dir := range dirs {
   375  		err := vfs.MkdirAll(dir.Path, dir.Perm)
   376  		if err != nil {
   377  			return err
   378  		}
   379  
   380  		if vfs.OSType() != OsWindows {
   381  			err = vfs.Chmod(dir.Path, dir.Perm)
   382  			if err != nil {
   383  				return err
   384  			}
   385  		}
   386  	}
   387  
   388  	return nil
   389  }
   390  
   391  // nextRandom is used in Utils.CreateTemp and Utils.MkdirTemp.
   392  //
   393  //go:linkname nextRandom os.nextRandom
   394  func nextRandom() string
   395  
   396  // prefixAndSuffix splits pattern by the last wildcard "*", if applicable,
   397  // returning prefix as the part before "*" and suffix as the part after "*".
   398  func prefixAndSuffix[T VFSBase](vfs T, pattern string) (prefix, suffix string, err error) {
   399  	for i := 0; i < len(pattern); i++ {
   400  		if IsPathSeparator(vfs, pattern[i]) {
   401  			return "", "", ErrPatternHasSeparator
   402  		}
   403  	}
   404  
   405  	if pos := strings.LastIndexByte(pattern, '*'); pos != -1 {
   406  		prefix, suffix = pattern[:pos], pattern[pos+1:]
   407  	} else {
   408  		prefix = pattern
   409  	}
   410  
   411  	return prefix, suffix, nil
   412  }
   413  
   414  // ReadDir reads the named directory,
   415  // returning all its directory entries sorted by filename.
   416  // If an error occurs reading the directory,
   417  // ReadDir returns the entries it was able to read before the error,
   418  // along with the error.
   419  func ReadDir[T VFSBase](vfs T, name string) ([]fs.DirEntry, error) {
   420  	f, err := vfs.OpenFile(name, os.O_RDONLY, 0)
   421  	if err != nil {
   422  		return nil, err
   423  	}
   424  
   425  	defer f.Close()
   426  
   427  	dirs, err := f.ReadDir(-1)
   428  
   429  	sort.Slice(dirs, func(i, j int) bool { return dirs[i].Name() < dirs[j].Name() })
   430  
   431  	return dirs, err
   432  }
   433  
   434  // ReadFile reads the named file and returns the contents.
   435  // A successful call returns err == nil, not err == EOF.
   436  // Because ReadFile reads the whole file, it does not treat an EOF from Read
   437  // as an error to be reported.
   438  func ReadFile[T VFSBase](vfs T, name string) ([]byte, error) {
   439  	f, err := vfs.OpenFile(name, os.O_RDONLY, 0)
   440  	if err != nil {
   441  		return nil, err
   442  	}
   443  
   444  	defer f.Close()
   445  
   446  	var size int
   447  
   448  	if info, err := f.Stat(); err == nil {
   449  		size64 := info.Size()
   450  		if int64(int(size64)) == size64 {
   451  			size = int(size64)
   452  		}
   453  	}
   454  
   455  	size++ // one byte for final read at EOF
   456  
   457  	// If a file claims a small size, read at least 512 bytes.
   458  	// In particular, files in Linux's /proc claim size 0 but
   459  	// then do not work right if read in small pieces,
   460  	// so an initial read of 1 byte would not work correctly.
   461  	if size < 512 {
   462  		size = 512
   463  	}
   464  
   465  	data := make([]byte, 0, size)
   466  
   467  	for {
   468  		if len(data) >= cap(data) {
   469  			d := append(data[:cap(data)], 0) //nolint:gocritic // append result not assigned to the same slice
   470  			data = d[:len(data)]
   471  		}
   472  
   473  		n, err := f.Read(data[len(data):cap(data)])
   474  		data = data[:len(data)+n]
   475  
   476  		if err != nil {
   477  			if err == io.EOF {
   478  				err = nil
   479  			}
   480  
   481  			return data, err
   482  		}
   483  	}
   484  }
   485  
   486  // SetUserByName sets the current user by name.
   487  // If the user is not found, the returned error is of type UnknownUserError.
   488  func SetUserByName[T VFSBase](vfs T, name string) error {
   489  	if !vfs.HasFeature(FeatIdentityMgr) {
   490  		return ErrPermDenied
   491  	}
   492  
   493  	if vfs.User().Name() == name {
   494  		return nil
   495  	}
   496  
   497  	u, err := vfs.Idm().LookupUser(name)
   498  	if err != nil {
   499  		return err
   500  	}
   501  
   502  	err = vfs.SetUser(u)
   503  
   504  	return err
   505  }
   506  
   507  // SplitAbs splits an absolute path immediately preceding the final Separator,
   508  // separating it into a directory and file name component.
   509  // If there is no Separator in path, splitPath returns an empty dir
   510  // and file set to path.
   511  // The returned values have the property that path = dir + PathSeparator + file.
   512  func SplitAbs[T VFSBase](vfs T, path string) (dir, file string) {
   513  	l := VolumeNameLen(vfs, path)
   514  
   515  	i := len(path) - 1
   516  	for i >= l && !IsPathSeparator(vfs, path[i]) {
   517  		i--
   518  	}
   519  
   520  	return path[:i], path[i+1:]
   521  }
   522  
   523  // SystemDirs returns an array of system directories always present in the file system.
   524  func SystemDirs[T VFSBase](vfs T, basePath string) []DirInfo {
   525  	switch vfs.OSType() {
   526  	case OsWindows:
   527  		if basePath == "" {
   528  			basePath = DefaultVolume
   529  		}
   530  
   531  		return []DirInfo{
   532  			{Path: HomeDir(vfs, basePath), Perm: DefaultDirPerm},
   533  			{Path: TempDirUser(vfs, basePath, AdminUserName(vfs.OSType())), Perm: DefaultDirPerm},
   534  			{Path: TempDirUser(vfs, basePath, DefaultName), Perm: DefaultDirPerm},
   535  			{Path: Join(vfs, basePath, `\Windows`), Perm: DefaultDirPerm},
   536  		}
   537  	default:
   538  		return []DirInfo{
   539  			{Path: HomeDir(vfs, basePath), Perm: HomeDirPerm()},
   540  			{Path: Join(vfs, basePath, "/root"), Perm: 0o700},
   541  			{Path: Join(vfs, basePath, "/tmp"), Perm: 0o777},
   542  		}
   543  	}
   544  }
   545  
   546  // TempDir returns the default directory to use for temporary files.
   547  //
   548  // On Unix systems, it returns $TMPDIR if non-empty, else /tmp.
   549  // On Windows, it uses GetTempPath, returning the first non-empty
   550  // value from %TMP%, %TEMP%, %USERPROFILE%, or the Windows directory.
   551  // On Plan 9, it returns /tmp.
   552  //
   553  // The directory is neither guaranteed to exist nor have accessible
   554  // permissions.
   555  func TempDir[T VFSBase](vfs T) string {
   556  	return TempDirUser(vfs, "", vfs.User().Name())
   557  }
   558  
   559  // TempDirUser returns the default directory to use for temporary files with for a specific user.
   560  func TempDirUser[T VFSBase](vfs T, basePath, username string) string {
   561  	if vfs.OSType() != OsWindows {
   562  		return Join(vfs, basePath, "/tmp")
   563  	}
   564  
   565  	if basePath == "" {
   566  		basePath = DefaultVolume
   567  	}
   568  
   569  	dir := Join(vfs, basePath, `\Users\`, username, `\AppData\Local\Temp`)
   570  
   571  	return dir
   572  }
   573  
   574  // ToOpenMode returns the open mode from the input flags.
   575  func ToOpenMode(flag int) OpenMode {
   576  	var om OpenMode
   577  
   578  	// Mask flags that can be used in read only mode (syscall.O_DIRECT for example)
   579  	if flag&0xFFF == os.O_RDONLY {
   580  		return OpenRead
   581  	}
   582  
   583  	if flag&os.O_RDWR != 0 {
   584  		om = OpenRead | OpenWrite
   585  	}
   586  
   587  	if flag&(os.O_EXCL|os.O_CREATE) == (os.O_EXCL | os.O_CREATE) {
   588  		om |= OpenCreate | OpenCreateExcl | OpenWrite
   589  	}
   590  
   591  	if flag&os.O_CREATE != 0 {
   592  		om |= OpenCreate | OpenWrite
   593  	}
   594  
   595  	if flag&os.O_APPEND != 0 {
   596  		om |= OpenAppend | OpenWrite
   597  	}
   598  
   599  	if flag&os.O_TRUNC != 0 {
   600  		om |= OpenTruncate | OpenWrite
   601  	}
   602  
   603  	if flag&os.O_WRONLY != 0 {
   604  		om |= OpenWrite
   605  	}
   606  
   607  	return om
   608  }
   609  
   610  // VolumeName returns leading volume name.
   611  // Given "C:\foo\bar" it returns "C:" on Windows.
   612  // Given "\\host\share\foo" it returns "\\host\share".
   613  // On other platforms it returns "".
   614  func VolumeName[T VFSBase](vfs T, path string) string {
   615  	return FromSlash(vfs, path[:VolumeNameLen(vfs, path)])
   616  }
   617  
   618  //go:linkname volumeNameLen path/filepath.volumeNameLen
   619  func volumeNameLen(path string) int
   620  
   621  // WalkDir walks the file tree rooted at root, calling fn for each file or
   622  // directory in the tree, including root.
   623  //
   624  // All errors that arise visiting files and directories are filtered by fn:
   625  // see the fs.WalkDirFunc documentation for details.
   626  //
   627  // The files are walked in lexical order, which makes the output deterministic
   628  // but requires WalkDir to read an entire directory into memory before proceeding
   629  // to walk that directory.
   630  //
   631  // WalkDir does not follow symbolic links.
   632  func WalkDir[T VFSBase](vfs T, root string, fn fs.WalkDirFunc) error {
   633  	info, err := vfs.Lstat(root)
   634  	if err != nil {
   635  		err = fn(root, nil, err)
   636  	} else {
   637  		err = walkDir(vfs, root, &statDirEntry{info}, fn)
   638  	}
   639  
   640  	if err == filepath.SkipDir {
   641  		return nil
   642  	}
   643  
   644  	return err
   645  }
   646  
   647  // walkDir recursively descends path, calling walkDirFn.
   648  func walkDir[T VFSBase](vfs T, path string, d fs.DirEntry, walkDirFn fs.WalkDirFunc) error {
   649  	if err := walkDirFn(path, d, nil); err != nil || !d.IsDir() {
   650  		if err == filepath.SkipDir && d.IsDir() {
   651  			// Successfully skipped directory.
   652  			err = nil
   653  		}
   654  
   655  		return err
   656  	}
   657  
   658  	dirs, err := ReadDir(vfs, path)
   659  	if err != nil {
   660  		// Second call, to report ReadDir error.
   661  		err = walkDirFn(path, d, err)
   662  		if err != nil {
   663  			return err
   664  		}
   665  	}
   666  
   667  	for _, d1 := range dirs {
   668  		path1 := Join(vfs, path, d1.Name())
   669  		if err := walkDir(vfs, path1, d1, walkDirFn); err != nil {
   670  			if err == filepath.SkipDir {
   671  				break
   672  			}
   673  
   674  			return err
   675  		}
   676  	}
   677  
   678  	return nil
   679  }
   680  
   681  type statDirEntry struct {
   682  	info fs.FileInfo
   683  }
   684  
   685  func (d *statDirEntry) Name() string               { return d.info.Name() }
   686  func (d *statDirEntry) IsDir() bool                { return d.info.IsDir() }
   687  func (d *statDirEntry) Type() fs.FileMode          { return d.info.Mode().Type() }
   688  func (d *statDirEntry) Info() (fs.FileInfo, error) { return d.info, nil }
   689  
   690  // WriteFile writes data to the named file, creating it if necessary.
   691  // If the file does not exist, WriteFile creates it with permissions perm (before umask);
   692  // otherwise WriteFile truncates it before writing, without changing permissions.
   693  func WriteFile[T VFSBase](vfs T, name string, data []byte, perm fs.FileMode) error {
   694  	f, err := vfs.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
   695  	if err != nil {
   696  		return err
   697  	}
   698  
   699  	_, err = f.Write(data)
   700  	if err1 := f.Close(); err1 != nil && err == nil {
   701  		err = err1
   702  	}
   703  
   704  	return err
   705  }