github.com/jfrazelle/docker@v1.1.2-0.20210712172922-bf78e25fe508/pkg/fileutils/fileutils.go (about)

     1  package fileutils // import "github.com/docker/docker/pkg/fileutils"
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"regexp"
    10  	"strings"
    11  	"text/scanner"
    12  )
    13  
    14  // PatternMatcher allows checking paths against a list of patterns
    15  type PatternMatcher struct {
    16  	patterns   []*Pattern
    17  	exclusions bool
    18  }
    19  
    20  // NewPatternMatcher creates a new matcher object for specific patterns that can
    21  // be used later to match against patterns against paths
    22  func NewPatternMatcher(patterns []string) (*PatternMatcher, error) {
    23  	pm := &PatternMatcher{
    24  		patterns: make([]*Pattern, 0, len(patterns)),
    25  	}
    26  	for _, p := range patterns {
    27  		// Eliminate leading and trailing whitespace.
    28  		p = strings.TrimSpace(p)
    29  		if p == "" {
    30  			continue
    31  		}
    32  		p = filepath.Clean(p)
    33  		newp := &Pattern{}
    34  		if p[0] == '!' {
    35  			if len(p) == 1 {
    36  				return nil, errors.New("illegal exclusion pattern: \"!\"")
    37  			}
    38  			newp.exclusion = true
    39  			p = p[1:]
    40  			pm.exclusions = true
    41  		}
    42  		// Do some syntax checking on the pattern.
    43  		// filepath's Match() has some really weird rules that are inconsistent
    44  		// so instead of trying to dup their logic, just call Match() for its
    45  		// error state and if there is an error in the pattern return it.
    46  		// If this becomes an issue we can remove this since its really only
    47  		// needed in the error (syntax) case - which isn't really critical.
    48  		if _, err := filepath.Match(p, "."); err != nil {
    49  			return nil, err
    50  		}
    51  		newp.cleanedPattern = p
    52  		newp.dirs = strings.Split(p, string(os.PathSeparator))
    53  		pm.patterns = append(pm.patterns, newp)
    54  	}
    55  	return pm, nil
    56  }
    57  
    58  // Matches matches path against all the patterns. Matches is not safe to be
    59  // called concurrently
    60  func (pm *PatternMatcher) Matches(file string) (bool, error) {
    61  	matched := false
    62  	file = filepath.FromSlash(file)
    63  	parentPath := filepath.Dir(file)
    64  	parentPathDirs := strings.Split(parentPath, string(os.PathSeparator))
    65  
    66  	for _, pattern := range pm.patterns {
    67  		negative := false
    68  
    69  		if pattern.exclusion {
    70  			negative = true
    71  		}
    72  
    73  		match, err := pattern.match(file)
    74  		if err != nil {
    75  			return false, err
    76  		}
    77  
    78  		if !match && parentPath != "." {
    79  			// Check to see if the pattern matches one of our parent dirs.
    80  			if len(pattern.dirs) <= len(parentPathDirs) {
    81  				match, _ = pattern.match(strings.Join(parentPathDirs[:len(pattern.dirs)], string(os.PathSeparator)))
    82  			}
    83  		}
    84  
    85  		if match {
    86  			matched = !negative
    87  		}
    88  	}
    89  
    90  	return matched, nil
    91  }
    92  
    93  // Exclusions returns true if any of the patterns define exclusions
    94  func (pm *PatternMatcher) Exclusions() bool {
    95  	return pm.exclusions
    96  }
    97  
    98  // Patterns returns array of active patterns
    99  func (pm *PatternMatcher) Patterns() []*Pattern {
   100  	return pm.patterns
   101  }
   102  
   103  // Pattern defines a single regexp used to filter file paths.
   104  type Pattern struct {
   105  	cleanedPattern string
   106  	dirs           []string
   107  	regexp         *regexp.Regexp
   108  	exclusion      bool
   109  }
   110  
   111  func (p *Pattern) String() string {
   112  	return p.cleanedPattern
   113  }
   114  
   115  // Exclusion returns true if this pattern defines exclusion
   116  func (p *Pattern) Exclusion() bool {
   117  	return p.exclusion
   118  }
   119  
   120  func (p *Pattern) match(path string) (bool, error) {
   121  
   122  	if p.regexp == nil {
   123  		if err := p.compile(); err != nil {
   124  			return false, filepath.ErrBadPattern
   125  		}
   126  	}
   127  
   128  	b := p.regexp.MatchString(path)
   129  
   130  	return b, nil
   131  }
   132  
   133  func (p *Pattern) compile() error {
   134  	regStr := "^"
   135  	pattern := p.cleanedPattern
   136  	// Go through the pattern and convert it to a regexp.
   137  	// We use a scanner so we can support utf-8 chars.
   138  	var scan scanner.Scanner
   139  	scan.Init(strings.NewReader(pattern))
   140  
   141  	sl := string(os.PathSeparator)
   142  	escSL := sl
   143  	if sl == `\` {
   144  		escSL += `\`
   145  	}
   146  
   147  	for scan.Peek() != scanner.EOF {
   148  		ch := scan.Next()
   149  
   150  		if ch == '*' {
   151  			if scan.Peek() == '*' {
   152  				// is some flavor of "**"
   153  				scan.Next()
   154  
   155  				// Treat **/ as ** so eat the "/"
   156  				if string(scan.Peek()) == sl {
   157  					scan.Next()
   158  				}
   159  
   160  				if scan.Peek() == scanner.EOF {
   161  					// is "**EOF" - to align with .gitignore just accept all
   162  					regStr += ".*"
   163  				} else {
   164  					// is "**"
   165  					// Note that this allows for any # of /'s (even 0) because
   166  					// the .* will eat everything, even /'s
   167  					regStr += "(.*" + escSL + ")?"
   168  				}
   169  			} else {
   170  				// is "*" so map it to anything but "/"
   171  				regStr += "[^" + escSL + "]*"
   172  			}
   173  		} else if ch == '?' {
   174  			// "?" is any char except "/"
   175  			regStr += "[^" + escSL + "]"
   176  		} else if ch == '.' || ch == '$' {
   177  			// Escape some regexp special chars that have no meaning
   178  			// in golang's filepath.Match
   179  			regStr += `\` + string(ch)
   180  		} else if ch == '\\' {
   181  			// escape next char. Note that a trailing \ in the pattern
   182  			// will be left alone (but need to escape it)
   183  			if sl == `\` {
   184  				// On windows map "\" to "\\", meaning an escaped backslash,
   185  				// and then just continue because filepath.Match on
   186  				// Windows doesn't allow escaping at all
   187  				regStr += escSL
   188  				continue
   189  			}
   190  			if scan.Peek() != scanner.EOF {
   191  				regStr += `\` + string(scan.Next())
   192  			} else {
   193  				regStr += `\`
   194  			}
   195  		} else {
   196  			regStr += string(ch)
   197  		}
   198  	}
   199  
   200  	regStr += "$"
   201  
   202  	re, err := regexp.Compile(regStr)
   203  	if err != nil {
   204  		return err
   205  	}
   206  
   207  	p.regexp = re
   208  	return nil
   209  }
   210  
   211  // Matches returns true if file matches any of the patterns
   212  // and isn't excluded by any of the subsequent patterns.
   213  func Matches(file string, patterns []string) (bool, error) {
   214  	pm, err := NewPatternMatcher(patterns)
   215  	if err != nil {
   216  		return false, err
   217  	}
   218  	file = filepath.Clean(file)
   219  
   220  	if file == "." {
   221  		// Don't let them exclude everything, kind of silly.
   222  		return false, nil
   223  	}
   224  
   225  	return pm.Matches(file)
   226  }
   227  
   228  // CopyFile copies from src to dst until either EOF is reached
   229  // on src or an error occurs. It verifies src exists and removes
   230  // the dst if it exists.
   231  func CopyFile(src, dst string) (int64, error) {
   232  	cleanSrc := filepath.Clean(src)
   233  	cleanDst := filepath.Clean(dst)
   234  	if cleanSrc == cleanDst {
   235  		return 0, nil
   236  	}
   237  	sf, err := os.Open(cleanSrc)
   238  	if err != nil {
   239  		return 0, err
   240  	}
   241  	defer sf.Close()
   242  	if err := os.Remove(cleanDst); err != nil && !os.IsNotExist(err) {
   243  		return 0, err
   244  	}
   245  	df, err := os.Create(cleanDst)
   246  	if err != nil {
   247  		return 0, err
   248  	}
   249  	defer df.Close()
   250  	return io.Copy(df, sf)
   251  }
   252  
   253  // ReadSymlinkedDirectory returns the target directory of a symlink.
   254  // The target of the symbolic link may not be a file.
   255  func ReadSymlinkedDirectory(path string) (string, error) {
   256  	var realPath string
   257  	var err error
   258  	if realPath, err = filepath.Abs(path); err != nil {
   259  		return "", fmt.Errorf("unable to get absolute path for %s: %s", path, err)
   260  	}
   261  	if realPath, err = filepath.EvalSymlinks(realPath); err != nil {
   262  		return "", fmt.Errorf("failed to canonicalise path for %s: %s", path, err)
   263  	}
   264  	realPathInfo, err := os.Stat(realPath)
   265  	if err != nil {
   266  		return "", fmt.Errorf("failed to stat target '%s' of '%s': %s", realPath, path, err)
   267  	}
   268  	if !realPathInfo.Mode().IsDir() {
   269  		return "", fmt.Errorf("canonical path points to a file '%s'", realPath)
   270  	}
   271  	return realPath, nil
   272  }
   273  
   274  // CreateIfNotExists creates a file or a directory only if it does not already exist.
   275  func CreateIfNotExists(path string, isDir bool) error {
   276  	if _, err := os.Stat(path); err != nil {
   277  		if os.IsNotExist(err) {
   278  			if isDir {
   279  				return os.MkdirAll(path, 0755)
   280  			}
   281  			if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
   282  				return err
   283  			}
   284  			f, err := os.OpenFile(path, os.O_CREATE, 0755)
   285  			if err != nil {
   286  				return err
   287  			}
   288  			f.Close()
   289  		}
   290  	}
   291  	return nil
   292  }