From 7b7ff8ae8f97393b2e6ae6e061c1f780077c32b6 Mon Sep 17 00:00:00 2001 From: Puneet Dixit Date: Thu, 4 Jun 2026 16:19:07 +0530 Subject: [PATCH] fix: skip inaccessible children when listing directories (#5958) --- files/file.go | 57 +++++++++++++++++++++++++++-- files/file_test.go | 89 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 2 deletions(-) diff --git a/files/file.go b/files/file.go index a034abc0..e84c7e2a 100644 --- a/files/file.go +++ b/files/file.go @@ -18,6 +18,7 @@ import ( "path" "path/filepath" "regexp" + "sort" "strings" "time" @@ -452,8 +453,7 @@ func (i *FileInfo) addSubtitle(fPath string) { } func (i *FileInfo) readListing(checker rules.Checker, readHeader bool, calcImgRes bool) error { - afs := &afero.Afero{Fs: i.Fs} - dir, err := afs.ReadDir(i.Path) + dir, err := readDir(i.Fs, i.Path) if err != nil { return err } @@ -535,3 +535,56 @@ func (i *FileInfo) readListing(checker rules.Checker, readHeader bool, calcImgRe i.Listing = listing return nil } + +func readDir(afs afero.Fs, dirname string) ([]os.FileInfo, error) { + dir, err := afero.ReadDir(afs, dirname) + if err == nil { + return dir, nil + } + + dir, fallbackErr := readDirNames(afs, dirname) + if fallbackErr != nil { + return nil, err + } + + return dir, nil +} + +func readDirNames(afs afero.Fs, dirname string) ([]os.FileInfo, error) { + file, err := afs.Open(dirname) + if err != nil { + return nil, err + } + + names, err := file.Readdirnames(-1) + if closeErr := file.Close(); err == nil && closeErr != nil { + err = closeErr + } + if err != nil { + return nil, err + } + + sort.Strings(names) + dir := make([]os.FileInfo, 0, len(names)) + for _, name := range names { + fPath := path.Join(dirname, name) + info, err := lstatIfPossible(afs, fPath) + if err != nil { + log.Printf("Skipping inaccessible file %s: %v", fPath, err) + continue + } + + dir = append(dir, info) + } + + return dir, nil +} + +func lstatIfPossible(afs afero.Fs, name string) (os.FileInfo, error) { + if lstaterFs, ok := afs.(afero.Lstater); ok { + info, _, err := lstaterFs.LstatIfPossible(name) + return info, err + } + + return afs.Stat(name) +} diff --git a/files/file_test.go b/files/file_test.go index d1f647f3..9b46a85d 100644 --- a/files/file_test.go +++ b/files/file_test.go @@ -2,6 +2,7 @@ package files import ( "os" + "path" "path/filepath" "testing" @@ -131,3 +132,91 @@ func TestStatRejectsLinkedAncestorEscape(t *testing.T) { t.Fatalf("expected in-scope file to be served, got %v", err) } } + +type allowAllChecker struct{} + +func (allowAllChecker) Check(string) bool { + return true +} + +type inaccessibleChildFs struct { + afero.Fs + child string +} + +func (fs inaccessibleChildFs) Open(name string) (afero.File, error) { + file, err := fs.Fs.Open(name) + if err != nil { + return nil, err + } + + if path.Clean(name) == "/" { + return inaccessibleChildDir{File: file}, nil + } + + return file, nil +} + +func (fs inaccessibleChildFs) Stat(name string) (os.FileInfo, error) { + if path.Clean(name) == fs.child { + return nil, os.ErrPermission + } + + return fs.Fs.Stat(name) +} + +func (fs inaccessibleChildFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { + if path.Clean(name) == fs.child { + return nil, false, os.ErrPermission + } + + if lstater, ok := fs.Fs.(afero.Lstater); ok { + return lstater.LstatIfPossible(name) + } + + info, err := fs.Fs.Stat(name) + return info, false, err +} + +type inaccessibleChildDir struct { + afero.File +} + +func (dir inaccessibleChildDir) Readdir(int) ([]os.FileInfo, error) { + return nil, os.ErrPermission +} + +func TestReadListingSkipsInaccessibleChildren(t *testing.T) { + memFs := afero.NewMemMapFs() + for _, dir := range []string{"/media", "/proton-mount"} { + if err := memFs.Mkdir(dir, 0o755); err != nil { + t.Fatal(err) + } + } + + file, err := NewFileInfo(&FileOptions{ + Fs: inaccessibleChildFs{Fs: memFs, child: "/proton-mount"}, + Path: "/", + Expand: true, + Checker: allowAllChecker{}, + }) + if err != nil { + t.Fatal(err) + } + + if file.Listing == nil { + t.Fatal("expected root listing") + } + + if got := len(file.Items); got != 1 { + t.Fatalf("expected one accessible child, got %d", got) + } + + if got := file.Items[0].Name; got != "media" { + t.Fatalf("expected accessible child to be listed, got %q", got) + } + + if got := file.NumDirs; got != 1 { + t.Fatalf("expected one listed directory, got %d", got) + } +}