fileutils.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. package fileutils // import "github.com/docker/docker/pkg/fileutils"
  2. import (
  3. "errors"
  4. "fmt"
  5. "io"
  6. "os"
  7. "path/filepath"
  8. "regexp"
  9. "strings"
  10. "text/scanner"
  11. "unicode/utf8"
  12. )
  13. // escapeBytes is a bitmap used to check whether a character should be escaped when creating the regex.
  14. var escapeBytes [8]byte
  15. // shouldEscape reports whether a rune should be escaped as part of the regex.
  16. //
  17. // This only includes characters that require escaping in regex but are also NOT valid filepath pattern characters.
  18. // Additionally, '\' is not excluded because there is specific logic to properly handle this, as it's a path separator
  19. // on Windows.
  20. //
  21. // Adapted from regexp::QuoteMeta in go stdlib.
  22. // See https://cs.opensource.google/go/go/+/refs/tags/go1.17.2:src/regexp/regexp.go;l=703-715;drc=refs%2Ftags%2Fgo1.17.2
  23. func shouldEscape(b rune) bool {
  24. return b < utf8.RuneSelf && escapeBytes[b%8]&(1<<(b/8)) != 0
  25. }
  26. func init() {
  27. for _, b := range []byte(`.+()|{}$`) {
  28. escapeBytes[b%8] |= 1 << (b / 8)
  29. }
  30. }
  31. // PatternMatcher allows checking paths against a list of patterns
  32. type PatternMatcher struct {
  33. patterns []*Pattern
  34. exclusions bool
  35. }
  36. // NewPatternMatcher creates a new matcher object for specific patterns that can
  37. // be used later to match against patterns against paths
  38. func NewPatternMatcher(patterns []string) (*PatternMatcher, error) {
  39. pm := &PatternMatcher{
  40. patterns: make([]*Pattern, 0, len(patterns)),
  41. }
  42. for _, p := range patterns {
  43. // Eliminate leading and trailing whitespace.
  44. p = strings.TrimSpace(p)
  45. if p == "" {
  46. continue
  47. }
  48. p = filepath.Clean(p)
  49. newp := &Pattern{}
  50. if p[0] == '!' {
  51. if len(p) == 1 {
  52. return nil, errors.New("illegal exclusion pattern: \"!\"")
  53. }
  54. newp.exclusion = true
  55. p = p[1:]
  56. pm.exclusions = true
  57. }
  58. // Do some syntax checking on the pattern.
  59. // filepath's Match() has some really weird rules that are inconsistent
  60. // so instead of trying to dup their logic, just call Match() for its
  61. // error state and if there is an error in the pattern return it.
  62. // If this becomes an issue we can remove this since its really only
  63. // needed in the error (syntax) case - which isn't really critical.
  64. if _, err := filepath.Match(p, "."); err != nil {
  65. return nil, err
  66. }
  67. newp.cleanedPattern = p
  68. newp.dirs = strings.Split(p, string(os.PathSeparator))
  69. pm.patterns = append(pm.patterns, newp)
  70. }
  71. return pm, nil
  72. }
  73. // Matches returns true if "file" matches any of the patterns
  74. // and isn't excluded by any of the subsequent patterns.
  75. //
  76. // The "file" argument should be a slash-delimited path.
  77. //
  78. // Matches is not safe to call concurrently.
  79. //
  80. // Deprecated: This implementation is buggy (it only checks a single parent dir
  81. // against the pattern) and will be removed soon. Use either
  82. // MatchesOrParentMatches or MatchesUsingParentResults instead.
  83. func (pm *PatternMatcher) Matches(file string) (bool, error) {
  84. matched := false
  85. file = filepath.FromSlash(file)
  86. parentPath := filepath.Dir(file)
  87. parentPathDirs := strings.Split(parentPath, string(os.PathSeparator))
  88. for _, pattern := range pm.patterns {
  89. // Skip evaluation if this is an inclusion and the filename
  90. // already matched the pattern, or it's an exclusion and it has
  91. // not matched the pattern yet.
  92. if pattern.exclusion != matched {
  93. continue
  94. }
  95. match, err := pattern.match(file)
  96. if err != nil {
  97. return false, err
  98. }
  99. if !match && parentPath != "." {
  100. // Check to see if the pattern matches one of our parent dirs.
  101. if len(pattern.dirs) <= len(parentPathDirs) {
  102. match, _ = pattern.match(strings.Join(parentPathDirs[:len(pattern.dirs)], string(os.PathSeparator)))
  103. }
  104. }
  105. if match {
  106. matched = !pattern.exclusion
  107. }
  108. }
  109. return matched, nil
  110. }
  111. // MatchesOrParentMatches returns true if "file" matches any of the patterns
  112. // and isn't excluded by any of the subsequent patterns.
  113. //
  114. // The "file" argument should be a slash-delimited path.
  115. //
  116. // Matches is not safe to call concurrently.
  117. func (pm *PatternMatcher) MatchesOrParentMatches(file string) (bool, error) {
  118. matched := false
  119. file = filepath.FromSlash(file)
  120. parentPath := filepath.Dir(file)
  121. parentPathDirs := strings.Split(parentPath, string(os.PathSeparator))
  122. for _, pattern := range pm.patterns {
  123. // Skip evaluation if this is an inclusion and the filename
  124. // already matched the pattern, or it's an exclusion and it has
  125. // not matched the pattern yet.
  126. if pattern.exclusion != matched {
  127. continue
  128. }
  129. match, err := pattern.match(file)
  130. if err != nil {
  131. return false, err
  132. }
  133. if !match && parentPath != "." {
  134. // Check to see if the pattern matches one of our parent dirs.
  135. for i := range parentPathDirs {
  136. match, _ = pattern.match(strings.Join(parentPathDirs[:i+1], string(os.PathSeparator)))
  137. if match {
  138. break
  139. }
  140. }
  141. }
  142. if match {
  143. matched = !pattern.exclusion
  144. }
  145. }
  146. return matched, nil
  147. }
  148. // MatchesUsingParentResult returns true if "file" matches any of the patterns
  149. // and isn't excluded by any of the subsequent patterns. The functionality is
  150. // the same as Matches, but as an optimization, the caller keeps track of
  151. // whether the parent directory matched.
  152. //
  153. // The "file" argument should be a slash-delimited path.
  154. //
  155. // MatchesUsingParentResult is not safe to call concurrently.
  156. //
  157. // Deprecated: this function does behave correctly in some cases (see
  158. // https://github.com/docker/buildx/issues/850).
  159. //
  160. // Use MatchesUsingParentResults instead.
  161. func (pm *PatternMatcher) MatchesUsingParentResult(file string, parentMatched bool) (bool, error) {
  162. matched := parentMatched
  163. file = filepath.FromSlash(file)
  164. for _, pattern := range pm.patterns {
  165. // Skip evaluation if this is an inclusion and the filename
  166. // already matched the pattern, or it's an exclusion and it has
  167. // not matched the pattern yet.
  168. if pattern.exclusion != matched {
  169. continue
  170. }
  171. match, err := pattern.match(file)
  172. if err != nil {
  173. return false, err
  174. }
  175. if match {
  176. matched = !pattern.exclusion
  177. }
  178. }
  179. return matched, nil
  180. }
  181. // MatchInfo tracks information about parent dir matches while traversing a
  182. // filesystem.
  183. type MatchInfo struct {
  184. parentMatched []bool
  185. }
  186. // MatchesUsingParentResults returns true if "file" matches any of the patterns
  187. // and isn't excluded by any of the subsequent patterns. The functionality is
  188. // the same as Matches, but as an optimization, the caller passes in
  189. // intermediate results from matching the parent directory.
  190. //
  191. // The "file" argument should be a slash-delimited path.
  192. //
  193. // MatchesUsingParentResults is not safe to call concurrently.
  194. func (pm *PatternMatcher) MatchesUsingParentResults(file string, parentMatchInfo MatchInfo) (bool, MatchInfo, error) {
  195. parentMatched := parentMatchInfo.parentMatched
  196. if len(parentMatched) != 0 && len(parentMatched) != len(pm.patterns) {
  197. return false, MatchInfo{}, errors.New("wrong number of values in parentMatched")
  198. }
  199. file = filepath.FromSlash(file)
  200. matched := false
  201. matchInfo := MatchInfo{
  202. parentMatched: make([]bool, len(pm.patterns)),
  203. }
  204. for i, pattern := range pm.patterns {
  205. match := false
  206. // If the parent matched this pattern, we don't need to recheck.
  207. if len(parentMatched) != 0 {
  208. match = parentMatched[i]
  209. }
  210. if !match {
  211. // Skip evaluation if this is an inclusion and the filename
  212. // already matched the pattern, or it's an exclusion and it has
  213. // not matched the pattern yet.
  214. if pattern.exclusion != matched {
  215. continue
  216. }
  217. var err error
  218. match, err = pattern.match(file)
  219. if err != nil {
  220. return false, matchInfo, err
  221. }
  222. // If the zero value of MatchInfo was passed in, we don't have
  223. // any information about the parent dir's match results, and we
  224. // apply the same logic as MatchesOrParentMatches.
  225. if !match && len(parentMatched) == 0 {
  226. if parentPath := filepath.Dir(file); parentPath != "." {
  227. parentPathDirs := strings.Split(parentPath, string(os.PathSeparator))
  228. // Check to see if the pattern matches one of our parent dirs.
  229. for i := range parentPathDirs {
  230. match, _ = pattern.match(strings.Join(parentPathDirs[:i+1], string(os.PathSeparator)))
  231. if match {
  232. break
  233. }
  234. }
  235. }
  236. }
  237. }
  238. matchInfo.parentMatched[i] = match
  239. if match {
  240. matched = !pattern.exclusion
  241. }
  242. }
  243. return matched, matchInfo, nil
  244. }
  245. // Exclusions returns true if any of the patterns define exclusions
  246. func (pm *PatternMatcher) Exclusions() bool {
  247. return pm.exclusions
  248. }
  249. // Patterns returns array of active patterns
  250. func (pm *PatternMatcher) Patterns() []*Pattern {
  251. return pm.patterns
  252. }
  253. // Pattern defines a single regexp used to filter file paths.
  254. type Pattern struct {
  255. matchType matchType
  256. cleanedPattern string
  257. dirs []string
  258. regexp *regexp.Regexp
  259. exclusion bool
  260. }
  261. type matchType int
  262. const (
  263. unknownMatch matchType = iota
  264. exactMatch
  265. prefixMatch
  266. suffixMatch
  267. regexpMatch
  268. )
  269. func (p *Pattern) String() string {
  270. return p.cleanedPattern
  271. }
  272. // Exclusion returns true if this pattern defines exclusion
  273. func (p *Pattern) Exclusion() bool {
  274. return p.exclusion
  275. }
  276. func (p *Pattern) match(path string) (bool, error) {
  277. if p.matchType == unknownMatch {
  278. if err := p.compile(); err != nil {
  279. return false, filepath.ErrBadPattern
  280. }
  281. }
  282. switch p.matchType {
  283. case exactMatch:
  284. return path == p.cleanedPattern, nil
  285. case prefixMatch:
  286. // strip trailing **
  287. return strings.HasPrefix(path, p.cleanedPattern[:len(p.cleanedPattern)-2]), nil
  288. case suffixMatch:
  289. // strip leading **
  290. suffix := p.cleanedPattern[2:]
  291. if strings.HasSuffix(path, suffix) {
  292. return true, nil
  293. }
  294. // **/foo matches "foo"
  295. return suffix[0] == os.PathSeparator && path == suffix[1:], nil
  296. case regexpMatch:
  297. return p.regexp.MatchString(path), nil
  298. }
  299. return false, nil
  300. }
  301. func (p *Pattern) compile() error {
  302. regStr := "^"
  303. pattern := p.cleanedPattern
  304. // Go through the pattern and convert it to a regexp.
  305. // We use a scanner so we can support utf-8 chars.
  306. var scan scanner.Scanner
  307. scan.Init(strings.NewReader(pattern))
  308. sl := string(os.PathSeparator)
  309. escSL := sl
  310. if sl == `\` {
  311. escSL += `\`
  312. }
  313. p.matchType = exactMatch
  314. for i := 0; scan.Peek() != scanner.EOF; i++ {
  315. ch := scan.Next()
  316. if ch == '*' {
  317. if scan.Peek() == '*' {
  318. // is some flavor of "**"
  319. scan.Next()
  320. // Treat **/ as ** so eat the "/"
  321. if string(scan.Peek()) == sl {
  322. scan.Next()
  323. }
  324. if scan.Peek() == scanner.EOF {
  325. // is "**EOF" - to align with .gitignore just accept all
  326. if p.matchType == exactMatch {
  327. p.matchType = prefixMatch
  328. } else {
  329. regStr += ".*"
  330. p.matchType = regexpMatch
  331. }
  332. } else {
  333. // is "**"
  334. // Note that this allows for any # of /'s (even 0) because
  335. // the .* will eat everything, even /'s
  336. regStr += "(.*" + escSL + ")?"
  337. p.matchType = regexpMatch
  338. }
  339. if i == 0 {
  340. p.matchType = suffixMatch
  341. }
  342. } else {
  343. // is "*" so map it to anything but "/"
  344. regStr += "[^" + escSL + "]*"
  345. p.matchType = regexpMatch
  346. }
  347. } else if ch == '?' {
  348. // "?" is any char except "/"
  349. regStr += "[^" + escSL + "]"
  350. p.matchType = regexpMatch
  351. } else if shouldEscape(ch) {
  352. // Escape some regexp special chars that have no meaning
  353. // in golang's filepath.Match
  354. regStr += `\` + string(ch)
  355. } else if ch == '\\' {
  356. // escape next char. Note that a trailing \ in the pattern
  357. // will be left alone (but need to escape it)
  358. if sl == `\` {
  359. // On windows map "\" to "\\", meaning an escaped backslash,
  360. // and then just continue because filepath.Match on
  361. // Windows doesn't allow escaping at all
  362. regStr += escSL
  363. continue
  364. }
  365. if scan.Peek() != scanner.EOF {
  366. regStr += `\` + string(scan.Next())
  367. p.matchType = regexpMatch
  368. } else {
  369. regStr += `\`
  370. }
  371. } else if ch == '[' || ch == ']' {
  372. regStr += string(ch)
  373. p.matchType = regexpMatch
  374. } else {
  375. regStr += string(ch)
  376. }
  377. }
  378. if p.matchType != regexpMatch {
  379. return nil
  380. }
  381. regStr += "$"
  382. re, err := regexp.Compile(regStr)
  383. if err != nil {
  384. return err
  385. }
  386. p.regexp = re
  387. p.matchType = regexpMatch
  388. return nil
  389. }
  390. // Matches returns true if file matches any of the patterns
  391. // and isn't excluded by any of the subsequent patterns.
  392. //
  393. // This implementation is buggy (it only checks a single parent dir against the
  394. // pattern) and will be removed soon. Use MatchesOrParentMatches instead.
  395. func Matches(file string, patterns []string) (bool, error) {
  396. pm, err := NewPatternMatcher(patterns)
  397. if err != nil {
  398. return false, err
  399. }
  400. file = filepath.Clean(file)
  401. if file == "." {
  402. // Don't let them exclude everything, kind of silly.
  403. return false, nil
  404. }
  405. return pm.Matches(file)
  406. }
  407. // MatchesOrParentMatches returns true if file matches any of the patterns
  408. // and isn't excluded by any of the subsequent patterns.
  409. func MatchesOrParentMatches(file string, patterns []string) (bool, error) {
  410. pm, err := NewPatternMatcher(patterns)
  411. if err != nil {
  412. return false, err
  413. }
  414. file = filepath.Clean(file)
  415. if file == "." {
  416. // Don't let them exclude everything, kind of silly.
  417. return false, nil
  418. }
  419. return pm.MatchesOrParentMatches(file)
  420. }
  421. // CopyFile copies from src to dst until either EOF is reached
  422. // on src or an error occurs. It verifies src exists and removes
  423. // the dst if it exists.
  424. func CopyFile(src, dst string) (int64, error) {
  425. cleanSrc := filepath.Clean(src)
  426. cleanDst := filepath.Clean(dst)
  427. if cleanSrc == cleanDst {
  428. return 0, nil
  429. }
  430. sf, err := os.Open(cleanSrc)
  431. if err != nil {
  432. return 0, err
  433. }
  434. defer sf.Close()
  435. if err := os.Remove(cleanDst); err != nil && !os.IsNotExist(err) {
  436. return 0, err
  437. }
  438. df, err := os.Create(cleanDst)
  439. if err != nil {
  440. return 0, err
  441. }
  442. defer df.Close()
  443. return io.Copy(df, sf)
  444. }
  445. // ReadSymlinkedDirectory returns the target directory of a symlink.
  446. // The target of the symbolic link may not be a file.
  447. func ReadSymlinkedDirectory(path string) (string, error) {
  448. var realPath string
  449. var err error
  450. if realPath, err = filepath.Abs(path); err != nil {
  451. return "", fmt.Errorf("unable to get absolute path for %s: %s", path, err)
  452. }
  453. if realPath, err = filepath.EvalSymlinks(realPath); err != nil {
  454. return "", fmt.Errorf("failed to canonicalise path for %s: %s", path, err)
  455. }
  456. realPathInfo, err := os.Stat(realPath)
  457. if err != nil {
  458. return "", fmt.Errorf("failed to stat target '%s' of '%s': %s", realPath, path, err)
  459. }
  460. if !realPathInfo.Mode().IsDir() {
  461. return "", fmt.Errorf("canonical path points to a file '%s'", realPath)
  462. }
  463. return realPath, nil
  464. }
  465. // CreateIfNotExists creates a file or a directory only if it does not already exist.
  466. func CreateIfNotExists(path string, isDir bool) error {
  467. if _, err := os.Stat(path); err != nil {
  468. if os.IsNotExist(err) {
  469. if isDir {
  470. return os.MkdirAll(path, 0755)
  471. }
  472. if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
  473. return err
  474. }
  475. f, err := os.OpenFile(path, os.O_CREATE, 0755)
  476. if err != nil {
  477. return err
  478. }
  479. f.Close()
  480. }
  481. }
  482. return nil
  483. }