github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/internal/walkdir_iterate.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  // Derived from Go's src/io/fs/walk.go
    18  //
    19  // Copyright 2020 The Go Authors. All rights reserved.
    20  // Use of this source code is governed by a BSD-style
    21  // license that can be found in the LICENSE file.
    22  
    23  import (
    24  	"errors"
    25  	"io"
    26  	"io/fs"
    27  	"path"
    28  
    29  	scalibrfs "github.com/google/osv-scalibr/fs"
    30  	"github.com/google/osv-scalibr/fs/diriterate"
    31  )
    32  
    33  type postWalkDirFunc func(path string, d fs.DirEntry)
    34  
    35  // walkDirUnsorted recursively descends path, calling walkDirFn. From caller side this function
    36  // should be equivalent to fs.walkDir, except that the files are not ordered, but walked in the
    37  // order returned by the file system. This function does not store the full directory in memory,
    38  // which should reduce the memory footprint from O(n) to O(1).
    39  // This might lead to inconsistencies in the seen files, as another process might delete a file
    40  // during the walk, which is then not seen by the walk. That problem existed also with fs.WalkDir,
    41  // which would return files which do not exist anymore. More details for unix:
    42  // https://man7.org/linux/man-pages/man2/getdents.2.html
    43  func walkDirUnsorted(fsys scalibrfs.FS, name string, d fs.DirEntry, walkDirFn fs.WalkDirFunc, postFN postWalkDirFunc) error {
    44  	if postFN != nil {
    45  		defer postFN(name, d)
    46  	}
    47  	// This is the main call to walkDirFn for files and directories, without errors.
    48  	if err := walkDirFn(name, d, nil); err != nil || !d.IsDir() {
    49  		if errors.Is(err, fs.SkipDir) && d.IsDir() {
    50  			// Successfully skipped directory.
    51  			err = nil
    52  		}
    53  		return err
    54  	}
    55  
    56  	dirs, err := diriterate.ReadDir(fsys, name)
    57  	if err != nil {
    58  		// Second call, to report ReadDir error.
    59  		// Same error handling as in fs.WalkDir: If an error occurred, the walkDirFn is called again,
    60  		// which can decide to continue (nil), SkipDir or skip all by other errors (e.g. SkipAll).
    61  		err = walkDirFn(name, d, err)
    62  		if err != nil {
    63  			if errors.Is(err, fs.SkipDir) && d.IsDir() {
    64  				err = nil
    65  			}
    66  			return err
    67  		}
    68  		// End iteration after an error
    69  		return nil
    70  	}
    71  	// Error can be ignored, as no write is happening.
    72  	defer dirs.Close()
    73  
    74  	for {
    75  		d1, err := dirs.Next()
    76  		if err != nil {
    77  			if errors.Is(err, io.EOF) {
    78  				break
    79  			}
    80  			// Second call, to report ReadDir error.
    81  			// Same error handling as in fs.WalkDir: If an error occurred, the walkDirFn is called again,
    82  			// which can decide to continue (nil), SkipDir or skip all by other errors (e.g. SkipAll).
    83  			err = walkDirFn(name, d, err)
    84  			if err != nil {
    85  				if errors.Is(err, fs.SkipDir) && d.IsDir() {
    86  					err = nil
    87  				}
    88  				return err
    89  			}
    90  			// End iteration after an error
    91  			return nil
    92  		}
    93  		name1 := path.Join(name, d1.Name())
    94  		if err := walkDirUnsorted(fsys, name1, d1, walkDirFn, postFN); err != nil {
    95  			if errors.Is(err, fs.SkipDir) {
    96  				break
    97  			}
    98  			return err
    99  		}
   100  	}
   101  	return nil
   102  }
   103  
   104  // WalkDirUnsorted walks the file tree rooted at root, calling fn for each file or
   105  // directory in the tree, including root. It also calls postFN after the walk of the
   106  // file or dir has finished, including the traversal of all sub-directories.
   107  //
   108  // All errors that arise visiting files and directories are filtered by fn:
   109  // see the [fs.WalkDirFunc] documentation for details.
   110  //
   111  // WalkDirUnsorted does not follow symbolic links found in directories,
   112  // but if root itself is a symbolic link, its target will be walked.
   113  func WalkDirUnsorted(fsys scalibrfs.FS, root string, fn fs.WalkDirFunc, postFN postWalkDirFunc) error {
   114  	info, err := fs.Stat(fsys, root)
   115  	if err != nil {
   116  		err = fn(root, nil, err)
   117  	} else {
   118  		err = walkDirUnsorted(fsys, root, fs.FileInfoToDirEntry(info), fn, postFN)
   119  	}
   120  	if errors.Is(err, fs.SkipDir) || errors.Is(err, fs.SkipAll) {
   121  		return nil
   122  	}
   123  	return err
   124  }