Skip to content

Commit

Permalink
implement basic exclusion list support (good enough for --exclude)
Browse files Browse the repository at this point in the history
This hasn’t been tested extensively yet.

Please report discrepancies (compared to original rsync) as you discover them.
  • Loading branch information
stapelberg committed Aug 30, 2024
1 parent 67193a5 commit 15c12f9
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 11 deletions.
36 changes: 34 additions & 2 deletions interop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ func TestMain(m *testing.M) {
}
}

// TODO: non-empty exclusion list

func TestRsyncVersion(t *testing.T) {
// This function is not an actual test, just used to include the rsync
// version in test output.
Expand Down Expand Up @@ -313,6 +311,40 @@ func TestInteropSubdir(t *testing.T) {
}
}

func TestInteropSubdirExclude(t *testing.T) {
_, source, dest := createSourceFiles(t)

// start a server to sync from
srv := rsynctest.New(t, rsynctest.InteropModule(source))

// sync into dest dir
rsync := exec.Command("rsync", //"/home/michael/src/openrsync/openrsync",
append(
append([]string{
// "--debug=all4",
"--archive",
// TODO: implement support for include rules
//"-f", "+ *.o",
// NOTE: Using -f is the more modern replacement
// for using --exclude like so:
//"--exclude=dummy",
"-f", "- dummy",
"-v", "-v", "-v", "-v",
"--port=" + srv.Port,
}, sourcesArgs(t)...),
dest)...)
rsync.Stdout = os.Stdout
rsync.Stderr = os.Stderr
if err := rsync.Run(); err != nil {
t.Fatalf("%v: %v", rsync.Args, err)
}

dummyFn := filepath.Join(dest, "dummy")
if _, err := os.ReadFile(dummyFn); !os.IsNotExist(err) {
t.Fatalf("ReadFile(%s) did not return -ENOENT, but %v", dummyFn, err)
}
}

func TestInteropRemoteCommand(t *testing.T) {
_, source, dest := createSourceFiles(t)

Expand Down
112 changes: 112 additions & 0 deletions rsyncd/exclude.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package rsyncd

import (
"io"
"path/filepath"
"strings"

"github.com/gokrazy/rsync/internal/log"
"github.com/gokrazy/rsync/internal/rsyncwire"
)

type filterRuleList struct {
filters []*filterRule
}

// exclude.c:add_rule
func (l *filterRuleList) addRule(fr *filterRule) {
if strings.HasSuffix(fr.pattern, "/") {
fr.flag |= filtruleDirectory
fr.pattern = strings.TrimSuffix(fr.pattern, "/")
}
if strings.ContainsFunc(fr.pattern, func(r rune) bool {
return r == '*' || r == '[' || r == '?'
}) {
fr.flag |= filtruleWild
}
l.filters = append(l.filters, fr)
}

// exclude.c:check_filter
func (l *filterRuleList) matches(name string) bool {
for _, fr := range l.filters {
if fr.matches(name) {
return true
}
}
return false
}

// exclude.c:recv_filter_list
func recvFilterList(c *rsyncwire.Conn) (*filterRuleList, error) {
var l filterRuleList
const exclusionListEnd = 0
for {
length, err := c.ReadInt32()
if err != nil {
return nil, err
}
if length == exclusionListEnd {
break
}
line := make([]byte, length)
if _, err := io.ReadFull(c.Reader, line); err != nil {
return nil, err
}
log.Printf("exclude: %q", string(line))
fr, err := parseFilter(string(line))
if err != nil {
return nil, err
}
l.addRule(fr)
}
return &l, nil
}

const (
filtruleInclude = 1 << iota
filtruleClearList
filtruleDirectory
filtruleWild
)

type filterRule struct {
flag int
pattern string
}

// exclude.c:rule_matches
func (fr *filterRule) matches(name string) bool {
log.Printf("fr %v matches %q?", fr, name)
if fr.flag&filtruleWild != 0 {
panic("wildcard filter rules not yet implemented")
}
if !strings.ContainsRune(fr.pattern, '/') &&
fr.flag&filtruleWild == 0 {
name = filepath.Base(name)
}
return fr.pattern == name
}

// exclude.c:parse_filter_str / exclude.c:parse_rule_tok
func parseFilter(line string) (*filterRule, error) {
rule := new(filterRule)

// We only support what rsync calls XFLG_OLD_PREFIXES
if strings.HasPrefix(line, "- ") {
// clear include flag
rule.flag &= ^filtruleInclude
line = strings.TrimPrefix(line, "- ")
} else if strings.HasPrefix(line, "+ ") {
// set include flag
rule.flag |= filtruleInclude
line = strings.TrimPrefix(line, "+ ")
} else if strings.HasPrefix(line, "!") {
// set clear_list flag
rule.flag |= filtruleClearList
}

rule.pattern = line

return rule, nil
}
6 changes: 5 additions & 1 deletion rsyncd/flist.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var (
)

// rsync/flist.c:send_file_list
func (st *sendTransfer) sendFileList(mod Module, opts *Opts, paths []string) (*fileList, error) {
func (st *sendTransfer) sendFileList(mod Module, opts *Opts, paths []string, excl *filterRuleList) (*fileList, error) {
var fileList fileList
fec := &rsyncwire.Buffer{}

Expand Down Expand Up @@ -48,6 +48,10 @@ func (st *sendTransfer) sendFileList(mod Module, opts *Opts, paths []string) (*f
return err
}

if excl.matches(path) {
return nil
}

// Only ever transmit long names, like openrsync
flags := byte(rsync.XMIT_LONG_NAME)

Expand Down
11 changes: 3 additions & 8 deletions rsyncd/rsyncd.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,22 +356,17 @@ func (s *Server) HandleConn(module Module, rd io.Reader, crd *countingReader, cw
}

// receive the exclusion list (openrsync’s is always empty)
const exclusionListEnd = 0
got, err := c.ReadInt32()
exclusionList, err := recvFilterList(c)
if err != nil {
return err
}
if want := int32(exclusionListEnd); got != want {
return fmt.Errorf("protocol error: non-empty exclusion list received")
}

s.logger.Printf("exclusion list read")
s.logger.Printf("exclusion list read (entries: %d)", len(exclusionList.filters))

// “Update exchange” as per
// https://github.com/kristapsdz/openrsync/blob/master/rsync.5

// send file list
fileList, err := st.sendFileList(module, opts, paths)
fileList, err := st.sendFileList(module, opts, paths, exclusionList)
if err != nil {
return err
}
Expand Down

0 comments on commit 15c12f9

Please sign in to comment.