diff --git a/go.mod b/go.mod index 89091984..f2db33c3 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/smartystreets/goconvey v1.7.2 github.com/spf13/cobra v1.8.1 github.com/termie/go-shutil v0.0.0-20140729215957-bcacb06fecae - github.com/wtsi-hgi/godirwalk v1.18.1 github.com/wtsi-ssg/wr v0.5.9 ) diff --git a/go.sum b/go.sum index 9bc6b957..064a3a97 100644 --- a/go.sum +++ b/go.sum @@ -284,8 +284,6 @@ github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPD github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/wtsi-hgi/godirwalk v1.18.1 h1:t7eaGXYBfTtfIEGLizPCC9fzASTvZtdhKEEri8TyyJs= -github.com/wtsi-hgi/godirwalk v1.18.1/go.mod h1:rLa4FlI9kdT7o67jwFos8qgaX3K2sMC6XI4FXJ1iVyk= github.com/wtsi-ssg/wr v0.5.9 h1:lJWNuJfVvhTpXQqxRN5RbffhvK3HMog0fFpUFznvoz8= github.com/wtsi-ssg/wr v0.5.9/go.mod h1:njSdCX+xv1xzzw3Oy3Smid6s/IyIQEvLsKbRwaq4fC8= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/walk/dirent.go b/walk/dirent.go index 4a51a898..b130e878 100644 --- a/walk/dirent.go +++ b/walk/dirent.go @@ -27,149 +27,240 @@ package walk import ( + "bytes" "io/fs" "os" "sync" - "unsafe" - - "github.com/wtsi-hgi/godirwalk" ) +func newDirentPool(size int) *sync.Pool { + return &sync.Pool{ + New: func() any { + return &Dirent{ + name: make([]byte, 0, size), + parent: nullDirEnt, + next: nullDirEnt, + } + }, + } +} + var ( - filePathPool64 = sync.Pool{New: func() any { x := make(FilePath, 0, 64); return &x }} //nolint:gochecknoglobals,mnd,nlreturn,lll - filePathPool128 = sync.Pool{New: func() any { x := make(FilePath, 0, 128); return &x }} //nolint:gochecknoglobals,mnd,nlreturn,lll - filePathPool256 = sync.Pool{New: func() any { x := make(FilePath, 0, 256); return &x }} //nolint:gochecknoglobals,mnd,nlreturn,lll - filePathPool512 = sync.Pool{New: func() any { x := make(FilePath, 0, 512); return &x }} //nolint:gochecknoglobals,mnd,nlreturn,lll - filePathPool1024 = sync.Pool{New: func() any { x := make(FilePath, 0, 1024); return &x }} //nolint:gochecknoglobals,mnd,nlreturn,lll - filePathPool2048 = sync.Pool{New: func() any { x := make(FilePath, 0, 2048); return &x }} //nolint:gochecknoglobals,mnd,nlreturn,lll - filePathPool4096 = sync.Pool{New: func() any { x := make(FilePath, 0, 4096); return &x }} //nolint:gochecknoglobals,mnd,nlreturn,lll + direntPool0 = newDirentPool(0) //nolint:gochecknoglobals + direntPool32 = newDirentPool(32) //nolint:gochecknoglobals,mnd + direntPool64 = newDirentPool(64) //nolint:gochecknoglobals,mnd + direntPool128 = newDirentPool(128) //nolint:gochecknoglobals,mnd + dirEntPool256 = newDirentPool(257) //nolint:gochecknoglobals,mnd + + nullDirEnt = new(Dirent) //nolint:gochecknoglobals ) -// FilePath is a byte-slice of a path, utilising object pools to reduce memory -// allocations. -// -// It is the clients responsibility to call the Done method once it is no longer -// needed. -type FilePath []byte +func init() { //nolint:gochecknoinits + nullDirEnt.parent = nullDirEnt + nullDirEnt.next = nullDirEnt +} -func newFilePathSize(size int) *FilePath { +func getDirent(size int) *Dirent { switch { + case size == 0: + return direntPool0.Get().(*Dirent) //nolint:forcetypeassert,errcheck + case size <= 32: //nolint:mnd + return direntPool32.Get().(*Dirent) //nolint:forcetypeassert,errcheck case size <= 64: //nolint:mnd - return filePathPool64.Get().(*FilePath) //nolint:forcetypeassert + return direntPool64.Get().(*Dirent) //nolint:forcetypeassert,errcheck case size <= 128: //nolint:mnd - return filePathPool128.Get().(*FilePath) //nolint:forcetypeassert - case size <= 256: //nolint:mnd - return filePathPool256.Get().(*FilePath) //nolint:forcetypeassert - case size <= 512: //nolint:mnd - return filePathPool512.Get().(*FilePath) //nolint:forcetypeassert - case size <= 1024: //nolint:mnd - return filePathPool1024.Get().(*FilePath) //nolint:forcetypeassert - case size <= 2048: //nolint:mnd - return filePathPool2048.Get().(*FilePath) //nolint:forcetypeassert + return direntPool128.Get().(*Dirent) //nolint:forcetypeassert,errcheck } - return filePathPool4096.Get().(*FilePath) //nolint:forcetypeassert + return dirEntPool256.Get().(*Dirent) //nolint:forcetypeassert,errcheck +} + +func putDirent(d *Dirent) { + d.name = d.name[:0] + d.parent = nullDirEnt + d.next = nullDirEnt + d.depth = 0 + + switch cap(d.name) { + case 0: + direntPool0.Put(d) + case 32: //nolint:mnd + direntPool32.Put(d) + case 64: //nolint:mnd + direntPool64.Put(d) + case 128: //nolint:mnd + direntPool128.Put(d) + default: + dirEntPool256.Put(d) + } } -// NewFilePath creates a new FilePath, setting the value to the given string. -func NewFilePath(path string) *FilePath { - c := newFilePathSize(len(path)) - c.writeString(path) +// Dirent represents a file system directory entry (a file or a directory), +// providing information about the entry's path, type and inode. +type Dirent struct { + parent *Dirent // left + name []byte + depth int16 + + // Type is the type bits of the file mode of this entry. + Type fs.FileMode + + // Inode is the file system inode number for this entry. + Inode uint64 - return c + next *Dirent // right + ready sync.Mutex } -func (f *FilePath) writeString(str string) { - *f = append(*f, str...) +// IsDir returns true if we are a directory. +func (d *Dirent) IsDir() bool { + return d.Type.IsDir() } -func (f *FilePath) writeBytes(p []byte) { - *f = append(*f, p...) +// IsRegular returns true if we are a regular file. +func (d *Dirent) IsRegular() bool { + return d.Type.IsRegular() } -// Done deallocates the underlying byte-slice; any uses of the Bytes method are -// now invalid and may change. -func (f *FilePath) Done() { //nolint:gocyclo - *f = (*f)[:0] +// IsSymlink returns true if we are a symlink. +func (d *Dirent) IsSymlink() bool { + return d.Type&os.ModeSymlink != 0 +} - switch cap(*f) { - case 64: //nolint:mnd - filePathPool64.Put(f) - case 128: //nolint:mnd - filePathPool128.Put(f) - case 256: //nolint:mnd - filePathPool256.Put(f) - case 512: //nolint:mnd - filePathPool512.Put(f) - case 1024: //nolint:mnd - filePathPool1024.Put(f) - case 2048: //nolint:mnd - filePathPool2048.Put(f) - case 4096: //nolint:mnd - filePathPool4096.Put(f) +func (d *Dirent) appendTo(p []byte) []byte { + if d.parent != nil { + p = d.parent.appendTo(p) } + + return append(p, d.name...) } -func (f *FilePath) sub(d *godirwalk.Dirent) *FilePath { - name := d.Name() - size := len(*f) + len(name) +// Bytes returns the FilePath as a literal byte-slice. +func (d *Dirent) Bytes() []byte { + return d.appendTo(nil) +} - if d.IsDir() { - size++ +func (d *Dirent) compare(e *Dirent) int { + if d.depth < e.depth { + e = e.getDepth(d.depth) + } else if d.depth > e.depth { + d = d.getDepth(e.depth) } - c := newFilePathSize(size) - - c.writeBytes(*f) - c.writeString(name) + return e.compareTo(d) +} - if d.IsDir() { - c.writeString("/") +func (d *Dirent) getDepth(n int16) *Dirent { + for d.depth != n { + d = d.parent } - return c + return d } -// Bytes returns the FilePath as a literal byte-slice. -func (f *FilePath) Bytes() []byte { - return *f +func (d *Dirent) compareTo(e *Dirent) int { + if d == e { + return 0 + } + + cmp := d.parent.compareTo(e.parent) + + if cmp == 0 { + return bytes.Compare(d.name, e.name) + } + + return cmp } -func (f *FilePath) string() string { - return unsafe.String(&(*f)[0], len(*f)) +func (d *Dirent) done() *Dirent { + next := d.next + d.next = nullDirEnt + + if len(d.name) == 0 { + putDirent(d.parent) + } + + if !d.IsDir() { + putDirent(d) + } + + return next } -// Dirent represents a file system directory entry (a file or a directory), -// providing information about the entry's path, type and inode. -type Dirent struct { - // Path is the complete path to the directory entry (including both - // directory and basename) - Path *FilePath +func (d *Dirent) insert(e *Dirent) *Dirent { //nolint:gocyclo + if d == nullDirEnt { + return e + } - // Type is the type bits of the file mode of this entry. - Type os.FileMode + switch bytes.Compare(d.name, e.name) { + case 1: + d.parent = d.parent.insert(e) + case -1: + d.next = d.next.insert(e) + } - // Inode is the file system inode number for this entry. - Inode uint64 + d.setDepth() + + switch d.parent.depth - d.next.depth { + case -2: + if d.next.parent.depth > d.next.next.depth { + d.next = d.next.rotateRight() + } + + return d.rotateLeft() + case 2: //nolint:mnd + if d.parent.next.depth > d.parent.parent.depth { + d.parent = d.parent.rotateLeft() + } + + return d.rotateRight() + } + + return d } -// newDirentForDirectoryPath returns a Dirent for the given directory, with -// a Type for directories and no Inode. -func newDirentForDirectoryPath(dir string) Dirent { - return Dirent{Path: NewFilePath(dir), Type: fs.ModeDir} +func (d *Dirent) setDepth() { + if d == nullDirEnt { + return + } + + if d.parent.depth > d.next.depth { + d.depth = d.parent.depth + 1 + } else { + d.depth = d.next.depth + 1 + } } -// IsDir returns true if we are a directory. -func (d *Dirent) IsDir() bool { - return d.Type.IsDir() +func (d *Dirent) rotateLeft() *Dirent { + n := d.next + d.next = n.parent + n.parent = d + + d.setDepth() + n.setDepth() + + return n } -// IsRegular returns true if we are a regular file. -func (d *Dirent) IsRegular() bool { - return d.Type.IsRegular() +func (d *Dirent) rotateRight() *Dirent { + n := d.parent + d.parent = n.next + n.next = d + + d.setDepth() + n.setDepth() + + return n } -// IsSymlink returns true if we are a symlink. -func (d *Dirent) IsSymlink() bool { - return d.Type&os.ModeSymlink != 0 +func (d *Dirent) flatten(parent, prev *Dirent, depth int16) *Dirent { + if d == nullDirEnt { + return prev + } + + d.parent.flatten(parent, prev, depth).next = d + d.parent = parent + d.depth = depth + + return d.next.flatten(parent, d, depth) } diff --git a/walk/dirent_test.go b/walk/dirent_test.go index 7bfa648c..2775222b 100644 --- a/walk/dirent_test.go +++ b/walk/dirent_test.go @@ -71,11 +71,4 @@ func TestDirent(t *testing.T) { So(d.IsRegular(), ShouldBeFalse) So(d.IsSymlink(), ShouldBeTrue) }) - - Convey("You can make a fake Direct for directories", t, func() { - d := newDirentForDirectoryPath("/a/dir") - So(d.IsDir(), ShouldBeTrue) - So(d.IsRegular(), ShouldBeFalse) - So(d.IsSymlink(), ShouldBeFalse) - }) } diff --git a/walk/file.go b/walk/file.go index 21b0b03a..3e734d8c 100644 --- a/walk/file.go +++ b/walk/file.go @@ -33,10 +33,13 @@ import ( "path/filepath" "strconv" "sync" + "unsafe" ) const userOnlyPerm = 0700 +const maxPathLength = 4096 + // non-ascii bytes could become \xXX (4x the length at worst), the two // speech-marks are +2 and a newline is +1. const maxQuotedPathLength = 4096*4 + 2 + 1 @@ -162,12 +165,16 @@ func NewFiles(outDir string, n int) (*Files, error) { // // It will terminate the walk if writes to our output files fail. func (f *Files) WritePaths() PathCallback { - var quoted [maxQuotedPathLength]byte + var ( + quoted [maxQuotedPathLength]byte + tmpPath [maxPathLength]byte + ) return func(entry *Dirent) error { - defer entry.Path.Done() - - return f.writePath(append(strconv.AppendQuote(quoted[:0], entry.Path.string()), '\n')) + return f.writePath(append( + strconv.AppendQuote( + quoted[:0], unsafe.String(&tmpPath[0], len(entry.appendTo(tmpPath[:0]))), + ), '\n')) } } diff --git a/walk/walk.go b/walk/walk.go index a42f3d73..673097d0 100644 --- a/walk/walk.go +++ b/walk/walk.go @@ -30,15 +30,15 @@ package walk import ( + "bytes" "context" + "errors" + "io/fs" "os" "path/filepath" "slices" - "sort" - "strings" - "sync" - - "github.com/wtsi-hgi/godirwalk" + "syscall" + "unsafe" ) const walkers = 16 @@ -77,19 +77,6 @@ func New(cb PathCallback, includDirs, ignoreSymlinks bool) *Walker { // will be provided problematic paths encountered during the walk. type ErrorCallback func(path string, err error) -type pathRequest struct { - path *FilePath - response chan []Dirent -} - -var pathRequestPool = sync.Pool{ //nolint:gochecknoglobals - New: func() any { - return &pathRequest{ - response: make(chan []Dirent), - } - }, -} - // Walk will discover all the paths nested under the given dir, and send them to // our PathCallback. // @@ -99,66 +86,93 @@ var pathRequestPool = sync.Pool{ //nolint:gochecknoglobals // errors will mean the path isn't output, but the walk will continue and this // method won't return an error. func (w *Walker) Walk(dir string, errCB ErrorCallback) error { + inode, err := getInitialInode(dir) + if err != nil { + return err + } + dir = filepath.Clean(dir) + "/" - requestCh := make(chan *pathRequest) - sortedRequestCh := make(chan *pathRequest) - direntCh := make(chan Dirent, dirsChSize) - flowControl := newController() + requestCh := make(chan *Dirent) + sortedRequestCh := make(chan *Dirent) ctx, stop := context.WithCancel(context.Background()) for range walkers { - go w.handleDirReads(ctx, sortedRequestCh, errCB) + go w.handleDirReads(ctx, sortedRequestCh, requestCh, errCB, w.ignoreSymlinks) } - go func() { - walkDirectory(ctx, newDirentForDirectoryPath(dir), - flowControl, createPathRequestor(requestCh), w.sendDirs) - close(direntCh) - }() + go sortDirents(ctx, requestCh, sortedRequestCh) - go sortPathRequests(ctx, requestCh, sortedRequestCh) - go flowControl.PassControl(direntCh) + r := &Dirent{ + name: []byte(dir), + Type: fs.ModeDir, + Inode: inode, + next: nullDirEnt, + } + + r.ready.Lock() + + sortedRequestCh <- r defer stop() - return w.sendDirentsToPathCallback(direntCh) + return w.sendDirentsToPathCallback(r) } -func createPathRequestor(requestCh chan *pathRequest) func(*FilePath) []Dirent { - return func(path *FilePath) []Dirent { - pr := pathRequestPool.Get().(*pathRequest) //nolint:errcheck,forcetypeassert - defer pathRequestPool.Put(pr) - - pr.path = path +func getInitialInode(dir string) (uint64, error) { + fi, err := os.Stat(dir) + if err != nil { + return 0, err + } - requestCh <- pr + if !fi.IsDir() { + return 0, fs.ErrInvalid + } - return <-pr.response + st, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + return 0, fs.ErrInvalid } + + return st.Ino, nil } -func (w *Walker) sendDirentsToPathCallback(direntCh <-chan Dirent) error { - for dirent := range direntCh { - if err := w.pathCB(&dirent); err != nil { - return err +func (w *Walker) sendDirentsToPathCallback(r *Dirent) error { + for ; r != nullDirEnt; r = r.done() { + if len(r.name) > 0 { + if err := w.sendDirentToPathCallback(r); err != nil { + return err + } } } return nil } -type heap []*pathRequest +func (w *Walker) sendDirentToPathCallback(r *Dirent) error { + isDir := r.IsDir() -func pathCompare(a, b *pathRequest) int { - return strings.Compare(b.path.string(), a.path.string()) + if w.sendDirs || !isDir { + if err := w.pathCB(r); err != nil { + return err + } + } + + if isDir { + r.ready.Lock() + r.ready.Unlock() //nolint:staticcheck + } + + return nil } -func (h *heap) Insert(req *pathRequest) { - pos, _ := slices.BinarySearchFunc(*h, req, pathCompare) +type heap []*Dirent + +func (h *heap) Insert(req *Dirent) { + pos, _ := slices.BinarySearchFunc(*h, req, (*Dirent).compare) *h = slices.Insert(*h, pos, req) } -func (h heap) Top() *pathRequest { +func (h heap) Top() *Dirent { return h[len(h)-1] } @@ -166,12 +180,12 @@ func (h *heap) Pop() { *h = (*h)[:len(*h)-1] } -func (h *heap) Push(req *pathRequest) { +func (h *heap) Push(req *Dirent) { *h = append(*h, req) } -func sortPathRequests(ctx context.Context, requestCh <-chan *pathRequest, //nolint:gocyclo - sortedRequestCh chan<- *pathRequest) { +func sortDirents(ctx context.Context, requestCh <-chan *Dirent, //nolint:gocyclo + sortedRequestCh chan<- *Dirent) { var h heap for { @@ -195,113 +209,184 @@ func sortPathRequests(ctx context.Context, requestCh <-chan *pathRequest, //noli } } -func (w *Walker) handleDirReads(ctx context.Context, requests chan *pathRequest, errCB ErrorCallback) { +func (w *Walker) handleDirReads(ctx context.Context, sortedRequests, requestCh chan *Dirent, + errCB ErrorCallback, ignoreSymlinks bool) { buffer := make([]byte, os.Getpagesize()) + var pathBuffer [maxPathLength + 1]byte + Loop: for { select { case <-ctx.Done(): break Loop - case request := <-requests: - children, err := godirwalk.ReadDirents(request.path.string(), buffer) + case request := <-sortedRequests: + l := len(request.appendTo(pathBuffer[:0])) + pathBuffer[l] = 0 + + root, err := scan(buffer, &pathBuffer[0], ignoreSymlinks) if err != nil { - errCB(string(request.path.Bytes()), err) + errCB(string(pathBuffer[:l]), err) } - request.response <- w.childrenToDirents(children, request.path) + go scanChildDirs(ctx, requestCh, request, root) } } } -func (w *Walker) childrenToDirents(children godirwalk.Dirents, parent *FilePath) []Dirent { - dirents := make([]Dirent, 0, len(children)) +func scanChildDirs(ctx context.Context, requestCh chan *Dirent, request, root *Dirent) { + marker := getDirent(0) + marker.next = request.next + marker.parent = request - for _, child := range children { - dirent := Dirent{ - Path: parent.sub(child), - Type: child.ModeType(), - Inode: child.Inode(), - } + root.flatten(request, request, request.depth+1).next = marker - if w.ignoreSymlinks && dirent.IsSymlink() { - continue + for r := request.next; r != marker; { + next := r.next + + if r.IsDir() { + r.ready.Lock() + + select { + case <-ctx.Done(): + return + case requestCh <- r: + } } - dirents = append(dirents, dirent) + r = next } - sort.Slice(dirents, func(i, j int) bool { - return dirents[i].Path.string() < dirents[j].Path.string() - }) - - return dirents + request.ready.Unlock() } -type flowController struct { - controller chan chan<- Dirent +type scanner struct { + buffer, read []byte + fh int + syscall.Dirent + err error } -func newController() *flowController { - return controllerPool.Get().(*flowController) //nolint:forcetypeassert -} +func (s *scanner) Next() bool { + for len(s.read) == 0 { + n, err := syscall.ReadDirent(s.fh, s.buffer) + if err != nil { + if errors.Is(err, syscall.EINTR) { + continue + } -func (f *flowController) GetControl() chan<- Dirent { - return <-f.controller -} + s.err = err + + return false + } -func (f *flowController) PassControl(control chan<- Dirent) { - f.controller <- control - <-f.controller + if n <= 0 { + return false + } + + s.read = s.buffer[:n] + } + + copy((*[unsafe.Sizeof(syscall.Dirent{})]byte)(unsafe.Pointer(&s.Dirent))[:], s.read) + s.read = s.read[s.Reclen:] + + return true } -func (f *flowController) EndControl() { - f.controller <- nil - controllerPool.Put(f) +func (s *scanner) Get() ([]byte, fs.FileMode, uint64) { + mode := s.getMode() + + return s.getName(mode.IsDir()), mode, s.Ino } -var controllerPool = sync.Pool{ //nolint:gochecknoglobals - New: func() any { - return &flowController{ - controller: make(chan chan<- Dirent), - } - }, +var ( + dot = []byte{'.', '\x00'} //nolint:gochecknoglobals + dotdot = []byte{'.', '.', '\x00'} //nolint:gochecknoglobals +) + +func (s *scanner) getName(isDir bool) []byte { + n := s.Dirent.Name[:] + name := *(*[]byte)(unsafe.Pointer(&n)) + + l := bytes.IndexByte(name, 0) + if l <= 0 || bytes.Equal(name[:2], dot) || bytes.Equal(name[:3], dotdot) { + return nil + } + + if isDir { + s.Dirent.Name[l] = '/' + l++ + } + + return name[:l] } -func walkDirectory(ctx context.Context, dirent Dirent, - flowControl *flowController, request func(*FilePath) []Dirent, sendDirs bool) { - children := request(dirent.Path) - childControllers := make([]*flowController, len(children)) +func (s *scanner) getMode() fs.FileMode { + switch s.Type { + case syscall.DT_DIR: + return fs.ModeDir + case syscall.DT_LNK: + return fs.ModeSymlink + case syscall.DT_CHR: + return fs.ModeDevice | fs.ModeCharDevice + case syscall.DT_BLK: + return fs.ModeDevice + case syscall.DT_FIFO: + return fs.ModeNamedPipe + case syscall.DT_SOCK: + return fs.ModeSocket + } - for n, child := range children { - if child.IsDir() { - childControllers[n] = newController() + return 0 +} - go walkDirectory(ctx, child, childControllers[n], request, sendDirs) - } +func scan(buffer []byte, path *byte, ignoreSymlinks bool) (*Dirent, error) { + root := nullDirEnt + + fh, err := open(path) + if err != nil { + return root, err } - control := flowControl.GetControl() + defer syscall.Close(fh) - if sendDirs { - sendEntry(ctx, dirent, control) + s := scanner{ + buffer: buffer, + fh: fh, } - for n, childController := range childControllers { - if childController == nil { - sendEntry(ctx, children[n], control) - } else { - childController.PassControl(control) + for s.Next() { + name, mode, inode := s.Get() + if inode == 0 || len(name) == 0 || ignoreSymlinks && mode&fs.ModeSymlink != 0 { + continue } + + de := getDirent(len(name)) + + de.name = append(de.name, name...) + de.Type = mode + de.Inode = inode + + root = root.insert(de) } - flowControl.EndControl() + return root, s.err } -func sendEntry(ctx context.Context, dirent Dirent, direntCh chan<- Dirent) { - select { - case <-ctx.Done(): - return - case direntCh <- dirent: +func open(path *byte) (int, error) { + const atFDCWD = -0x64 + + dfd := atFDCWD + + ifh, _, err := syscall.Syscall6( + syscall.SYS_OPENAT, + uintptr(dfd), + uintptr(unsafe.Pointer(path)), + uintptr(syscall.O_RDONLY), + uintptr(0), 0, 0) + if err != 0 { + return 0, err } + + return int(ifh), nil } diff --git a/walk/walk_test.go b/walk/walk_test.go index e1645d45..e6f15db3 100644 --- a/walk/walk_test.go +++ b/walk/walk_test.go @@ -152,11 +152,20 @@ func TestWalk(t *testing.T) { w := New(files.WritePaths(), true, false) err = w.Walk("/root", cb) So(err, ShouldBeNil) - So(len(walkErrors), ShouldEqual, 1) + + mu.Lock() + l := len(walkErrors) + mu.Unlock() + + So(l, ShouldEqual, 1) var writeError *WriteError - So(errors.As(walkErrors[0], &writeError), ShouldBeFalse) + mu.Lock() + err = walkErrors[0] + mu.Unlock() + + So(errors.As(err, &writeError), ShouldBeFalse) outPath := filepath.Join(outDir, "walk.1") _, err = os.ReadFile(outPath)