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