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