github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/internal/walkdir_iterate_test.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package internal
    16  
    17  import (
    18  	"errors"
    19  	"io"
    20  	"io/fs"
    21  	"os"
    22  	pathpkg "path"
    23  	"path/filepath"
    24  	"reflect"
    25  	"sort"
    26  	"testing"
    27  	"testing/fstest"
    28  	"time"
    29  
    30  	scalibrfs "github.com/google/osv-scalibr/fs"
    31  )
    32  
    33  // These are the same tests as in io/fs/walk_test.go, but ignoring the order of walking.
    34  
    35  type Node struct {
    36  	name    string
    37  	entries []*Node // nil if the entry is a file
    38  	mark    int
    39  }
    40  
    41  var tree = &Node{
    42  	"testdata",
    43  	[]*Node{
    44  		{"a", nil, 0},
    45  		{"b", []*Node{}, 0},
    46  		{"c", nil, 0},
    47  		{
    48  			"d",
    49  			[]*Node{
    50  				{"x", nil, 0},
    51  				{"y", []*Node{}, 0},
    52  				{
    53  					"z",
    54  					[]*Node{
    55  						{"u", nil, 0},
    56  						{"v", nil, 0},
    57  					},
    58  					0,
    59  				},
    60  			},
    61  			0,
    62  		},
    63  	},
    64  	0,
    65  }
    66  
    67  func walkTree(n *Node, path string, f func(path string, n *Node)) {
    68  	f(path, n)
    69  	for _, e := range n.entries {
    70  		walkTree(e, pathpkg.Join(path, e.name), f)
    71  	}
    72  }
    73  
    74  func makeTree() scalibrfs.FS {
    75  	fsys := fstest.MapFS{}
    76  	walkTree(tree, tree.name, func(path string, n *Node) {
    77  		if n.entries == nil {
    78  			fsys[path] = &fstest.MapFile{}
    79  		} else {
    80  			fsys[path] = &fstest.MapFile{Mode: fs.ModeDir}
    81  		}
    82  	})
    83  	return fsys
    84  }
    85  
    86  // Assumes that each node name is unique. Good enough for a test.
    87  // If clearErr is true, any incoming error is cleared before return. The errors
    88  // are always accumulated, though.
    89  func mark(tree *Node, entry fs.DirEntry, err error, errors *[]error, clearErr bool) error {
    90  	name := entry.Name()
    91  	walkTree(tree, tree.name, func(path string, n *Node) {
    92  		if n.name == name {
    93  			n.mark++
    94  		}
    95  	})
    96  	if err != nil {
    97  		*errors = append(*errors, err)
    98  		if clearErr {
    99  			return nil
   100  		}
   101  		return err
   102  	}
   103  	return nil
   104  }
   105  
   106  func TestWalkDir(t *testing.T) {
   107  	t.Chdir(t.TempDir())
   108  
   109  	fsys := makeTree()
   110  	errors := make([]error, 0, 10)
   111  	clearErr := true
   112  	markFn := func(path string, entry fs.DirEntry, err error) error {
   113  		return mark(tree, entry, err, &errors, clearErr)
   114  	}
   115  	// Expect no errors.
   116  	err := WalkDirUnsorted(fsys, ".", markFn, nil)
   117  	if err != nil {
   118  		t.Fatalf("no error expected, found: %s", err)
   119  	}
   120  	if len(errors) != 0 {
   121  		t.Fatalf("unexpected errors: %s", errors)
   122  	}
   123  	walkTree(tree, tree.name, func(path string, n *Node) {
   124  		if n.mark != 1 {
   125  			t.Errorf("node %s mark = %d; expected 1", path, n.mark)
   126  		}
   127  		n.mark = 0
   128  	})
   129  }
   130  
   131  func TestIssue51617(t *testing.T) {
   132  	dir := t.TempDir()
   133  	for _, sub := range []string{"a", filepath.Join("a", "bad"), filepath.Join("a", "next")} {
   134  		if err := os.Mkdir(filepath.Join(dir, sub), 0755); err != nil {
   135  			t.Fatal(err)
   136  		}
   137  	}
   138  	bad := filepath.Join(dir, "a", "bad")
   139  	if err := os.Chmod(bad, 0); err != nil {
   140  		t.Fatal(err)
   141  	}
   142  	//nolint:errcheck
   143  	defer os.Chmod(bad, 0700) // avoid errors on cleanup
   144  	var saw []string
   145  	err := WalkDirUnsorted(scalibrfs.DirFS(dir), ".", func(path string, d fs.DirEntry, err error) error {
   146  		if err != nil {
   147  			return filepath.SkipDir
   148  		}
   149  		if d.IsDir() {
   150  			saw = append(saw, path)
   151  		}
   152  		return nil
   153  	}, nil)
   154  	if err != nil {
   155  		t.Fatal(err)
   156  	}
   157  	want := []string{".", "a", "a/bad", "a/next"}
   158  	sort.Strings(saw)
   159  	if !reflect.DeepEqual(saw, want) {
   160  		t.Errorf("got directories %v, want %v", saw, want)
   161  	}
   162  }
   163  
   164  // FS implementation that doesn't implement ReadDirFile on the sub-directories.
   165  type fakeFS struct{}
   166  
   167  func (f fakeFS) Open(name string) (fs.File, error) {
   168  	return &fakeFile{}, nil
   169  }
   170  func (fakeFS) ReadDir(name string) ([]fs.DirEntry, error) {
   171  	if name == "." { // root dir
   172  		return []fs.DirEntry{
   173  			&fakeDirEntry{name: "file1.txt", isDir: false},
   174  			&fakeDirEntry{name: "dir", isDir: true},
   175  		}, nil
   176  	} else if name == "dir" {
   177  		return []fs.DirEntry{&fakeDirEntry{name: "file2.txt", isDir: false}}, nil
   178  	}
   179  	return nil, errors.New("file not found")
   180  }
   181  func (fakeFS) Stat(name string) (fs.FileInfo, error) {
   182  	return &fakeDirEntry{name: name, isDir: name == "." || name == "dir"}, nil
   183  }
   184  
   185  type fakeFile struct{}
   186  
   187  func (f *fakeFile) Stat() (fs.FileInfo, error)                { return nil, nil }
   188  func (f *fakeFile) Read(buffer []byte) (count int, err error) { return 0, io.EOF }
   189  func (*fakeFile) Close() error                                { return nil }
   190  
   191  var fakeFSTree = &Node{
   192  	".",
   193  	[]*Node{
   194  		{"file1.txt", nil, 0},
   195  		{
   196  			"dir",
   197  			[]*Node{{"file2.txt", nil, 0}},
   198  			0,
   199  		},
   200  	},
   201  	0,
   202  }
   203  
   204  type fakeDirEntry struct {
   205  	name  string
   206  	isDir bool
   207  }
   208  
   209  func (f *fakeDirEntry) Name() string               { return f.name }
   210  func (f *fakeDirEntry) Size() int64                { return 0 }
   211  func (f *fakeDirEntry) Mode() fs.FileMode          { return 0 }
   212  func (f *fakeDirEntry) ModTime() time.Time         { return time.Time{} }
   213  func (f *fakeDirEntry) IsDir() bool                { return f.isDir }
   214  func (f *fakeDirEntry) Type() fs.FileMode          { return 0 }
   215  func (f *fakeDirEntry) Info() (fs.FileInfo, error) { return nil, errors.New("not implemented") }
   216  func (f *fakeDirEntry) Sys() any                   { return nil }
   217  
   218  func TestWalkDirFallbackToDirFS(t *testing.T) {
   219  	fsys := &fakeFS{}
   220  	errors := make([]error, 0, 10)
   221  	clearErr := true
   222  	markFn := func(path string, entry fs.DirEntry, err error) error {
   223  		return mark(fakeFSTree, entry, err, &errors, clearErr)
   224  	}
   225  	// Expect no errors.
   226  	if err := WalkDirUnsorted(fsys, ".", markFn, nil); err != nil {
   227  		t.Fatalf("no error expected, found: %s", err)
   228  	}
   229  	if len(errors) != 0 {
   230  		t.Fatalf("unexpected errors: %s", errors)
   231  	}
   232  	walkTree(fakeFSTree, fakeFSTree.name, func(path string, n *Node) {
   233  		if n.mark != 1 {
   234  			t.Errorf("node %s mark = %d; expected 1", path, n.mark)
   235  		}
   236  		n.mark = 0
   237  	})
   238  }