ignore.go 15 KB

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