afero icon indicating copy to clipboard operation
afero copied to clipboard

ReadDir is not setting FileInfo up correctly for symlinks

Open XenoPhex opened this issue 3 years ago • 2 comments

TLDR: When one calls IsDir() on a FileInfo returned back from afero.ReadDir, and that FileInfo is for a symlink to another directory, the IsDir() function returns false. When one gets a FileInfo from os.Stat for the same symlink, IsDir() returns true.

Example Dir:

$ ll /tmp/foo
total 8
drwxr-xr-x   5 anand  wheel   160B Feb  2 16:48 .
drwxrwxrwt  13 root   wheel   416B Feb  2 17:28 ..
drwxr-xr-x   2 anand  wheel    64B Feb  2 16:47 bar
lrwxr-xr-x   1 anand  wheel     3B Feb  2 16:47 baz -> bar
-rw-r--r--   1 anand  wheel   731B Feb  2 17:05 main.go

Example code (from main.go):

package main

import (
	"fmt"
	"os"

	"github.com/spf13/afero"
)

func main() {
	fmt.Println("os: bar is a dir:", isDir("bar"))
	fmt.Println("os: baz is a dir:", isDir("baz"))

	fs := afero.NewOsFs()
	fmt.Println("afero: bar is a dir:", isDirAfero(fs, "bar"))
	fmt.Println("afero: baz is a dir:", isDirAfero(fs, "baz"))

	infos, err := afero.ReadDir(fs, "/tmp/foo")
	if err != nil {
		panic(err)
	}
	for _, info := range infos {
		fmt.Println("afero.ReadDir", info.Name(), "is a dir:", info.IsDir())
	}
}

func isDir(p string) bool {
	i, err := os.Stat(p)
	if err != nil {
		panic(err)
	}
	return i.IsDir()
}

func isDirAfero(fs afero.Fs, p string) bool {
	i, err := fs.Stat(p)
	if err != nil {
		panic(err)
	}
	return i.IsDir()
}

Output of main.go:

$ cd /tmp/foo
$ go run main.go
os: bar is a dir: true
os: baz is a dir: true
afero: bar is a dir: true
afero: baz is a dir: true
afero.ReadDir bar is a dir: true
afero.ReadDir baz is a dir: false
afero.ReadDir main.go is a dir: false

As we can see the following line from the output was unexpected:

afero.ReadDir baz is a dir: false

What I expected

I expected Info.IsDir() to return true for baz.

XenoPhex avatar Feb 03 '21 01:02 XenoPhex

This doesn't seem to be a library problem, this is due to the functioning of de 'os'.

	infos, _ := os.ReadDir("/tmp/foo")
	for _, info := range infos {
		fmt.Println("", info.Name(), "is a dir:", info.IsDir())
	}

the output is:

 bar is a dir: true
 baz is a dir: false

The error seems to be caused by using a different function to fill in the stat data: os.ReadDir, will call Lstat and not Stat function, in their subdirectories.

go/src/os/dir_unix.go You can check this by looking for the function func (f *File) readdir and see that the stats are filled by calling

info, err := lstat(f.name + "/" + string(name))

AndrusGerman avatar May 02 '22 15:05 AndrusGerman

This has an impact on afero.Walk as it relies on IsDir(). If you only care about following the symlink of the root directory and you're using OsFs, then using EvalSymlinks is a viable workaround. This, however, does not help my case as I must support different filesystems.

lcarva avatar Aug 07 '23 20:08 lcarva