WIP: skip reading dirs if we can
diff --git a/doublestar_test.go b/doublestar_test.go index dfce33d..c13fd52 100644 --- a/doublestar_test.go +++ b/doublestar_test.go
@@ -36,7 +36,7 @@ {"/*", "/debug/", false, false, false, nil, false, false, true, false, 0, 0}, {"/*", "//", false, false, false, nil, false, false, true, false, 0, 0}, {"abc", "abc", true, true, false, nil, false, false, true, true, 1, 1}, - {"*", "abc", true, true, false, nil, false, false, true, true, 26, 21}, + {"*", "abc", true, true, false, nil, false, false, true, true, 27, 21}, {"*c", "abc", true, true, false, nil, false, false, true, true, 2, 2}, {"*/", "a/", true, true, false, nil, false, false, true, false, 0, 0}, {"a*", "a", true, true, false, nil, false, false, true, true, 9, 9}, @@ -64,8 +64,8 @@ {"a[!a]b", "a☺b", true, true, false, nil, false, false, false, true, 1, 1}, {"a???b", "a☺b", false, false, false, nil, false, false, true, true, 0, 0}, {"a[^a][^a][^a]b", "a☺b", false, false, false, nil, false, false, true, true, 0, 0}, - {"[a-ζ]*", "α", true, true, false, nil, false, false, true, true, 23, 20}, - {"*[a-ζ]", "A", false, false, false, nil, false, false, true, true, 23, 20}, + {"[a-ζ]*", "α", true, true, false, nil, false, false, true, true, 24, 20}, + {"*[a-ζ]", "A", false, false, false, nil, false, false, true, true, 24, 20}, {"a?b", "a/b", false, false, false, nil, false, false, true, true, 1, 1}, {"a*b", "a/b", false, false, false, nil, false, false, true, true, 1, 1}, {"[\\]a]", "]", true, true, false, nil, false, false, true, !onWindows, 2, 2}, @@ -210,6 +210,8 @@ {"hidden-tests/**", "hidden-tests/hidden-dir/hidden-file", true, true, false, nil, false, false, false, onWindows, 6, 6}, {"hidden-tests/.hidden-dir/*", "hidden-tests/.hidden-dir/.hidden-file", true, true, false, nil, false, false, true, !onWindows, 2, 2}, {"hidden-tests/hidden-dir/*", "hidden-tests/hidden-dir/hidden-file", true, true, false, nil, false, false, true, onWindows, 2, 2}, + // this pattern is technically "standard", but path.Glob will fail to match + {"*/dir/file", "noreaddirpermission/dir/file", true, true, false, nil, true, false, false, !onWindows, 1, 1}, } // True if the file system supports case-sensitive filenames @@ -950,11 +952,19 @@ symlink("/tmp/nonexistant-file-20160902155705", "testdata/broken-symlink") symlink("a/b", "testdata/working-symlink") + // no permisions at all if !exists("testdata", "nopermission") { mkdirp("testdata", "nopermission", "dir") touch("testdata", "nopermission", "file") os.Chmod(path.Join("testdata", "nopermission"), 0) } + + // no permission to read dir + if !exists("testdata", "noreaddirpermission") { + mkdirp("testdata", "noreaddirpermission", "dir") + touch("testdata", "noreaddirpermission", "dir", "file") + os.Chmod(path.Join("testdata", "noreaddirpermission"), 0o111) + } } // initialize numResultsFilesOnly
diff --git a/glob.go b/glob.go index f3ef039..0c4394c 100644 --- a/glob.go +++ b/glob.go
@@ -109,11 +109,26 @@ return g.globDir(fsys, unescapeMeta(dir), pattern, matches, firstSegment, beforeMeta) } + // recurse on dir var dirs []string dirs, err = g.doGlob(fsys, dir, matches, false, beforeMeta) if err != nil { return } + + // if the pattern does not contain any meta, we can skip globbing + if indexMeta(pattern) == -1 { + for _, d := range dirs { + matches, err = g.globDirNoMeta(fsys, d, pattern, matches, firstSegment, false) + if err != nil { + return + } + } + + return + } + + // for each dir from above, glob for _, d := range dirs { matches, err = g.globDir(fsys, d, pattern, matches, firstSegment, false) if err != nil { @@ -193,19 +208,7 @@ m = matches if pattern == "" { - if !canMatchFiles || !g.filesOnly { - // pattern can be an empty string if the original pattern ended in a - // slash, in which case, we should just return dir, but only if it - // actually exists and it's a directory (or a symlink to a directory) - _, isDir, err := g.isPathDir(fsys, dir, beforeMeta) - if err != nil { - return nil, err - } - if isDir { - m = append(m, dir) - } - } - return + return g.globDirEmptyPattern(fsys, dir, m, canMatchFiles, beforeMeta) } if pattern == "**" { @@ -310,6 +313,61 @@ return matches, nil } +// find files/subdirectories in the given `dir` that match `pattern`, +// assuming `pattern` has no meta characters +func (g *glob) globDirNoMeta(fsys fs.FS, dir, pattern string, matches []string, canMatchFiles, beforeMeta bool) (m []string, e error) { + m = matches + + if pattern == "" { + return g.globDirEmptyPattern(fsys, dir, m, canMatchFiles, beforeMeta) + } + + fullPath := path.Join(dir, pattern) + + var info fs.FileInfo + var matched bool + info, matched, e = g.exists(fsys, fullPath, beforeMeta) + if e != nil { + return + } + if matched { + matched = canMatchFiles + if !matched || g.filesOnly { + matched = info.IsDir() + if canMatchFiles { + // if we're here, it's because g.filesOnly + // is set and we don't want directories + matched = !matched + } + } + if matched { + m = append(m, fullPath) + } + } + + return +} + +// handle an empty pattern while globbing a directory +func (g *glob) globDirEmptyPattern(fsys fs.FS, dir string, matches []string, canMatchFiles, beforeMeta bool) (m []string, e error) { + m = matches + + if !canMatchFiles || !g.filesOnly { + // pattern can be an empty string if the original pattern ended in a + // slash, in which case, we should just return dir, but only if it + // actually exists and it's a directory (or a symlink to a directory) + _, isDir, err := g.isPathDir(fsys, dir, beforeMeta) + if err != nil { + return nil, err + } + if isDir { + m = append(m, dir) + } + } + + return +} + // Returns true if the pattern has a doublestar in the middle of the pattern. // In this case, GlobWalk is faster because it can get away with less // allocations. However, Glob has a _very_ slight edge if the pattern ends in
diff --git a/globwalk.go b/globwalk.go index 9516cef..8a55865 100644 --- a/globwalk.go +++ b/globwalk.go
@@ -114,8 +114,13 @@ return g.globDirWalk(fsys, unescapeMeta(dir), pattern, firstSegment, beforeMeta, fn) } + patternHasNoMeta := indexMeta(pattern) == -1 return g.doGlobWalk(fsys, dir, false, beforeMeta, func(p string, d fs.DirEntry) error { - if err := g.globDirWalk(fsys, p, pattern, firstSegment, false, fn); err != nil { + if patternHasNoMeta { + if err := g.globDirNoMetaWalk(fsys, p, pattern, firstSegment, false, fn); err != nil { + return err + } + } else if err := g.globDirWalk(fsys, p, pattern, firstSegment, false, fn); err != nil { return err } return nil @@ -240,22 +245,7 @@ func (g *glob) globDirWalk(fsys fs.FS, dir, pattern string, canMatchFiles, beforeMeta bool, fn GlobWalkFunc) (e error) { if pattern == "" { - if !canMatchFiles || !g.filesOnly { - // pattern can be an empty string if the original pattern ended in a - // slash, in which case, we should just return dir, but only if it - // actually exists and it's a directory (or a symlink to a directory) - info, isDir, err := g.isPathDir(fsys, dir, beforeMeta) - if err != nil { - return err - } - if isDir { - e = fn(dir, dirEntryFromFileInfo(info)) - if e == SkipDir { - e = nil - } - } - } - return + return g.globDirEmptyPatternWalk(fsys, dir, canMatchFiles, beforeMeta, fn) } if pattern == "**" { @@ -393,6 +383,61 @@ return } +func (g *glob) globDirNoMetaWalk(fsys fs.FS, dir, pattern string, canMatchFiles, beforeMeta bool, fn GlobWalkFunc) (e error) { + if pattern == "" { + return g.globDirEmptyPatternWalk(fsys, dir, canMatchFiles, beforeMeta, fn) + } + + fullPath := path.Join(dir, pattern) + + var info fs.FileInfo + var matched bool + info, matched, e = g.exists(fsys, fullPath, beforeMeta) + if e != nil { + return + } + if matched { + matched = canMatchFiles + if !matched || g.filesOnly { + matched = info.IsDir() + if canMatchFiles { + // if we're here, it's because g.filesOnly + // is set and we don't want directories + matched = !matched + } + } + if matched { + if e = fn(fullPath, dirEntryFromFileInfo(info)); e != nil { + if e == SkipDir { + e = nil + } + } + } + } + + return +} + +func (g *glob) globDirEmptyPatternWalk(fsys fs.FS, dir string, canMatchFiles, beforeMeta bool, fn GlobWalkFunc) (e error) { + if !canMatchFiles || !g.filesOnly { + // pattern can be an empty string if the original pattern ended in a + // slash, in which case, we should just return dir, but only if it + // actually exists and it's a directory (or a symlink to a directory) + info, isDir, err := g.isPathDir(fsys, dir, beforeMeta) + if err != nil { + return err + } + if isDir { + e = fn(dir, dirEntryFromFileInfo(info)) + if e == SkipDir { + e = nil + } + } + } + + return +} + type DirEntryFromFileInfo struct { fi fs.FileInfo }