// SPDX-License-Identifier: BSD-2-Clause // Some code in this file was derived from https://github.com/karrick/godirwalk. //go:build unix package ufs import ( "bytes" iofs "io/fs" "os" "path" "path/filepath" "reflect" "unsafe" "golang.org/x/sys/unix" ) type WalkDiratFunc func(dirfd int, name, relative string, d DirEntry, err error) error func (fs *UnixFS) WalkDirat(dirfd int, name string, fn WalkDiratFunc) error { if dirfd == 0 { // TODO: proper validation, ideally a dedicated function. dirfd = int(fs.dirfd.Load()) } info, err := fs.Lstatat(dirfd, name) if err != nil { err = fn(dirfd, name, name, nil, err) } else { b := newScratchBuffer() err = fs.walkDir(b, dirfd, name, name, iofs.FileInfoToDirEntry(info), fn) } if err == SkipDir || err == SkipAll { return nil } return err } func (fs *UnixFS) walkDir(b []byte, parentfd int, name, relative string, d DirEntry, walkDirFn WalkDiratFunc) error { if err := walkDirFn(parentfd, name, relative, d, nil); err != nil || !d.IsDir() { if err == SkipDir && d.IsDir() { // Successfully skipped directory. err = nil } return err } dirfd, err := fs.openat(parentfd, name, O_DIRECTORY|O_RDONLY, 0) if err != nil { return err } defer unix.Close(dirfd) dirs, err := fs.readDir(dirfd, name, b) if err != nil { // Second call, to report ReadDir error. err = walkDirFn(dirfd, name, relative, d, err) if err != nil { if err == SkipDir && d.IsDir() { err = nil } return err } } for _, d1 := range dirs { if err := fs.walkDir(b, dirfd, d1.Name(), path.Join(relative, d1.Name()), d1, walkDirFn); err != nil { if err == SkipDir { break } return err } } return nil } // ReadDirMap . // TODO: document func ReadDirMap[T any](fs *UnixFS, path string, fn func(DirEntry) (T, error)) ([]T, error) { dirfd, name, closeFd, err := fs.safePath(path) defer closeFd() if err != nil { return nil, err } fd, err := fs.openat(dirfd, name, O_DIRECTORY|O_RDONLY, 0) if err != nil { return nil, err } defer unix.Close(fd) entries, err := fs.readDir(fd, ".", nil) if err != nil { return nil, err } out := make([]T, len(entries)) for i, e := range entries { idx := i e := e v, err := fn(e) if err != nil { return nil, err } out[idx] = v } return out, nil } // nameOffset is a compile time constant const nameOffset = int(unsafe.Offsetof(unix.Dirent{}.Name)) func nameFromDirent(de *unix.Dirent) (name []byte) { // Because this GOOS' syscall.Dirent does not provide a field that specifies // the name length, this function must first calculate the max possible name // length, and then search for the NULL byte. ml := int(de.Reclen) - nameOffset // Convert syscall.Dirent.Name, which is array of int8, to []byte, by // overwriting Cap, Len, and Data slice header fields to the max possible // name length computed above, and finding the terminating NULL byte. // // TODO: is there an alternative to the deprecated SliceHeader? // SliceHeader was mainly deprecated due to it being misused for avoiding // allocations when converting a byte slice to a string, ref; // https://go.dev/issue/53003 sh := (*reflect.SliceHeader)(unsafe.Pointer(&name)) sh.Cap = ml sh.Len = ml sh.Data = uintptr(unsafe.Pointer(&de.Name[0])) if index := bytes.IndexByte(name, 0); index >= 0 { // Found NULL byte; set slice's cap and len accordingly. sh.Cap = index sh.Len = index return } // NOTE: This branch is not expected, but included for defensive // programming, and provides a hard stop on the name based on the structure // field array size. sh.Cap = len(de.Name) sh.Len = sh.Cap return } // modeTypeFromDirent converts a syscall defined constant, which is in purview // of OS, to a constant defined by Go, assumed by this project to be stable. // // When the syscall constant is not recognized, this function falls back to a // Stat on the file system. func (fs *UnixFS) modeTypeFromDirent(fd int, de *unix.Dirent, osDirname, osBasename string) (FileMode, error) { switch de.Type { case unix.DT_REG: return 0, nil case unix.DT_DIR: return ModeDir, nil case unix.DT_LNK: return ModeSymlink, nil case unix.DT_CHR: return ModeDevice | ModeCharDevice, nil case unix.DT_BLK: return ModeDevice, nil case unix.DT_FIFO: return ModeNamedPipe, nil case unix.DT_SOCK: return ModeSocket, nil default: // If syscall returned unknown type (e.g., DT_UNKNOWN, DT_WHT), then // resolve actual mode by reading file information. return fs.modeType(fd, filepath.Join(osDirname, osBasename)) } } // modeType returns the mode type of the file system entry identified by // osPathname by calling os.LStat function, to intentionally not follow symbolic // links. // // Even though os.LStat provides all file mode bits, we want to ensure same // values returned to caller regardless of whether we obtained file mode bits // from syscall or stat call. Therefore, mask out the additional file mode bits // that are provided by stat but not by the syscall, so users can rely on their // values. func (fs *UnixFS) modeType(dirfd int, name string) (os.FileMode, error) { fi, err := fs.Lstatat(dirfd, name) if err == nil { return fi.Mode() & ModeType, nil } return 0, err } var minimumScratchBufferSize = os.Getpagesize() func newScratchBuffer() []byte { return make([]byte, minimumScratchBufferSize) } func (fs *UnixFS) readDir(fd int, name string, b []byte) ([]DirEntry, error) { scratchBuffer := b if scratchBuffer == nil || len(scratchBuffer) < minimumScratchBufferSize { scratchBuffer = newScratchBuffer() } var entries []DirEntry var workBuffer []byte var sde unix.Dirent for { if len(workBuffer) == 0 { n, err := unix.Getdents(fd, scratchBuffer) if err != nil { if err == unix.EINTR { continue } return nil, convertErrorType(err) } if n <= 0 { // end of directory: normal exit return entries, nil } workBuffer = scratchBuffer[:n] // trim work buffer to number of bytes read } // "Go is like C, except that you just put `unsafe` all over the place". copy((*[unsafe.Sizeof(unix.Dirent{})]byte)(unsafe.Pointer(&sde))[:], workBuffer) workBuffer = workBuffer[sde.Reclen:] // advance buffer for next iteration through loop if sde.Ino == 0 { continue // inode set to 0 indicates an entry that was marked as deleted } nameSlice := nameFromDirent(&sde) nameLength := len(nameSlice) if nameLength == 0 || (nameSlice[0] == '.' && (nameLength == 1 || (nameLength == 2 && nameSlice[1] == '.'))) { continue } childName := string(nameSlice) mt, err := fs.modeTypeFromDirent(fd, &sde, name, childName) if err != nil { return nil, convertErrorType(err) } entries = append(entries, &dirent{name: childName, path: name, modeType: mt, dirfd: fd, fs: fs}) } } // dirent stores the name and file system mode type of discovered file system // entries. type dirent struct { name string path string modeType FileMode dirfd int fs *UnixFS } func (de dirent) Name() string { return de.name } func (de dirent) IsDir() bool { return de.modeType&ModeDir != 0 } func (de dirent) Type() FileMode { return de.modeType } func (de dirent) Info() (FileInfo, error) { if de.fs == nil { return nil, nil } return de.fs.Lstatat(de.dirfd, de.name) } func (de dirent) Open() (File, error) { if de.fs == nil { return nil, nil } return de.fs.OpenFileat(de.dirfd, de.name, O_RDONLY, 0) } // reset releases memory held by entry err and name, and resets mode type to 0. func (de *dirent) reset() { de.name = "" de.path = "" de.modeType = 0 }