github.com/haraldrudell/parl@v0.4.176/pfs/traverser-next.go (about)

     1  /*
     2  © 2023–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package pfs
     7  
     8  import (
     9  	"io/fs"
    10  	"path/filepath"
    11  )
    12  
    13  // Next returns the next file-system entry
    14  //   - Next ends with [ResultEntry.DirEntry] nil, ie. [ResultEntry.IsEnd] returns true
    15  //     or ResultEntry.Reason == REnd
    16  //   - symlinks and directories can be skipped by invoking [ResultEntry.Skip].
    17  //     Those have ResultEntry.Reason == RSkippable
    18  //   - symlinks have information based on the symlink source but [ResultEntry.Abs] is the
    19  //     fully resolved symlink target
    20  //   - [ResultEntry.ProvidedPath] is a path to the entry based upon the initially
    21  //     provided path. May be empty string
    22  //   - [ResultEntry.Abs] is an absolute symlink-free clean path but only available when
    23  //     [ResultEntry.Err] is nil
    24  //   - [ResultEntry.Err] holds any error associated with the returned entry
    25  //   - —
    26  //   - result.Err is from:
    27  //   - — process working directory cannot be read
    28  //   - — directory read error or [os.Readlink] or [os.Lstat] failed
    29  func (t *Traverser) Next() (result ResultEntry) {
    30  	var entry ResultEntry
    31  
    32  	// if pending initial path, create its root and entry
    33  	if t.initialPath != "" {
    34  		entry = t.createInitialRoot()
    35  	} else {
    36  		for {
    37  
    38  			// process any pending returned entries
    39  			if len(t.skippables) > 0 {
    40  				entry = t.skippables[0]
    41  				t.skippables[0] = ResultEntry{}
    42  				t.skippables = t.skippables[1:]
    43  
    44  				// handle skip and error
    45  				if t.skipCheck(entry.No) {
    46  					entry = ResultEntry{}
    47  					continue // skip: not a directory that should be listed
    48  				}
    49  
    50  				// symlink that wasn’t skipped
    51  				if entry.Type()&fs.ModeSymlink != 0 {
    52  					t.processSymlink(entry.Abs)
    53  					entry = ResultEntry{}
    54  					continue // entry complete
    55  				}
    56  
    57  				// read directory that wasn’t skipped
    58  				if entry.Err = t.readDir(entry.Abs, entry.ProvidedPath); entry.Err == nil {
    59  					entry = ResultEntry{}
    60  					continue // directory read successfully
    61  				}
    62  				entry.Reason = RDirBad
    63  				// the directory is returned again for the error
    64  
    65  				// process pending directory entries
    66  			} else if len(t.dirEntries) > 0 {
    67  				var dir = t.dirEntries[0]
    68  				// number of directory entries typically ranges a dozen up to 3,000
    69  				// one small slice alloc equals copy of 3,072 bytes: trimleft_bench_test.go
    70  				// sizeof dirEntry is 48 bytes: 64 elements is 3,072 bytes
    71  				// alloc is once per directory, copy is once per directory entry
    72  				// number of copied elements for n is: [n(n+1)]/2: if 11 or more directory entries: alloc is faster
    73  				// do alloc here
    74  				t.dirEntries[0] = dirEntry{}
    75  				t.dirEntries = t.dirEntries[1:]
    76  				var name = dir.dirEntry.Name()
    77  				entry.ProvidedPath = filepath.Join(dir.providedPath, name)
    78  				entry.Abs = filepath.Join(dir.abs, name)
    79  				entry.DirEntry = dir.dirEntry
    80  
    81  				// process any additional roots
    82  			} else {
    83  				var root *Root2
    84  				for t.rootIndex+1 < t.rootsRegistry.ListLength() {
    85  					t.rootIndex++
    86  					if root = t.rootsRegistry.GetValue(t.rootIndex); root != nil {
    87  						break
    88  					}
    89  				}
    90  				if root != nil {
    91  					entry.ProvidedPath = root.ProvidedPath
    92  					entry.Abs = root.Abs
    93  					// either DirEntry or Err will be non-nil
    94  					entry.DirEntry, entry.Err = AddDirEntry(entry.Abs)
    95  				} else {
    96  
    97  					// out of entries
    98  					return // REnd
    99  				}
   100  			}
   101  
   102  			// possibly return the entry
   103  			//	- entry has ProvidedPath and (DirEntry/Abs or Err)
   104  			//	- if entry is read from directory, IsDir/Type is always available
   105  			//	- if entry.Err is non-nil, Abs and Name/IsDir/Type/Info are unavailable
   106  			//	- entry may be any modeType including directory or symlink
   107  
   108  			// resolve error-free symlinks
   109  			if entry.Err == nil && entry.Type()&fs.ModeSymlink != 0 {
   110  				// the symlink can be:
   111  				//	- broken: entry.Err is non-nil
   112  				//	- matching or a descendant of an existing root: ignore
   113  				//	- a separate location creating a new root
   114  				//	- a parent directory obsoleting an existing root
   115  
   116  				// resolve all symlinks returning absolute, symlink-free, clean path
   117  				var abs string
   118  				if abs, entry.Err = filepath.EvalSymlinks(entry.Abs); entry.Err == nil {
   119  					var dirEntry fs.DirEntry
   120  					if dirEntry, entry.Err = AddDirEntry(abs); entry.Err == nil {
   121  						// ProvidedPath is symlink source
   122  						entry.Abs = abs
   123  						// DirEntry is symlink source
   124  						_ = dirEntry
   125  						entry.Reason = RSkippable
   126  						entry.No = t.skipNo.Add(1)
   127  						t.skippables = append(t.skippables, entry)
   128  					}
   129  				}
   130  				if entry.Err != nil {
   131  					entry.Reason = RSymlinkBad
   132  				}
   133  			}
   134  
   135  			// if entry is an obsolete root, it has already been traversed
   136  			if entry.Err == nil && t.obsoleteRoots.HasAbs(entry.Abs) {
   137  				entry = ResultEntry{}
   138  				continue
   139  			}
   140  
   141  			break
   142  		}
   143  	}
   144  
   145  	// the entry is to be returned
   146  	if entry.Err == nil && entry.IsDir() {
   147  		entry.No = t.skipNo.Add(1)
   148  		t.skippables = append(t.skippables, entry)
   149  	}
   150  	result = entry
   151  	if result.Reason == REnd {
   152  		if result.Err == nil {
   153  			if result.No != 0 {
   154  				result.Reason = RSkippable
   155  				result.SkipEntry = t.skip
   156  			} else {
   157  				result.Reason = REntry
   158  			}
   159  		} else {
   160  			result.Reason = RError
   161  		}
   162  	}
   163  
   164  	return // return of good or errored entry
   165  }