main pico / pkg / rsync / filter.go
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}