ignore.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. // Copyright (C) 2014 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. package ignore
  7. import (
  8. "bufio"
  9. "bytes"
  10. "errors"
  11. "fmt"
  12. "io"
  13. "path/filepath"
  14. "strings"
  15. "time"
  16. "github.com/gobwas/glob"
  17. "github.com/syncthing/syncthing/lib/fs"
  18. "github.com/syncthing/syncthing/lib/ignore/ignoreresult"
  19. "github.com/syncthing/syncthing/lib/osutil"
  20. "github.com/syncthing/syncthing/lib/sha256"
  21. "github.com/syncthing/syncthing/lib/sync"
  22. )
  23. // A ParseError signifies an error with contents of an ignore file,
  24. // including I/O errors on included files. An I/O error on the root level
  25. // ignore file is not a ParseError.
  26. type ParseError struct {
  27. inner error
  28. }
  29. func (e *ParseError) Error() string {
  30. return fmt.Sprintf("parse error: %v", e.inner)
  31. }
  32. func (e *ParseError) Unwrap() error {
  33. return e.inner
  34. }
  35. func IsParseError(err error) bool {
  36. var e *ParseError
  37. return errors.As(err, &e)
  38. }
  39. func parseError(err error) error {
  40. if err == nil {
  41. return nil
  42. }
  43. return &ParseError{err}
  44. }
  45. type Pattern struct {
  46. pattern string
  47. match glob.Glob
  48. result ignoreresult.R
  49. }
  50. func (p Pattern) String() string {
  51. ret := p.pattern
  52. if !p.result.IsIgnored() {
  53. ret = "!" + ret
  54. }
  55. if p.result.IsCaseFolded() {
  56. ret = "(?i)" + ret
  57. }
  58. if p.result.IsDeletable() {
  59. ret = "(?d)" + ret
  60. }
  61. return ret
  62. }
  63. func (p Pattern) allowsSkippingIgnoredDirs() bool {
  64. if p.result.IsIgnored() {
  65. return true
  66. }
  67. if p.pattern[0] != '/' {
  68. return false
  69. }
  70. // A "/**" at the end is allowed and doesn't have any bearing on the
  71. // below checks; remove it before checking.
  72. pattern := strings.TrimSuffix(p.pattern, "/**")
  73. if len(pattern) == 0 {
  74. return true
  75. }
  76. if strings.Contains(pattern[1:], "/") {
  77. return false
  78. }
  79. // Double asterisk everywhere in the path except at the end is bad
  80. return !strings.Contains(strings.TrimSuffix(pattern, "**"), "**")
  81. }
  82. // The ChangeDetector is responsible for determining if files have changed
  83. // on disk. It gets told to Remember() files (name and modtime) and will
  84. // then get asked if a file has been Seen() (i.e., Remember() has been
  85. // called on it) and if any of the files have Changed(). To forget all
  86. // files, call Reset().
  87. type ChangeDetector interface {
  88. Remember(fs fs.Filesystem, name string, modtime time.Time)
  89. Seen(fs fs.Filesystem, name string) bool
  90. Changed() bool
  91. Reset()
  92. }
  93. type Matcher struct {
  94. fs fs.Filesystem
  95. lines []string // exact lines read from .stignore
  96. patterns []Pattern // patterns including those from included files
  97. withCache bool
  98. matches *cache
  99. curHash string
  100. stop chan struct{}
  101. changeDetector ChangeDetector
  102. mut sync.Mutex
  103. }
  104. // An Option can be passed to New()
  105. type Option func(*Matcher)
  106. // WithCache enables or disables lookup caching. The default is disabled.
  107. func WithCache(v bool) Option {
  108. return func(m *Matcher) {
  109. m.withCache = v
  110. }
  111. }
  112. // WithChangeDetector sets a custom ChangeDetector. The default is to simply
  113. // use the on disk modtime for comparison.
  114. func WithChangeDetector(cd ChangeDetector) Option {
  115. return func(m *Matcher) {
  116. m.changeDetector = cd
  117. }
  118. }
  119. func New(fs fs.Filesystem, opts ...Option) *Matcher {
  120. m := &Matcher{
  121. fs: fs,
  122. stop: make(chan struct{}),
  123. mut: sync.NewMutex(),
  124. }
  125. for _, opt := range opts {
  126. opt(m)
  127. }
  128. if m.changeDetector == nil {
  129. m.changeDetector = newModtimeChecker()
  130. }
  131. if m.withCache {
  132. go m.clean(2 * time.Hour)
  133. }
  134. return m
  135. }
  136. // Load and parse a file. The returned error may be of type *ParseError in
  137. // which case a file was loaded from disk but the patterns could not be
  138. // parsed. In this case the contents of the file are nonetheless available
  139. // in the Lines() method.
  140. func (m *Matcher) Load(file string) error {
  141. m.mut.Lock()
  142. defer m.mut.Unlock()
  143. if m.changeDetector.Seen(m.fs, file) && !m.changeDetector.Changed() {
  144. return nil
  145. }
  146. fd, info, err := loadIgnoreFile(m.fs, file)
  147. if err != nil {
  148. m.parseLocked(&bytes.Buffer{}, file)
  149. return err
  150. }
  151. defer fd.Close()
  152. m.changeDetector.Reset()
  153. err = m.parseLocked(fd, file)
  154. // If we failed to parse, don't cache, as next time Load is called
  155. // we'll pretend it's all good.
  156. if err == nil {
  157. m.changeDetector.Remember(m.fs, file, info.ModTime())
  158. }
  159. return err
  160. }
  161. // Load and parse an io.Reader. See Load() for notes on the returned error.
  162. func (m *Matcher) Parse(r io.Reader, file string) error {
  163. m.mut.Lock()
  164. defer m.mut.Unlock()
  165. return m.parseLocked(r, file)
  166. }
  167. func (m *Matcher) parseLocked(r io.Reader, file string) error {
  168. lines, patterns, err := parseIgnoreFile(m.fs, r, file, m.changeDetector, make(map[string]struct{}))
  169. // Error is saved and returned at the end. We process the patterns
  170. // (possibly blank) anyway.
  171. m.lines = lines
  172. newHash := hashPatterns(patterns)
  173. if newHash == m.curHash {
  174. // We've already loaded exactly these patterns.
  175. return err
  176. }
  177. m.curHash = newHash
  178. m.patterns = patterns
  179. if m.withCache {
  180. m.matches = newCache(patterns)
  181. }
  182. return err
  183. }
  184. // Match matches the patterns plus temporary and internal files.
  185. func (m *Matcher) Match(file string) (result ignoreresult.R) {
  186. switch {
  187. case fs.IsTemporary(file):
  188. return ignoreresult.IgnoreAndSkip
  189. case fs.IsInternal(file):
  190. return ignoreresult.IgnoreAndSkip
  191. case file == ".":
  192. return ignoreresult.NotIgnored
  193. }
  194. m.mut.Lock()
  195. defer m.mut.Unlock()
  196. if len(m.patterns) == 0 {
  197. return ignoreresult.NotIgnored
  198. }
  199. if m.matches != nil {
  200. // Check the cache for a known result.
  201. res, ok := m.matches.get(file)
  202. if ok {
  203. return res
  204. }
  205. // Update the cache with the result at return time
  206. defer func() {
  207. m.matches.set(file, result)
  208. }()
  209. }
  210. // Check all the patterns for a match. Track whether the patterns so far
  211. // allow skipping matched directories or not. As soon as we hit an
  212. // exclude pattern (with some exceptions), we can't skip directories
  213. // anymore.
  214. file = filepath.ToSlash(file)
  215. var lowercaseFile string
  216. canSkipDir := true
  217. for _, pattern := range m.patterns {
  218. if canSkipDir && !pattern.allowsSkippingIgnoredDirs() {
  219. canSkipDir = false
  220. }
  221. res := pattern.result
  222. if canSkipDir {
  223. res = res.WithSkipDir()
  224. }
  225. if pattern.result.IsCaseFolded() {
  226. if lowercaseFile == "" {
  227. lowercaseFile = strings.ToLower(file)
  228. }
  229. if pattern.match.Match(lowercaseFile) {
  230. return res
  231. }
  232. } else if pattern.match.Match(file) {
  233. return res
  234. }
  235. }
  236. // Default to not matching.
  237. return ignoreresult.NotIgnored
  238. }
  239. // Lines return a list of the unprocessed lines in .stignore at last load
  240. func (m *Matcher) Lines() []string {
  241. m.mut.Lock()
  242. defer m.mut.Unlock()
  243. return m.lines
  244. }
  245. // Patterns return a list of the loaded patterns, as they've been parsed
  246. func (m *Matcher) Patterns() []string {
  247. m.mut.Lock()
  248. defer m.mut.Unlock()
  249. patterns := make([]string, len(m.patterns))
  250. for i, pat := range m.patterns {
  251. patterns[i] = pat.String()
  252. }
  253. return patterns
  254. }
  255. func (m *Matcher) String() string {
  256. return fmt.Sprintf("Matcher/%v@%p", m.Patterns(), m)
  257. }
  258. func (m *Matcher) Hash() string {
  259. m.mut.Lock()
  260. defer m.mut.Unlock()
  261. return m.curHash
  262. }
  263. func (m *Matcher) Stop() {
  264. close(m.stop)
  265. }
  266. func (m *Matcher) clean(d time.Duration) {
  267. t := time.NewTimer(d / 2)
  268. for {
  269. select {
  270. case <-m.stop:
  271. return
  272. case <-t.C:
  273. m.mut.Lock()
  274. if m.matches != nil {
  275. m.matches.clean(d)
  276. }
  277. t.Reset(d / 2)
  278. m.mut.Unlock()
  279. }
  280. }
  281. }
  282. func hashPatterns(patterns []Pattern) string {
  283. h := sha256.New()
  284. for _, pat := range patterns {
  285. h.Write([]byte(pat.String()))
  286. h.Write([]byte("\n"))
  287. }
  288. return fmt.Sprintf("%x", h.Sum(nil))
  289. }
  290. func loadIgnoreFile(fs fs.Filesystem, file string) (fs.File, fs.FileInfo, error) {
  291. fd, err := fs.Open(file)
  292. if err != nil {
  293. return fd, nil, err
  294. }
  295. info, err := fd.Stat()
  296. if err != nil {
  297. fd.Close()
  298. }
  299. return fd, info, err
  300. }
  301. func loadParseIncludeFile(filesystem fs.Filesystem, file string, cd ChangeDetector, linesSeen map[string]struct{}) ([]Pattern, error) {
  302. // Allow escaping the folders filesystem.
  303. // TODO: Deprecate, somehow?
  304. if filesystem.Type() == fs.FilesystemTypeBasic {
  305. uri := filesystem.URI()
  306. joined := filepath.Join(uri, file)
  307. if !fs.IsParent(joined, uri) {
  308. filesystem = fs.NewFilesystem(filesystem.Type(), filepath.Dir(joined))
  309. file = filepath.Base(joined)
  310. }
  311. }
  312. if cd.Seen(filesystem, file) {
  313. return nil, errors.New("multiple include")
  314. }
  315. fd, info, err := loadIgnoreFile(filesystem, file)
  316. if err != nil {
  317. // isNotExist is considered "ok" in a sense of that a folder doesn't have to act
  318. // upon it. This is because it is allowed for .stignore to not exist. However,
  319. // included ignore files are not allowed to be missing and these errors should be
  320. // acted upon on. So we don't preserve the error chain here and manually set an
  321. // error instead, if the file is missing.
  322. if fs.IsNotExist(err) {
  323. err = errors.New("file not found")
  324. }
  325. return nil, err
  326. }
  327. defer fd.Close()
  328. cd.Remember(filesystem, file, info.ModTime())
  329. _, patterns, err := parseIgnoreFile(filesystem, fd, file, cd, linesSeen)
  330. return patterns, err
  331. }
  332. func parseLine(line string) ([]Pattern, error) {
  333. pattern := Pattern{
  334. result: ignoreresult.Ignored,
  335. }
  336. // Allow prefixes to be specified in any order, but only once.
  337. var seenPrefix [3]bool
  338. for {
  339. if strings.HasPrefix(line, "!") && !seenPrefix[0] {
  340. seenPrefix[0] = true
  341. line = line[1:]
  342. pattern.result = pattern.result.ToggleIgnored()
  343. } else if strings.HasPrefix(line, "(?i)") && !seenPrefix[1] {
  344. seenPrefix[1] = true
  345. pattern.result = pattern.result.WithFoldCase()
  346. line = line[4:]
  347. } else if strings.HasPrefix(line, "(?d)") && !seenPrefix[2] {
  348. seenPrefix[2] = true
  349. pattern.result = pattern.result.WithDeletable()
  350. line = line[4:]
  351. } else {
  352. break
  353. }
  354. }
  355. if line == "" {
  356. return nil, parseError(errors.New("missing pattern"))
  357. }
  358. if pattern.result.IsCaseFolded() {
  359. line = strings.ToLower(line)
  360. }
  361. pattern.pattern = line
  362. var err error
  363. if strings.HasPrefix(line, "/") {
  364. // Pattern is rooted in the current dir only
  365. pattern.match, err = glob.Compile(line[1:], '/')
  366. return []Pattern{pattern}, parseError(err)
  367. }
  368. patterns := make([]Pattern, 2)
  369. if strings.HasPrefix(line, "**/") {
  370. // Add the pattern as is, and without **/ so it matches in current dir
  371. pattern.match, err = glob.Compile(line, '/')
  372. if err != nil {
  373. return nil, parseError(err)
  374. }
  375. patterns[0] = pattern
  376. line = line[3:]
  377. pattern.pattern = line
  378. pattern.match, err = glob.Compile(line, '/')
  379. if err != nil {
  380. return nil, parseError(err)
  381. }
  382. patterns[1] = pattern
  383. return patterns, nil
  384. }
  385. // Path name or pattern, add it so it matches files both in
  386. // current directory and subdirs.
  387. pattern.match, err = glob.Compile(line, '/')
  388. if err != nil {
  389. return nil, parseError(err)
  390. }
  391. patterns[0] = pattern
  392. line = "**/" + line
  393. pattern.pattern = line
  394. pattern.match, err = glob.Compile(line, '/')
  395. if err != nil {
  396. return nil, parseError(err)
  397. }
  398. patterns[1] = pattern
  399. return patterns, nil
  400. }
  401. func parseIgnoreFile(fs fs.Filesystem, fd io.Reader, currentFile string, cd ChangeDetector, linesSeen map[string]struct{}) ([]string, []Pattern, error) {
  402. var patterns []Pattern
  403. addPattern := func(line string) error {
  404. newPatterns, err := parseLine(line)
  405. if err != nil {
  406. return fmt.Errorf("invalid pattern %q in ignore file: %w", line, err)
  407. }
  408. patterns = append(patterns, newPatterns...)
  409. return nil
  410. }
  411. scanner := bufio.NewScanner(fd)
  412. var lines []string
  413. for scanner.Scan() {
  414. line := strings.TrimSpace(scanner.Text())
  415. lines = append(lines, line)
  416. }
  417. if err := scanner.Err(); err != nil {
  418. return nil, nil, err
  419. }
  420. var err error
  421. for _, line := range lines {
  422. if _, ok := linesSeen[line]; ok {
  423. continue
  424. }
  425. linesSeen[line] = struct{}{}
  426. switch {
  427. case line == "":
  428. continue
  429. case strings.HasPrefix(line, "//"):
  430. continue
  431. }
  432. line = filepath.ToSlash(line)
  433. switch {
  434. case strings.HasPrefix(line, "#include"):
  435. fields := strings.SplitN(line, " ", 2)
  436. if len(fields) != 2 {
  437. err = parseError(errors.New("failed to parse #include line: no file?"))
  438. break
  439. }
  440. includeRel := strings.TrimSpace(fields[1])
  441. if includeRel == "" {
  442. err = parseError(errors.New("failed to parse #include line: no file?"))
  443. break
  444. }
  445. includeFile := filepath.Join(filepath.Dir(currentFile), includeRel)
  446. var includePatterns []Pattern
  447. if includePatterns, err = loadParseIncludeFile(fs, includeFile, cd, linesSeen); err == nil {
  448. patterns = append(patterns, includePatterns...)
  449. } else {
  450. // Wrap the error, as if the include does not exist, we get a
  451. // IsNotExists(err) == true error, which we use to check
  452. // existence of the .stignore file, and just end up assuming
  453. // there is none, rather than a broken include.
  454. err = parseError(fmt.Errorf("failed to load include file %s: %w", includeFile, err))
  455. }
  456. case strings.HasSuffix(line, "/**"):
  457. err = addPattern(line)
  458. case strings.HasSuffix(line, "/"):
  459. err = addPattern(line + "**")
  460. default:
  461. err = addPattern(line)
  462. if err == nil {
  463. err = addPattern(line + "/**")
  464. }
  465. }
  466. if err != nil {
  467. return lines, nil, err
  468. }
  469. }
  470. return lines, patterns, nil
  471. }
  472. // WriteIgnores is a convenience function to avoid code duplication
  473. func WriteIgnores(filesystem fs.Filesystem, path string, content []string) error {
  474. if len(content) == 0 {
  475. err := filesystem.Remove(path)
  476. if fs.IsNotExist(err) {
  477. return nil
  478. }
  479. return err
  480. }
  481. fd, err := osutil.CreateAtomicFilesystem(filesystem, path)
  482. if err != nil {
  483. return err
  484. }
  485. wr := osutil.LineEndingsWriter(fd)
  486. for _, line := range content {
  487. fmt.Fprintln(wr, line)
  488. }
  489. if err := fd.Close(); err != nil {
  490. return err
  491. }
  492. filesystem.Hide(path)
  493. return nil
  494. }
  495. type modtimeCheckerKey struct {
  496. fs fs.Filesystem
  497. name string
  498. }
  499. // modtimeChecker is the default implementation of ChangeDetector
  500. type modtimeChecker struct {
  501. modtimes map[modtimeCheckerKey]time.Time
  502. }
  503. func newModtimeChecker() *modtimeChecker {
  504. return &modtimeChecker{
  505. modtimes: map[modtimeCheckerKey]time.Time{},
  506. }
  507. }
  508. func (c *modtimeChecker) Remember(fs fs.Filesystem, name string, modtime time.Time) {
  509. c.modtimes[modtimeCheckerKey{fs, name}] = modtime
  510. }
  511. func (c *modtimeChecker) Seen(fs fs.Filesystem, name string) bool {
  512. _, ok := c.modtimes[modtimeCheckerKey{fs, name}]
  513. return ok
  514. }
  515. func (c *modtimeChecker) Reset() {
  516. c.modtimes = map[modtimeCheckerKey]time.Time{}
  517. }
  518. func (c *modtimeChecker) Changed() bool {
  519. for key, modtime := range c.modtimes {
  520. info, err := key.fs.Stat(key.name)
  521. if err != nil {
  522. return true
  523. }
  524. if !info.ModTime().Equal(modtime) {
  525. return true
  526. }
  527. }
  528. return false
  529. }