utils.go

  1package doublestar
  2
  3import (
  4	"errors"
  5	"os"
  6	"path"
  7	"path/filepath"
  8	"strings"
  9)
 10
 11// SplitPattern is a utility function. Given a pattern, SplitPattern will
 12// return two strings: the first string is everything up to the last slash
 13// (`/`) that appears _before_ any unescaped "meta" characters (ie, `*?[{`).
 14// The second string is everything after that slash. For example, given the
 15// pattern:
 16//
 17//   ../../path/to/meta*/**
 18//                ^----------- split here
 19//
 20// SplitPattern returns "../../path/to" and "meta*/**". This is useful for
 21// initializing os.DirFS() to call Glob() because Glob() will silently fail if
 22// your pattern includes `/./` or `/../`. For example:
 23//
 24//   base, pattern := SplitPattern("../../path/to/meta*/**")
 25//   fsys := os.DirFS(base)
 26//   matches, err := Glob(fsys, pattern)
 27//
 28// If SplitPattern cannot find somewhere to split the pattern (for example,
 29// `meta*/**`), it will return "." and the unaltered pattern (`meta*/**` in
 30// this example).
 31//
 32// Note that SplitPattern will also unescape any meta characters in the
 33// returned base string, so that it can be passed straight to os.DirFS().
 34//
 35// Of course, it is your responsibility to decide if the returned base path is
 36// "safe" in the context of your application. Perhaps you could use Match() to
 37// validate against a list of approved base directories?
 38//
 39func SplitPattern(p string) (base, pattern string) {
 40	base = "."
 41	pattern = p
 42
 43	splitIdx := -1
 44	for i := 0; i < len(p); i++ {
 45		c := p[i]
 46		if c == '\\' {
 47			i++
 48		} else if c == '/' {
 49			splitIdx = i
 50		} else if c == '*' || c == '?' || c == '[' || c == '{' {
 51			break
 52		}
 53	}
 54
 55	if splitIdx == 0 {
 56		return "/", p[1:]
 57	} else if splitIdx > 0 {
 58		return unescapeMeta(p[:splitIdx]), p[splitIdx+1:]
 59	}
 60
 61	return
 62}
 63
 64// FilepathGlob returns the names of all files matching pattern or nil if there
 65// is no matching file. The syntax of pattern is the same as in Match(). The
 66// pattern may describe hierarchical names such as usr/*/bin/ed.
 67//
 68// FilepathGlob ignores file system errors such as I/O errors reading
 69// directories by default. The only possible returned error is ErrBadPattern,
 70// reporting that the pattern is malformed.
 71//
 72// To enable aborting on I/O errors, the WithFailOnIOErrors option can be
 73// passed.
 74//
 75// Note: FilepathGlob is a convenience function that is meant as a drop-in
 76// replacement for `path/filepath.Glob()` for users who don't need the
 77// complication of io/fs. Basically, it:
 78//   - Runs `filepath.Clean()` and `ToSlash()` on the pattern
 79//   - Runs `SplitPattern()` to get a base path and a pattern to Glob
 80//   - Creates an FS object from the base path and `Glob()s` on the pattern
 81//   - Joins the base path with all of the matches from `Glob()`
 82//
 83// Returned paths will use the system's path separator, just like
 84// `filepath.Glob()`.
 85//
 86// Note: the returned error doublestar.ErrBadPattern is not equal to
 87// filepath.ErrBadPattern.
 88//
 89func FilepathGlob(pattern string, opts ...GlobOption) (matches []string, err error) {
 90	if pattern == "" {
 91		// special case to match filepath.Glob behavior
 92		g := newGlob(opts...)
 93		if g.failOnIOErrors {
 94			// match doublestar.Glob behavior here
 95			return nil, os.ErrInvalid
 96		}
 97		return nil, nil
 98	}
 99
100	pattern = filepath.Clean(pattern)
101	pattern = filepath.ToSlash(pattern)
102	base, f := SplitPattern(pattern)
103	if f == "" || f == "." || f == ".." {
104		// some special cases to match filepath.Glob behavior
105		if !ValidatePathPattern(pattern) {
106			return nil, ErrBadPattern
107		}
108
109		if filepath.Separator != '\\' {
110			pattern = unescapeMeta(pattern)
111		}
112
113		if _, err = os.Lstat(pattern); err != nil {
114			g := newGlob(opts...)
115			if errors.Is(err, os.ErrNotExist) {
116				return nil, g.handlePatternNotExist(true)
117			}
118			return nil, g.forwardErrIfFailOnIOErrors(err)
119		}
120		return []string{filepath.FromSlash(pattern)}, nil
121	}
122
123	fs := os.DirFS(base)
124	if matches, err = Glob(fs, f, opts...); err != nil {
125		return nil, err
126	}
127	for i := range matches {
128		// use path.Join because we used ToSlash above to ensure our paths are made
129		// of forward slashes, no matter what the system uses
130		matches[i] = filepath.FromSlash(path.Join(base, matches[i]))
131	}
132	return
133}
134
135// Finds the next comma, but ignores any commas that appear inside nested `{}`.
136// Assumes that each opening bracket has a corresponding closing bracket.
137func indexNextAlt(s string, allowEscaping bool) int {
138	alts := 1
139	l := len(s)
140	for i := 0; i < l; i++ {
141		if allowEscaping && s[i] == '\\' {
142			// skip next byte
143			i++
144		} else if s[i] == '{' {
145			alts++
146		} else if s[i] == '}' {
147			alts--
148		} else if s[i] == ',' && alts == 1 {
149			return i
150		}
151	}
152	return -1
153}
154
155var metaReplacer = strings.NewReplacer("\\*", "*", "\\?", "?", "\\[", "[", "\\]", "]", "\\{", "{", "\\}", "}")
156
157// Unescapes meta characters (*?[]{})
158func unescapeMeta(pattern string) string {
159	return metaReplacer.Replace(pattern)
160}