Antonio Mika
·
2026-10-10
1package rsync
2
3import (
4 "errors"
5 "fmt"
6 "io/fs"
7 "path"
8 "slices"
9 "strings"
10)
11
12// Filter rule flags (rsync.h FILTRULE_*).
13const (
14 ruleWild = 1 << iota
15 ruleWild2
16 ruleWild2Prefix
17 ruleWild3Suffix
18 ruleAbsPath
19 ruleInclude
20 ruleDirectory
21 ruleWordSplit
22 ruleNoInherit
23 ruleNoPrefixes
24 ruleMergeFile
25 rulePerdirMerge
26 ruleExcludeSelf
27 ruleFinishSetup
28 ruleNegate
29 ruleCVSIgnore
30 ruleSenderSide
31 ruleReceiverSide
32 ruleClearList
33 rulePerishable
34 ruleXattr
35)
36
37// containerFlags are the flags rules in a merge file take from the merge
38// rule (FILTRULES_FROM_CONTAINER).
39const containerFlags = ruleAbsPath | ruleInclude | ruleDirectory | ruleNegate | rulePerishable
40
41const sideFlags = ruleSenderSide | ruleReceiverSide
42
43// maxMergeFileSize bounds a per-directory merge file read from storage.
44const maxMergeFileSize = 1 << 20
45
46type filterRule struct {
47 pattern string
48 flags int
49 slashCnt int
50 // merge indexes filterList.merges for a per-directory merge rule.
51 merge int
52}
53
54// filterList holds the rules the client sent. The client only sends the
55// rules that apply to our side of the transfer. A per-directory merge rule
56// stands for the rules read from that file in each directory, which a
57// filterScope holds.
58type filterList struct {
59 rules []*filterRule
60 merges []*filterRule
61 // otherSide marks merge-file rules that only apply to the client.
62 otherSide int
63 deleteExcluded bool
64 cvsPerishable bool
65}
66
67// filterScope is the per-directory merge-file rules in effect in one
68// directory, each list holding the directory's own rules before the
69// inherited ones.
70type filterScope struct {
71 // root is the storage path of the transfer root, which absolute
72 // patterns are matched from.
73 root string
74 lists [][]*filterRule
75}
76
77// mergeFileError is a per-directory merge file that could not be used.
78// Skipping its rules could delete or send files they cover, so callers
79// stop instead.
80type mergeFileError struct {
81 name string
82 err error
83}
84
85func (e *mergeFileError) Error() string {
86 return fmt.Sprintf("per-directory filter file %q: %v", "/"+e.name, e.err)
87}
88
89func (e *mergeFileError) Unwrap() error { return e.err }
90
91// defaultCVSIgnore is the list rsync's -C adds.
92const defaultCVSIgnore = "RCS SCCS CVS CVS.adm RCSLOG cvslog.* tags TAGS" +
93 " .make.state .nse_depinfo *~ #* .#* ,* _$* *$" +
94 " *.old *.bak *.BAK *.orig *.rej .del-*" +
95 " *.a *.olb *.o *.obj *.so *.exe" +
96 " *.Z *.elc *.ln core" +
97 " .svn/ .git/ .hg/ .bzr/"
98
99var longRuleNames = map[byte]struct {
100 word string
101 ch byte
102}{
103 'c': {"clear", '!'},
104 'd': {"dir-merge", ':'},
105 'e': {"exclude", '-'},
106 'h': {"hide", 'H'},
107 'i': {"include", '+'},
108 'm': {"merge", '.'},
109 'p': {"protect", 'P'},
110 'r': {"risk", 'R'},
111 's': {"show", 'S'},
112}
113
114func isSpace(c byte) bool { return c == ' ' || (c >= '\t' && c <= '\r') }
115
116// parseRuleTok parses one rule (parse_rule_tok). tmpl is the flags of the
117// merge rule whose file the rule was read from, or 0 for the client's
118// rules. It returns a nil rule for an empty one.
119func parseRuleTok(s string, tmpl int, oldPrefixes bool) (*filterRule, error) {
120 if tmpl&ruleWordSplit != 0 {
121 s = strings.TrimLeftFunc(s, func(r rune) bool { return r < 0x80 && isSpace(byte(r)) })
122 }
123 if s == "" {
124 return nil, nil
125 }
126 rule := &filterRule{flags: tmpl & containerFlags}
127 switch {
128 case tmpl&ruleNoPrefixes != 0:
129 if s[0] == '!' && tmpl&ruleCVSIgnore != 0 {
130 rule.flags |= ruleClearList
131 }
132 case oldPrefixes:
133 switch {
134 case strings.HasPrefix(s, "- "):
135 rule.flags &^= ruleInclude
136 s = s[2:]
137 case strings.HasPrefix(s, "+ "):
138 rule.flags |= ruleInclude
139 s = s[2:]
140 case s[0] == '!':
141 rule.flags |= ruleClearList
142 }
143 default:
144 // p ends on the last character of the rule name, as the modifier
145 // loop starts by stepping past it.
146 ch, p := s[0], 0
147 if long, ok := longRuleNames[s[0]]; ok {
148 rest, found := strings.CutPrefix(s, long.word)
149 switch {
150 case !found:
151 return nil, errors.New("unknown filter rule")
152 case rest == "" || rest[0] == '_' || isSpace(rest[0]):
153 ch, p = long.ch, len(long.word)-1
154 case rest[0] == ',':
155 ch, p = long.ch, len(long.word)
156 default:
157 return nil, errors.New("unknown filter rule")
158 }
159 } else if len(s) > 1 && s[1] == ',' {
160 p = 1
161 }
162 sideFromPrefix := false
163 switch ch {
164 case ':':
165 rule.flags |= rulePerdirMerge | ruleFinishSetup | ruleMergeFile
166 case '.':
167 rule.flags |= ruleMergeFile
168 case '+':
169 rule.flags |= ruleInclude
170 case '-':
171 case 'S':
172 rule.flags |= ruleInclude | ruleSenderSide
173 sideFromPrefix = true
174 case 'H':
175 rule.flags |= ruleSenderSide
176 sideFromPrefix = true
177 case 'R':
178 rule.flags |= ruleInclude | ruleReceiverSide
179 sideFromPrefix = true
180 case 'P':
181 rule.flags |= ruleReceiverSide
182 sideFromPrefix = true
183 case '!':
184 rule.flags |= ruleClearList
185 default:
186 return nil, errors.New("unknown filter rule")
187 }
188 merge := rule.flags&ruleMergeFile != 0
189 for ch != '!' {
190 p++
191 if p >= len(s) || s[p] == ' ' || s[p] == '_' {
192 break
193 }
194 if tmpl&ruleWordSplit != 0 && isSpace(s[p]) {
195 p--
196 break
197 }
198 valid := true
199 switch s[p] {
200 case '-':
201 valid = merge && rule.flags&ruleNoPrefixes == 0
202 rule.flags |= ruleNoPrefixes
203 case '+':
204 valid = merge && rule.flags&ruleNoPrefixes == 0
205 rule.flags |= ruleNoPrefixes | ruleInclude
206 case '/':
207 rule.flags |= ruleAbsPath
208 case '!':
209 valid = !merge
210 rule.flags |= ruleNegate
211 case 'C':
212 valid = rule.flags&ruleNoPrefixes == 0 && !sideFromPrefix
213 rule.flags |= ruleNoPrefixes | ruleWordSplit | ruleNoInherit | ruleCVSIgnore
214 case 'e':
215 valid = merge
216 rule.flags |= ruleExcludeSelf
217 case 'n':
218 valid = merge
219 rule.flags |= ruleNoInherit
220 case 'p':
221 rule.flags |= rulePerishable
222 case 'r':
223 valid = !sideFromPrefix
224 rule.flags |= ruleReceiverSide
225 case 's':
226 valid = !sideFromPrefix
227 rule.flags |= ruleSenderSide
228 case 'w':
229 valid = merge
230 rule.flags |= ruleWordSplit
231 case 'x':
232 rule.flags |= ruleXattr
233 default:
234 valid = false
235 }
236 if !valid {
237 return nil, fmt.Errorf("invalid modifier %q in filter rule", s[p])
238 }
239 }
240 if p < len(s) {
241 p++
242 }
243 s = s[min(p, len(s)):]
244 }
245 if tmpl&sideFlags != 0 {
246 if rule.flags&sideFlags != 0 {
247 return nil, errors.New("specified-side merge file contains specified-side filter")
248 }
249 rule.flags |= tmpl & sideFlags
250 }
251
252 if tmpl&ruleWordSplit != 0 {
253 if i := strings.IndexFunc(s, func(r rune) bool { return r < 0x80 && isSpace(byte(r)) }); i >= 0 {
254 s = s[:i]
255 }
256 }
257 if rule.flags&ruleClearList != 0 {
258 if tmpl&ruleNoPrefixes == 0 && !oldPrefixes && s != "" {
259 return nil, errors.New("'!' rule has trailing characters")
260 }
261 if len(s) > 1 {
262 rule.flags &^= ruleClearList
263 }
264 } else if s == "" && rule.flags&ruleCVSIgnore == 0 {
265 return nil, errors.New("unexpected end of filter rule")
266 }
267 if len(s) >= maxPathLen {
268 return nil, errors.New("filter rule too long")
269 }
270 rule.pattern = s
271 return rule, nil
272}
273
274// parse adds the rules in one line received from the client. Before
275// protocol 29 only the "- " and "+ " prefixes exist.
276func (l *filterList) parse(line string, oldPrefixes bool) error {
277 rule, err := parseRuleTok(line, 0, oldPrefixes)
278 switch {
279 case err != nil:
280 return fmt.Errorf("%w: %q", err, line)
281 case rule == nil:
282 return nil
283 case rule.flags&ruleClearList != 0:
284 l.rules, l.merges = nil, nil
285 return nil
286 case rule.flags&ruleMergeFile != 0:
287 if rule.flags&rulePerdirMerge == 0 {
288 // Clients read merge files themselves and send their rules.
289 return fmt.Errorf("merge-file filter rules are not supported: %q", line)
290 }
291 return l.addMerge(rule)
292 case rule.flags&ruleCVSIgnore != 0:
293 l.rules = append(l.rules, l.cvsIgnoreRules()...)
294 return nil
295 }
296 l.add(rule.pattern, rule.flags)
297 return nil
298}
299
300// addMerge adds a per-directory merge rule. Only the file's base name is
301// read in each directory; a directory part names where the scan of parent
302// directories starts (setup_merge_file).
303func (l *filterList) addMerge(rule *filterRule) error {
304 if rule.pattern == "" {
305 rule.pattern = ".cvsignore"
306 }
307 base := path.Base(rule.pattern)
308 if strings.HasSuffix(rule.pattern, "/") || base == "." || base == ".." {
309 return fmt.Errorf("invalid merge-file name: %q", rule.pattern)
310 }
311 if rule.flags&ruleExcludeSelf != 0 {
312 l.add(base, 0)
313 rule.flags &^= ruleExcludeSelf
314 }
315 for _, m := range l.merges {
316 if path.Base(m.pattern) == base {
317 return nil
318 }
319 }
320 rule.merge = len(l.merges)
321 l.rules = append(l.rules, rule)
322 l.merges = append(l.merges, rule)
323 return nil
324}
325
326// cvsIgnoreRules returns rsync's default CVS excludes, which are
327// perishable from protocol 30 (get_cvs_excludes).
328func (l *filterList) cvsIgnoreRules() []*filterRule {
329 flags := ruleNoPrefixes
330 if l.cvsPerishable {
331 flags |= rulePerishable
332 }
333 var rules []*filterRule
334 for _, pat := range strings.Fields(defaultCVSIgnore) {
335 rules = append(rules, newRule(pat, flags))
336 }
337 return rules
338}
339
340func newRule(pat string, flags int) *filterRule {
341 rule := &filterRule{flags: flags}
342 if len(pat) > 1 && strings.HasSuffix(pat, "/") {
343 pat = pat[:len(pat)-1]
344 rule.flags |= ruleDirectory
345 }
346 rule.slashCnt = strings.Count(pat, "/")
347 rule.pattern = pat
348 if strings.ContainsAny(pat, "*[?") {
349 rule.flags |= ruleWild
350 if i := strings.Index(pat, "**"); i >= 0 {
351 rule.flags |= ruleWild2
352 if i == 0 {
353 rule.flags |= ruleWild2Prefix
354 }
355 if strings.HasSuffix(pat, "***") {
356 rule.flags |= ruleWild3Suffix
357 }
358 }
359 }
360 return rule
361}
362
363func (l *filterList) add(pat string, flags int) {
364 l.rules = append(l.rules, newRule(pat, flags))
365}
366
367// scopeTree builds the filter scope of each directory below a traversal
368// root the first time it is needed.
369type scopeTree struct {
370 l *filterList
371 root string
372 dir string
373 read func(string) ([]byte, error)
374 scopes map[string]*filterScope
375}
376
377// newScopeTree starts at dir, the storage path of the first directory
378// traversed. root is the storage path of the transfer root.
379func (l *filterList) newScopeTree(root, dir string, read func(string) ([]byte, error)) *scopeTree {
380 return &scopeTree{l: l, root: root, dir: dir, read: read, scopes: map[string]*filterScope{}}
381}
382
383// scope returns the scope for rel, a directory relative to the tree's
384// first directory, which is ".".
385func (t *scopeTree) scope(rel string) (*filterScope, error) {
386 if sc, ok := t.scopes[rel]; ok {
387 return sc, nil
388 }
389 var sc *filterScope
390 if rel == "." {
391 var err error
392 if sc, err = t.l.newScope(t.root, t.dir, t.read); err != nil {
393 return nil, err
394 }
395 } else {
396 parent, err := t.scope(path.Dir(rel))
397 if err != nil {
398 return nil, err
399 }
400 if sc, err = t.l.enter(parent, path.Join(t.dir, rel), t.read); err != nil {
401 return nil, err
402 }
403 }
404 t.scopes[rel] = sc
405 return sc, nil
406}
407
408// mergeReader reads per-directory merge files from storage, returning nil
409// for a missing one. files lists the regular files below tree, relative to
410// it; other files are looked up in their directory, as not every backend
411// reports a missing file from Open.
412func (s *session) mergeReader(tree string, files map[string]bool) func(string) ([]byte, error) {
413 return func(name string) ([]byte, error) {
414 rel, under := name, tree == ""
415 if !under {
416 rel, under = strings.CutPrefix(name, tree+"/")
417 }
418 found := false
419 if under && files != nil {
420 found = files[rel]
421 } else {
422 dir := path.Dir(name)
423 if dir == "." {
424 dir = ""
425 }
426 entries, err := s.fs.ReadDir(dir, false)
427 if err != nil && !errors.Is(err, fs.ErrNotExist) {
428 return nil, err
429 }
430 base := path.Base(name)
431 found = slices.ContainsFunc(entries, func(e FileInfo) bool {
432 return !e.IsDir && strings.Trim(e.Name, "/") == base
433 })
434 }
435 if !found {
436 return nil, nil
437 }
438 f, info, err := s.fs.Open(name)
439 if err != nil {
440 return nil, err
441 }
442 defer func() { _ = f.Close() }()
443 if info.Size > maxMergeFileSize {
444 return nil, fmt.Errorf("larger than %d bytes", maxMergeFileSize)
445 }
446 buf := make([]byte, info.Size)
447 if err := readFullAt(f, buf, 0); err != nil {
448 return nil, err
449 }
450 return buf, nil
451 }
452}
453
454// newScope returns the scope for dir, the storage path of the first
455// directory traversed. A merge rule whose name has a directory part that
456// is an ancestor of dir first reads the file in that directory and each
457// one below it down to dir's parent (setup_merge_file).
458func (l *filterList) newScope(root, dir string, read func(string) ([]byte, error)) (*filterScope, error) {
459 parent := &filterScope{root: root, lists: make([][]*filterRule, len(l.merges))}
460 for i, m := range l.merges {
461 if !strings.Contains(m.pattern, "/") {
462 continue
463 }
464 from := path.Dir(m.pattern)
465 if !strings.HasPrefix(from, "/") {
466 from = path.Join("/"+dir, from)
467 }
468 from = strings.TrimPrefix(path.Clean(from), "/")
469 if from == dir || (from != "" && !strings.HasPrefix(dir, from+"/")) {
470 continue
471 }
472 rest := strings.Split(strings.TrimPrefix(dir, from), "/")
473 d := from
474 for _, elem := range rest {
475 if elem == "" {
476 continue
477 }
478 rules, cleared, err := l.readMerge(m, d, read)
479 if err != nil {
480 return nil, err
481 }
482 if cleared {
483 parent.lists[i] = nil
484 }
485 if m.flags&ruleNoInherit == 0 {
486 parent.lists[i] = append(rules, parent.lists[i]...)
487 }
488 d = path.Join(d, elem)
489 }
490 }
491 return l.enter(parent, dir, read)
492}
493
494// enter returns the scope for dir, a storage path, below parent's
495// directory (push_local_filters).
496func (l *filterList) enter(parent *filterScope, dir string, read func(string) ([]byte, error)) (*filterScope, error) {
497 if len(l.merges) == 0 {
498 return parent, nil
499 }
500 sc := &filterScope{root: parent.root, lists: make([][]*filterRule, len(l.merges))}
501 for i, m := range l.merges {
502 var inherited []*filterRule
503 if m.flags&ruleNoInherit == 0 {
504 inherited = parent.lists[i]
505 }
506 rules, cleared, err := l.readMerge(m, dir, read)
507 if err != nil {
508 return nil, err
509 }
510 if cleared {
511 inherited = nil
512 }
513 sc.lists[i] = append(rules, inherited...)
514 }
515 return sc, nil
516}
517
518// readMerge parses the merge file for m in dir (parse_filter_file). A "!"
519// clears the rules before it, inherited ones included.
520func (l *filterList) readMerge(m *filterRule, dir string, read func(string) ([]byte, error)) (rules []*filterRule, cleared bool, err error) {
521 name := path.Join(dir, path.Base(m.pattern))
522 data, err := read(name)
523 if err != nil {
524 return nil, false, &mergeFileError{name: name, err: err}
525 }
526 var toks []string
527 if m.flags&ruleWordSplit != 0 {
528 toks = strings.FieldsFunc(string(data), func(r rune) bool { return r < 0x80 && isSpace(byte(r)) })
529 } else {
530 text := strings.ReplaceAll(string(data), "\r\n", "\n")
531 for line := range strings.FieldsFuncSeq(text, func(r rune) bool { return r == '\n' || r == '\r' }) {
532 if line[0] != '#' && line[0] != ';' {
533 toks = append(toks, line)
534 }
535 }
536 }
537 for _, tok := range toks {
538 rule, err := parseRuleTok(tok, m.flags, false)
539 if err != nil {
540 return nil, false, &mergeFileError{name: name, err: err}
541 }
542 if rule == nil {
543 continue
544 }
545 switch {
546 case rule.flags&ruleClearList != 0:
547 rules, cleared = nil, true
548 continue
549 case rule.flags&ruleMergeFile != 0:
550 return nil, false, &mergeFileError{name: name, err: errors.New("merge rules inside a merge file are not supported")}
551 case rule.flags&ruleCVSIgnore != 0:
552 rules = append(rules, l.cvsIgnoreRules()...)
553 continue
554 }
555 if l.deleteExcluded && rule.flags&sideFlags == 0 {
556 rule.flags |= ruleSenderSide
557 }
558 if rule.flags&sideFlags == l.otherSide {
559 continue
560 }
561 pat := rule.pattern
562 if rule.flags&ruleAbsPath == 0 && strings.HasPrefix(pat, "/") {
563 // Anchored patterns are relative to the file's directory.
564 rule.flags |= ruleAbsPath
565 if dir != "" {
566 pat = "/" + dir + pat
567 }
568 }
569 r := newRule(pat, rule.flags)
570 rules = append(rules, r)
571 }
572 return rules, cleared, nil
573}
574
575// check returns -1 if name is excluded, 1 if it is included and 0 when no
576// rule matched. name is relative to the transfer root. sc holds the
577// per-directory rules for name's directory, if any have been read.
578func (l *filterList) check(name string, isDir bool, sc *filterScope) int {
579 return l.checkPerishable(name, isDir, sc, false)
580}
581
582// checkPerishable is check, skipping perishable rules when
583// ignorePerishable is set (rsync's ignore_perishable).
584func (l *filterList) checkPerishable(name string, isDir bool, sc *filterScope, ignorePerishable bool) int {
585 root := ""
586 if sc != nil {
587 root = sc.root
588 }
589 for _, r := range l.rules {
590 if ignorePerishable && r.flags&rulePerishable != 0 {
591 continue
592 }
593 if r.flags&rulePerdirMerge != 0 {
594 if sc != nil {
595 if rc := checkRules(sc.lists[r.merge], name, isDir, root, ignorePerishable); rc != 0 {
596 return rc
597 }
598 }
599 continue
600 }
601 if rc := checkRules([]*filterRule{r}, name, isDir, root, ignorePerishable); rc != 0 {
602 return rc
603 }
604 }
605 return 0
606}
607
608func checkRules(rules []*filterRule, name string, isDir bool, root string, ignorePerishable bool) int {
609 for _, r := range rules {
610 if ignorePerishable && r.flags&rulePerishable != 0 {
611 continue
612 }
613 if r.matches(name, isDir, root) {
614 if r.flags&ruleInclude != 0 {
615 return 1
616 }
617 return -1
618 }
619 }
620 return 0
621}
622
623func (l *filterList) excluded(name string, isDir bool, sc *filterScope) bool {
624 return l != nil && l.check(name, isDir, sc) < 0
625}
626
627// protectsInDeletedDir reports whether a rule keeps name, which is inside a
628// directory being deleted. Perishable rules don't, so the directory can go.
629func (l *filterList) protectsInDeletedDir(name string, isDir bool, sc *filterScope) bool {
630 return l != nil && l.checkPerishable(name, isDir, sc, true) < 0
631}
632
633// matches is rsync's rule_matches. Absolute patterns match the storage
634// path, which is name below root.
635func (r *filterRule) matches(fname string, isDir bool, root string) bool {
636 ret := r.flags&ruleNegate == 0
637 name := strings.TrimPrefix(fname, "/")
638 if name == "" || r.flags&ruleXattr != 0 {
639 return false
640 }
641
642 var strs []string
643 switch {
644 case r.slashCnt == 0 && r.flags&ruleWild2 == 0:
645 if i := strings.LastIndexByte(name, '/'); i >= 0 {
646 name = name[i+1:]
647 }
648 case r.flags&ruleAbsPath != 0 && !strings.HasPrefix(fname, "/") && root != "":
649 strs = append(strs, root, "/")
650 case r.flags&ruleWild2Prefix != 0 && !strings.HasPrefix(fname, "/"):
651 strs = append(strs, "/")
652 }
653 strs = append(strs, name)
654 if isDir {
655 if r.flags&ruleWild3Suffix != 0 {
656 strs = append(strs, "/")
657 }
658 } else if r.flags&ruleDirectory != 0 {
659 return !ret
660 }
661
662 pattern := r.pattern
663 anchored := false
664 if strings.HasPrefix(pattern, "/") {
665 anchored = true
666 pattern = pattern[1:]
667 }
668
669 var where int
670 switch {
671 case !anchored && r.slashCnt > 0 && r.flags&ruleWild2 == 0:
672 where = r.slashCnt + 1
673 case !anchored && r.flags&ruleWild2Prefix == 0 && r.flags&ruleWild2 != 0:
674 where = -1
675 }
676
677 text := strings.Join(strs, "")
678 switch {
679 case r.flags&ruleWild != 0:
680 if wildmatchWhere(pattern, text, where) {
681 return ret
682 }
683 case len(strs) > 1:
684 if t, ok := trailingElements(text, where); ok && t == pattern {
685 return ret
686 }
687 case anchored:
688 if name == pattern {
689 return ret
690 }
691 default:
692 if strings.HasSuffix(name, pattern) &&
693 (len(name) == len(pattern) || name[len(name)-len(pattern)-1] == '/') {
694 return ret
695 }
696 }
697 return !ret
698}
699
700// trailingElements returns the last count slash-separated elements of s.
701// A count of zero or less returns s unchanged.
702func trailingElements(s string, count int) (string, bool) {
703 if count <= 0 {
704 return s, true
705 }
706 for i := len(s) - 1; i >= 0; i-- {
707 if s[i] == '/' {
708 count--
709 if count == 0 {
710 return s[i+1:], true
711 }
712 }
713 }
714 if count == 1 {
715 return s, true
716 }
717 return "", false
718}
719
720// wildmatchWhere matches pattern against the trailing where elements of
721// text (where > 0), at the start of text (where == 0), or at the start or
722// after any slash (where < 0).
723func wildmatchWhere(pattern, text string, where int) bool {
724 t, ok := trailingElements(text, where)
725 if !ok {
726 return false
727 }
728 m := dowild(pattern, t)
729 if m == wildMatch || where >= 0 || m == wildAbortAll {
730 return m == wildMatch
731 }
732 for i := 0; i < len(t); i++ {
733 if t[i] != '/' {
734 continue
735 }
736 m = dowild(pattern, t[i+1:])
737 if m != wildNoMatch && m != wildAbortToStarStar {
738 return m == wildMatch
739 }
740 }
741 return false
742}
743
744// wildmatch reports whether text matches the rsync wildcard pattern. "*"
745// and "?" don't match "/", "**" does.
746func wildmatch(pattern, text string) bool {
747 return dowild(pattern, text) == wildMatch
748}
749
750const (
751 wildNoMatch = iota
752 wildMatch
753 wildAbortAll
754 wildAbortToStarStar
755)
756
757func dowild(p, text string) int {
758 pi, ti := 0, 0
759 for ; pi < len(p); pi, ti = pi+1, ti+1 {
760 pch := p[pi]
761 if ti >= len(text) && pch != '*' {
762 return wildAbortAll
763 }
764 var tch byte
765 if ti < len(text) {
766 tch = text[ti]
767 }
768 switch pch {
769 case '\\':
770 pi++
771 if pi >= len(p) || tch != p[pi] {
772 return wildNoMatch
773 }
774 case '?':
775 if tch == '/' {
776 return wildNoMatch
777 }
778 case '*':
779 special := false
780 pi++
781 if pi < len(p) && p[pi] == '*' {
782 for pi < len(p) && p[pi] == '*' {
783 pi++
784 }
785 special = true
786 }
787 if pi >= len(p) {
788 // A trailing "**" matches everything, "*" only if
789 // there are no more slashes.
790 if !special && strings.IndexByte(text[ti:], '/') >= 0 {
791 return wildNoMatch
792 }
793 return wildMatch
794 }
795 for ; ; ti++ {
796 if ti >= len(text) {
797 return wildAbortAll
798 }
799 m := dowild(p[pi:], text[ti:])
800 if m != wildNoMatch {
801 if !special || m != wildAbortToStarStar {
802 return m
803 }
804 } else if !special && text[ti] == '/' {
805 return wildAbortToStarStar
806 }
807 }
808 case '[':
809 pi++
810 if pi >= len(p) {
811 return wildAbortAll
812 }
813 negate := false
814 if p[pi] == '!' || p[pi] == '^' {
815 negate = true
816 pi++
817 }
818 matched := false
819 var prev byte
820 for first := true; ; first = false {
821 if pi >= len(p) {
822 return wildAbortAll
823 }
824 c := p[pi]
825 if c == ']' && !first {
826 break
827 }
828 switch {
829 case c == '\\':
830 pi++
831 if pi >= len(p) {
832 return wildAbortAll
833 }
834 c = p[pi]
835 if tch == c {
836 matched = true
837 }
838 case c == '-' && prev != 0 && pi+1 < len(p) && p[pi+1] != ']':
839 pi++
840 c = p[pi]
841 if c == '\\' {
842 pi++
843 if pi >= len(p) {
844 return wildAbortAll
845 }
846 c = p[pi]
847 }
848 if tch <= c && tch >= prev {
849 matched = true
850 }
851 c = 0
852 case c == '[' && pi+1 < len(p) && p[pi+1] == ':':
853 k := strings.IndexByte(p[pi+2:], ']')
854 if k < 0 {
855 return wildAbortAll
856 }
857 if k == 0 || p[pi+2+k-1] != ':' {
858 // No ":]", so the '[' is literal.
859 if tch == '[' {
860 matched = true
861 }
862 break
863 }
864 ok, valid := charClass(p[pi+2:pi+2+k-1], tch)
865 if !valid {
866 return wildAbortAll
867 }
868 if ok {
869 matched = true
870 }
871 pi += 2 + k
872 c = 0
873 default:
874 if tch == c {
875 matched = true
876 }
877 }
878 prev = c
879 pi++
880 }
881 if matched == negate || tch == '/' {
882 return wildNoMatch
883 }
884 default:
885 if tch != pch {
886 return wildNoMatch
887 }
888 }
889 }
890 if ti < len(text) {
891 return wildNoMatch
892 }
893 return wildMatch
894}
895
896func charClass(class string, c byte) (match, valid bool) {
897 isUpper := c >= 'A' && c <= 'Z'
898 isLower := c >= 'a' && c <= 'z'
899 isDigit := c >= '0' && c <= '9'
900 isAlpha := isUpper || isLower
901 isSpace := c == ' ' || (c >= '\t' && c <= '\r')
902 isPrint := c >= 0x20 && c < 0x7f
903 switch class {
904 case "alnum":
905 return isAlpha || isDigit, true
906 case "alpha":
907 return isAlpha, true
908 case "blank":
909 return c == ' ' || c == '\t', true
910 case "cntrl":
911 return c < 0x20 || c == 0x7f, true
912 case "digit":
913 return isDigit, true
914 case "graph":
915 return isPrint && c != ' ', true
916 case "lower":
917 return isLower, true
918 case "print":
919 return isPrint, true
920 case "punct":
921 return isPrint && c != ' ' && !isAlpha && !isDigit, true
922 case "space":
923 return isSpace, true
924 case "upper":
925 return isUpper, true
926 case "xdigit":
927 return isDigit || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'), true
928 }
929 return false, false
930}