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 }