From ed464af779f00235b602f7a4fa2e50a0b079d862 Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Thu, 15 Jun 2017 14:54:58 -0400 Subject: [PATCH] *: xattr can Update() This is a gnarly patchset that has been mashed together. It uncovered that some aspects of Check were never really working correctly for `xattr` keywords, but also the `Update()` had been left undone for a while. This includes some API changes around the `Keyword` and `KeyVal` types. Also I would like to update the signature for the `UpdateKeywordFunc` to just accept a `KeyVal` as an argugment, rather than a keyword AND the value. with this context there would be no need to guess on the value of what's passed to the xattr update function of whether it needs or already is base64 encoded. Signed-off-by: Vincent Batts --- check_test.go | 2 +- cmd/gomtree/main.go | 5 +- compare.go | 4 +- debug.go | 26 ---------- keywordfunc.go | 70 +++++++++++++-------------- keywordfuncs_bsd.go | 44 ++++++++--------- keywordfuncs_linux.go | 59 ++++++++++++----------- keywordfuncs_unsupported.go | 36 +++++++------- keywords.go | 80 ++++++++++++++++++------------- keywords_linux_test.go | 12 ++--- keywords_test.go | 73 ++++++++++++++++------------ tar.go | 38 ++++++++------- update.go | 23 +++++---- update_linux_test.go | 94 +++++++++++++++++++++++++++++++++++++ update_test.go | 3 +- updatefuncs.go | 19 ++++---- updatefuncs_linux.go | 21 +++++++++ updatefuncs_unsupported.go | 9 ++++ walk.go | 38 +++++++++------ 19 files changed, 398 insertions(+), 258 deletions(-) delete mode 100644 debug.go create mode 100644 update_linux_test.go create mode 100644 updatefuncs_linux.go create mode 100644 updatefuncs_unsupported.go diff --git a/check_test.go b/check_test.go index 3acc079..a8dcb38 100644 --- a/check_test.go +++ b/check_test.go @@ -12,7 +12,7 @@ import ( // simple walk of current directory, and imediately check it. // may not be parallelizable. func TestCheck(t *testing.T) { - dh, err := Walk(".", nil, append(DefaultKeywords, "sha1"), nil) + dh, err := Walk(".", nil, append(DefaultKeywords, []Keyword{"sha1", "xattr"}...), nil) if err != nil { t.Fatal(err) } diff --git a/cmd/gomtree/main.go b/cmd/gomtree/main.go index 4c3d488..4bd252c 100644 --- a/cmd/gomtree/main.go +++ b/cmd/gomtree/main.go @@ -7,10 +7,10 @@ import ( "fmt" "io" "io/ioutil" - "log" "os" "strings" + "github.com/Sirupsen/logrus" "github.com/vbatts/go-mtree" ) @@ -37,7 +37,7 @@ var ( func main() { // so that defers cleanly exec if err := app(); err != nil { - log.Fatal(err) + logrus.Fatal(err) } } @@ -46,6 +46,7 @@ func app() error { if *flDebug { os.Setenv("DEBUG", "1") + logrus.SetLevel(logrus.DebugLevel) } if *flVersion { diff --git a/compare.go b/compare.go index 119b79c..b45600f 100644 --- a/compare.go +++ b/compare.go @@ -197,7 +197,7 @@ func compareEntry(oldEntry, newEntry Entry) ([]KeyDelta, error) { for _, kv := range oldKeys { key := kv.Keyword() // only add this diff if the new keys has this keyword - if key != "tar_time" && key != "time" && key != "xattr" && HasKeyword(newKeys, key) == emptyKV { + if key != "tar_time" && key != "time" && key != "xattr" && len(HasKeyword(newKeys, key)) == 0 { continue } @@ -216,7 +216,7 @@ func compareEntry(oldEntry, newEntry Entry) ([]KeyDelta, error) { for _, kv := range newKeys { key := kv.Keyword() // only add this diff if the old keys has this keyword - if key != "tar_time" && key != "time" && key != "xattr" && HasKeyword(oldKeys, key) == emptyKV { + if key != "tar_time" && key != "time" && key != "xattr" && len(HasKeyword(oldKeys, key)) == 0 { continue } diff --git a/debug.go b/debug.go deleted file mode 100644 index 2decc6d..0000000 --- a/debug.go +++ /dev/null @@ -1,26 +0,0 @@ -package mtree - -import ( - "fmt" - "os" - "time" -) - -// DebugOutput is the where DEBUG output is written -var DebugOutput = os.Stderr - -// Debugln writes output to DebugOutput, only if DEBUG environment variable is set -func Debugln(a ...interface{}) (n int, err error) { - if os.Getenv("DEBUG") != "" { - return fmt.Fprintf(DebugOutput, "[%d] [DEBUG] %s\n", time.Now().UnixNano(), a) - } - return 0, nil -} - -// Debugf writes formatted output to DebugOutput, only if DEBUG environment variable is set -func Debugf(format string, a ...interface{}) (n int, err error) { - if os.Getenv("DEBUG") != "" { - return fmt.Fprintf(DebugOutput, "[%d] [DEBUG] %s\n", time.Now().UnixNano(), fmt.Sprintf(format, a...)) - } - return 0, nil -} diff --git a/keywordfunc.go b/keywordfunc.go index 14ce44d..a3ef6e4 100644 --- a/keywordfunc.go +++ b/keywordfunc.go @@ -21,7 +21,7 @@ import ( // io.Reader `r` is to the file stream for the file payload. While this // function takes an io.Reader, the caller needs to reset it to the beginning // for each new KeywordFunc -type KeywordFunc func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) +type KeywordFunc func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) var ( // KeywordFuncs is the map of all keywords (and the functions to produce them) @@ -67,7 +67,7 @@ var ( } ) var ( - modeKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + modeKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { permissions := info.Mode().Perm() if os.ModeSetuid&info.Mode() > 0 { permissions |= (1 << 11) @@ -78,93 +78,93 @@ var ( if os.ModeSticky&info.Mode() > 0 { permissions |= (1 << 9) } - return KeyVal(fmt.Sprintf("mode=%#o", permissions)), nil + return []KeyVal{KeyVal(fmt.Sprintf("mode=%#o", permissions))}, nil } - sizeKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + sizeKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if sys, ok := info.Sys().(*tar.Header); ok { if sys.Typeflag == tar.TypeSymlink { - return KeyVal(fmt.Sprintf("size=%d", len(sys.Linkname))), nil + return []KeyVal{KeyVal(fmt.Sprintf("size=%d", len(sys.Linkname)))}, nil } } - return KeyVal(fmt.Sprintf("size=%d", info.Size())), nil + return []KeyVal{KeyVal(fmt.Sprintf("size=%d", info.Size()))}, nil } - cksumKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + cksumKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if !info.Mode().IsRegular() { - return emptyKV, nil + return nil, nil } sum, _, err := cksum(r) if err != nil { - return emptyKV, err + return nil, err } - return KeyVal(fmt.Sprintf("cksum=%d", sum)), nil + return []KeyVal{KeyVal(fmt.Sprintf("cksum=%d", sum))}, nil } hasherKeywordFunc = func(name string, newHash func() hash.Hash) KeywordFunc { - return func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + return func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if !info.Mode().IsRegular() { - return emptyKV, nil + return nil, nil } h := newHash() if _, err := io.Copy(h, r); err != nil { - return emptyKV, err + return nil, err } - return KeyVal(fmt.Sprintf("%s=%x", KeywordSynonym(name), h.Sum(nil))), nil + return []KeyVal{KeyVal(fmt.Sprintf("%s=%x", KeywordSynonym(name), h.Sum(nil)))}, nil } } - tartimeKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { - return KeyVal(fmt.Sprintf("tar_time=%d.%9.9d", info.ModTime().Unix(), 0)), nil + tartimeKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { + return []KeyVal{KeyVal(fmt.Sprintf("tar_time=%d.%9.9d", info.ModTime().Unix(), 0))}, nil } - timeKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + timeKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { tSec := info.ModTime().Unix() tNano := info.ModTime().Nanosecond() - return KeyVal(fmt.Sprintf("time=%d.%9.9d", tSec, tNano)), nil + return []KeyVal{KeyVal(fmt.Sprintf("time=%d.%9.9d", tSec, tNano))}, nil } - linkKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + linkKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if sys, ok := info.Sys().(*tar.Header); ok { if sys.Linkname != "" { linkname, err := govis.Vis(sys.Linkname, DefaultVisFlags) if err != nil { - return emptyKV, err + return nil, nil } - return KeyVal(fmt.Sprintf("link=%s", linkname)), nil + return []KeyVal{KeyVal(fmt.Sprintf("link=%s", linkname))}, nil } - return emptyKV, nil + return nil, nil } if info.Mode()&os.ModeSymlink != 0 { str, err := os.Readlink(path) if err != nil { - return emptyKV, err + return nil, nil } linkname, err := govis.Vis(str, DefaultVisFlags) if err != nil { - return emptyKV, err + return nil, nil } - return KeyVal(fmt.Sprintf("link=%s", linkname)), nil + return []KeyVal{KeyVal(fmt.Sprintf("link=%s", linkname))}, nil } - return emptyKV, nil + return nil, nil } - typeKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + typeKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if info.Mode().IsDir() { - return "type=dir", nil + return []KeyVal{"type=dir"}, nil } if info.Mode().IsRegular() { - return "type=file", nil + return []KeyVal{"type=file"}, nil } if info.Mode()&os.ModeSocket != 0 { - return "type=socket", nil + return []KeyVal{"type=socket"}, nil } if info.Mode()&os.ModeSymlink != 0 { - return "type=link", nil + return []KeyVal{"type=link"}, nil } if info.Mode()&os.ModeNamedPipe != 0 { - return "type=fifo", nil + return []KeyVal{"type=fifo"}, nil } if info.Mode()&os.ModeDevice != 0 { if info.Mode()&os.ModeCharDevice != 0 { - return "type=char", nil + return []KeyVal{"type=char"}, nil } - return "type=block", nil + return []KeyVal{"type=block"}, nil } - return emptyKV, nil + return nil, nil } ) diff --git a/keywordfuncs_bsd.go b/keywordfuncs_bsd.go index 8de2608..6114109 100644 --- a/keywordfuncs_bsd.go +++ b/keywordfuncs_bsd.go @@ -12,58 +12,58 @@ import ( ) var ( - flagsKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + flagsKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { // ideally this will pull in from here https://www.freebsd.org/cgi/man.cgi?query=chflags&sektion=2 - return emptyKV, nil + return nil, nil } - unameKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + unameKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if hdr, ok := info.Sys().(*tar.Header); ok { - return KeyVal(fmt.Sprintf("uname=%s", hdr.Uname)), nil + return []KeyVal{KeyVal(fmt.Sprintf("uname=%s", hdr.Uname))}, nil } stat := info.Sys().(*syscall.Stat_t) u, err := user.LookupId(fmt.Sprintf("%d", stat.Uid)) if err != nil { - return emptyKV, err + return nil, err } - return KeyVal(fmt.Sprintf("uname=%s", u.Username)), nil + return []KeyVal{KeyVal(fmt.Sprintf("uname=%s", u.Username))}, nil } - gnameKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + gnameKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if hdr, ok := info.Sys().(*tar.Header); ok { - return KeyVal(fmt.Sprintf("gname=%s", hdr.Gname)), nil + return []KeyVal{KeyVal(fmt.Sprintf("gname=%s", hdr.Gname))}, nil } stat := info.Sys().(*syscall.Stat_t) g, err := lookupGroupID(fmt.Sprintf("%d", stat.Gid)) if err != nil { - return emptyKV, err + return nil, err } - return KeyVal(fmt.Sprintf("gname=%s", g.Name)), nil + return []KeyVal{KeyVal(fmt.Sprintf("gname=%s", g.Name))}, nil } - uidKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + uidKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if hdr, ok := info.Sys().(*tar.Header); ok { - return KeyVal(fmt.Sprintf("uid=%d", hdr.Uid)), nil + return []KeyVal{KeyVal(fmt.Sprintf("uid=%d", hdr.Uid))}, nil } stat := info.Sys().(*syscall.Stat_t) - return KeyVal(fmt.Sprintf("uid=%d", stat.Uid)), nil + return []KeyVal{KeyVal(fmt.Sprintf("uid=%d", stat.Uid))}, nil } - gidKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + gidKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if hdr, ok := info.Sys().(*tar.Header); ok { - return KeyVal(fmt.Sprintf("gid=%d", hdr.Gid)), nil + return []KeyVal{KeyVal(fmt.Sprintf("gid=%d", hdr.Gid))}, nil } if stat, ok := info.Sys().(*syscall.Stat_t); ok { - return KeyVal(fmt.Sprintf("gid=%d", stat.Gid)), nil + return []KeyVal{KeyVal(fmt.Sprintf("gid=%d", stat.Gid))}, nil } - return emptyKV, nil + return nil, nil } - nlinkKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + nlinkKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if stat, ok := info.Sys().(*syscall.Stat_t); ok { - return KeyVal(fmt.Sprintf("nlink=%d", stat.Nlink)), nil + return []KeyVal{KeyVal(fmt.Sprintf("nlink=%d", stat.Nlink))}, nil } - return emptyKV, nil + return nil, nil } - xattrKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { - return emptyKV, nil + xattrKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { + return nil, nil } ) diff --git a/keywordfuncs_linux.go b/keywordfuncs_linux.go index c45e1ab..2fd82c2 100644 --- a/keywordfuncs_linux.go +++ b/keywordfuncs_linux.go @@ -9,7 +9,6 @@ import ( "io" "os" "os/user" - "strings" "syscall" "github.com/vbatts/go-mtree/pkg/govis" @@ -18,91 +17,91 @@ import ( var ( // this is bsd specific https://www.freebsd.org/cgi/man.cgi?query=chflags&sektion=2 - flagsKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { - return emptyKV, nil + flagsKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { + return nil, nil } - unameKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + unameKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if hdr, ok := info.Sys().(*tar.Header); ok { - return KeyVal(fmt.Sprintf("uname=%s", hdr.Uname)), nil + return []KeyVal{KeyVal(fmt.Sprintf("uname=%s", hdr.Uname))}, nil } stat := info.Sys().(*syscall.Stat_t) u, err := user.LookupId(fmt.Sprintf("%d", stat.Uid)) if err != nil { - return emptyKV, err + return nil, nil } - return KeyVal(fmt.Sprintf("uname=%s", u.Username)), nil + return []KeyVal{KeyVal(fmt.Sprintf("uname=%s", u.Username))}, nil } - gnameKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + gnameKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if hdr, ok := info.Sys().(*tar.Header); ok { - return KeyVal(fmt.Sprintf("gname=%s", hdr.Gname)), nil + return []KeyVal{KeyVal(fmt.Sprintf("gname=%s", hdr.Gname))}, nil } stat := info.Sys().(*syscall.Stat_t) g, err := lookupGroupID(fmt.Sprintf("%d", stat.Gid)) if err != nil { - return emptyKV, err + return nil, nil } - return KeyVal(fmt.Sprintf("gname=%s", g.Name)), nil + return []KeyVal{KeyVal(fmt.Sprintf("gname=%s", g.Name))}, nil } - uidKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + uidKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if hdr, ok := info.Sys().(*tar.Header); ok { - return KeyVal(fmt.Sprintf("uid=%d", hdr.Uid)), nil + return []KeyVal{KeyVal(fmt.Sprintf("uid=%d", hdr.Uid))}, nil } stat := info.Sys().(*syscall.Stat_t) - return KeyVal(fmt.Sprintf("uid=%d", stat.Uid)), nil + return []KeyVal{KeyVal(fmt.Sprintf("uid=%d", stat.Uid))}, nil } - gidKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + gidKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if hdr, ok := info.Sys().(*tar.Header); ok { - return KeyVal(fmt.Sprintf("gid=%d", hdr.Gid)), nil + return []KeyVal{KeyVal(fmt.Sprintf("gid=%d", hdr.Gid))}, nil } if stat, ok := info.Sys().(*syscall.Stat_t); ok { - return KeyVal(fmt.Sprintf("gid=%d", stat.Gid)), nil + return []KeyVal{KeyVal(fmt.Sprintf("gid=%d", stat.Gid))}, nil } - return emptyKV, nil + return nil, nil } - nlinkKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + nlinkKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if stat, ok := info.Sys().(*syscall.Stat_t); ok { - return KeyVal(fmt.Sprintf("nlink=%d", stat.Nlink)), nil + return []KeyVal{KeyVal(fmt.Sprintf("nlink=%d", stat.Nlink))}, nil } - return emptyKV, nil + return nil, nil } - xattrKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + xattrKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if hdr, ok := info.Sys().(*tar.Header); ok { if len(hdr.Xattrs) == 0 { - return emptyKV, nil + return nil, nil } klist := []KeyVal{} for k, v := range hdr.Xattrs { encKey, err := govis.Vis(k, DefaultVisFlags) if err != nil { - return emptyKV, err + return nil, nil } klist = append(klist, KeyVal(fmt.Sprintf("xattr.%s=%s", encKey, base64.StdEncoding.EncodeToString([]byte(v))))) } - return KeyVal(strings.Join(KeyValToString(klist), " ")), nil + return klist, nil } if !info.Mode().IsRegular() && !info.Mode().IsDir() { - return emptyKV, nil + return nil, nil } xlist, err := xattr.List(path) if err != nil { - return emptyKV, err + return nil, nil } klist := make([]KeyVal, len(xlist)) for i := range xlist { data, err := xattr.Get(path, xlist[i]) if err != nil { - return emptyKV, err + return nil, nil } encKey, err := govis.Vis(xlist[i], DefaultVisFlags) if err != nil { - return emptyKV, err + return nil, nil } klist[i] = KeyVal(fmt.Sprintf("xattr.%s=%s", encKey, base64.StdEncoding.EncodeToString(data))) } - return KeyVal(strings.Join(KeyValToString(klist), " ")), nil + return klist, nil } ) diff --git a/keywordfuncs_unsupported.go b/keywordfuncs_unsupported.go index a582f79..1284895 100644 --- a/keywordfuncs_unsupported.go +++ b/keywordfuncs_unsupported.go @@ -11,37 +11,37 @@ import ( var ( // this is bsd specific https://www.freebsd.org/cgi/man.cgi?query=chflags&sektion=2 - flagsKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { - return emptyKV, nil + flagsKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { + return nil, nil } - unameKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + unameKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if hdr, ok := info.Sys().(*tar.Header); ok { - return KeyVal(fmt.Sprintf("uname=%s", hdr.Uname)), nil + return []KeyVal{KeyVal(fmt.Sprintf("uname=%s", hdr.Uname))}, nil } - return emptyKV, nil + return nil, nil } - gnameKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + gnameKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if hdr, ok := info.Sys().(*tar.Header); ok { - return KeyVal(fmt.Sprintf("gname=%s", hdr.Gname)), nil + return []KeyVal{KeyVal(fmt.Sprintf("gname=%s", hdr.Gname))}, nil } - return emptyKV, nil + return nil, nil } - uidKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + uidKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if hdr, ok := info.Sys().(*tar.Header); ok { - return KeyVal(fmt.Sprintf("uid=%d", hdr.Uid)), nil + return []KeyVal{KeyVal(fmt.Sprintf("uid=%d", hdr.Uid))}, nil } - return emptyKV, nil + return nil, nil } - gidKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { + gidKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { if hdr, ok := info.Sys().(*tar.Header); ok { - return KeyVal(fmt.Sprintf("gid=%d", hdr.Gid)), nil + return []KeyVal{KeyVal(fmt.Sprintf("gid=%d", hdr.Gid))}, nil } - return emptyKV, nil + return nil, nil } - nlinkKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { - return emptyKV, nil + nlinkKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { + return nil, nil } - xattrKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (KeyVal, error) { - return emptyKV, nil + xattrKeywordFunc = func(path string, info os.FileInfo, r io.Reader) ([]KeyVal, error) { + return nil, nil } ) diff --git a/keywords.go b/keywords.go index 4266a0f..46f9a8c 100644 --- a/keywords.go +++ b/keywords.go @@ -13,8 +13,30 @@ const DefaultVisFlags govis.VisFlag = govis.VisWhite | govis.VisOctal | govis.Vi // Keyword is the string name of a keyword, with some convenience functions for // determining whether it is a default or bsd standard keyword. +// It first portion before the "=" type Keyword string +// Prefix is the portion of the keyword before a first "." (if present). +// +// Primarly for the xattr use-case, where the keyword `xattr.security.selinux` would have a Suffix of `security.selinux`. +func (k Keyword) Prefix() Keyword { + if strings.Contains(string(k), ".") { + return Keyword(strings.SplitN(string(k), ".", 2)[0]) + } + return k +} + +// Suffix is the portion of the keyword after a first ".". +// This is an option feature. +// +// Primarly for the xattr use-case, where the keyword `xattr.security.selinux` would have a Suffix of `security.selinux`. +func (k Keyword) Suffix() string { + if strings.Contains(string(k), ".") { + return strings.SplitN(string(k), ".", 2)[1] + } + return string(k) +} + // Default returns whether this keyword is in the default set of keywords func (k Keyword) Default() bool { return InKeywordSlice(k, DefaultKeywords) @@ -93,24 +115,7 @@ func (kv KeyVal) Keyword() Keyword { if !strings.Contains(string(kv), "=") { return Keyword("") } - chunks := strings.SplitN(strings.TrimSpace(string(kv)), "=", 2)[0] - if !strings.Contains(chunks, ".") { - return Keyword(chunks) - } - return Keyword(strings.SplitN(chunks, ".", 2)[0]) -} - -// KeywordSuffix is really only used for xattr, as the keyword is a prefix to -// the xattr "namespace.key" -func (kv KeyVal) KeywordSuffix() string { - if !strings.Contains(string(kv), "=") { - return "" - } - chunks := strings.SplitN(strings.TrimSpace(string(kv)), "=", 2)[0] - if !strings.Contains(chunks, ".") { - return "" - } - return strings.SplitN(chunks, ".", 2)[1] + return Keyword(strings.SplitN(strings.TrimSpace(string(kv)), "=", 2)[0]) } // Value is the data/value portion of "keyword=value" @@ -123,9 +128,6 @@ func (kv KeyVal) Value() string { // NewValue returns a new KeyVal with the newval func (kv KeyVal) NewValue(newval string) KeyVal { - if suff := kv.KeywordSuffix(); suff != "" { - return KeyVal(fmt.Sprintf("%s.%s=%s", kv.Keyword(), suff, newval)) - } return KeyVal(fmt.Sprintf("%s=%s", kv.Keyword(), newval)) } @@ -135,14 +137,23 @@ func (kv KeyVal) NewValue(newval string) KeyVal { // doing. func (kv KeyVal) Equal(b KeyVal) bool { // TODO: Implement handling of tar_mtime. - return kv.Keyword() == b.Keyword() && kv.KeywordSuffix() == b.KeywordSuffix() && kv.Value() == b.Value() + return kv.Keyword() == b.Keyword() && kv.Value() == b.Value() } -// keyvalSelector takes an array of KeyVal ("keyword=value") and filters out that only the set of keywords +func keywordPrefixes(kvset []Keyword) []Keyword { + kvs := []Keyword{} + for _, kv := range kvset { + kvs = append(kvs, kv.Prefix()) + } + return kvs +} + +// keyvalSelector takes an array of KeyVal ("keyword=value") and filters out +// that only the set of keywords func keyvalSelector(keyval []KeyVal, keyset []Keyword) []KeyVal { retList := []KeyVal{} for _, kv := range keyval { - if InKeywordSlice(kv.Keyword(), keyset) { + if InKeywordSlice(kv.Keyword().Prefix(), keywordPrefixes(keyset)) { retList = append(retList, kv) } } @@ -171,23 +182,23 @@ func keyValCopy(set []KeyVal) []KeyVal { // Has the "keyword" present in the list of KeyVal, and returns the // corresponding KeyVal, else an empty string. -func Has(keyvals []KeyVal, keyword string) KeyVal { +func Has(keyvals []KeyVal, keyword string) []KeyVal { return HasKeyword(keyvals, Keyword(keyword)) } // HasKeyword the "keyword" present in the list of KeyVal, and returns the // corresponding KeyVal, else an empty string. -func HasKeyword(keyvals []KeyVal, keyword Keyword) KeyVal { +// This match is done on the Prefix of the keyword only. +func HasKeyword(keyvals []KeyVal, keyword Keyword) []KeyVal { + kvs := []KeyVal{} for i := range keyvals { - if keyvals[i].Keyword() == keyword { - return keyvals[i] + if keyvals[i].Keyword().Prefix() == keyword.Prefix() { + kvs = append(kvs, keyvals[i]) } } - return emptyKV + return kvs } -var emptyKV = KeyVal("") - // MergeSet takes the current setKeyVals, and then applies the entryKeyVals // such that the entry's values win. The union is returned. func MergeSet(setKeyVals, entryKeyVals []string) []KeyVal { @@ -203,8 +214,11 @@ func MergeKeyValSet(setKeyVals, entryKeyVals []KeyVal) []KeyVal { seenKeywords := []Keyword{} for i := range retList { word := retList[i].Keyword() - if ekv := HasKeyword(entryKeyVals, word); ekv != emptyKV { - retList[i] = ekv + for _, kv := range HasKeyword(entryKeyVals, word) { + // match on the keyword prefix and suffix here + if kv.Keyword() == word { + retList[i] = kv + } } seenKeywords = append(seenKeywords, word) } diff --git a/keywords_linux_test.go b/keywords_linux_test.go index eeb7042..3f989f8 100644 --- a/keywords_linux_test.go +++ b/keywords_linux_test.go @@ -51,12 +51,12 @@ func TestXattr(t *testing.T) { t.Fatal(err) } // Check the directory - str, err := xattrKeywordFunc(dir, dirstat, nil) + kvs, err := xattrKeywordFunc(dir, dirstat, nil) if err != nil { t.Error(err) } - if str == "" { - t.Errorf("expected a keyval; got %q", str) + if len(kvs) == 0 { + t.Errorf("expected a keyval; got none") } filestat, err := fh.Stat() @@ -64,12 +64,12 @@ func TestXattr(t *testing.T) { t.Fatal(err) } // Check the regular file - str, err = xattrKeywordFunc(filepath.Join(dir, "file"), filestat, fh) + kvs, err = xattrKeywordFunc(filepath.Join(dir, "file"), filestat, fh) if err != nil { t.Error(err) } - if str == "" { - t.Errorf("expected a keyval; got %q", str) + if len(kvs) == 0 { + t.Errorf("expected a keyval; got none") } linkstat, err := os.Lstat(filepath.Join(dir, "symlink")) diff --git a/keywords_test.go b/keywords_test.go index 5f3a4b9..3dac002 100644 --- a/keywords_test.go +++ b/keywords_test.go @@ -8,38 +8,43 @@ import ( ) func TestKeyValRoundtrip(t *testing.T) { - kv := KeyVal("xattr.security.selinux=dW5jb25maW5lZF91Om9iamVjdF9yOnVzZXJfaG9tZV90OnMwAA==") - expected := "xattr" - got := string(kv.Keyword()) - if got != expected { - t.Errorf("expected %q; got %q", expected, got) - } + kv := KeyVal("xattr.security.selinux=dW5jb25maW5lZF91Om9iamVjdF9yOnVzZXJfaG9tZV90OnMwAA==") + expected := "xattr.security.selinux" + got := string(kv.Keyword()) + if got != expected { + t.Errorf("expected %q; got %q", expected, got) + } - expected = "security.selinux" - got = kv.KeywordSuffix() - if got != expected { - t.Errorf("expected %q; got %q", expected, got) - } + expected = "xattr" + got = string(kv.Keyword().Prefix()) + if got != expected { + t.Errorf("expected %q; got %q", expected, got) + } - expected = "dW5jb25maW5lZF91Om9iamVjdF9yOnVzZXJfaG9tZV90OnMwAA==" - got = kv.Value() - if got != expected { - t.Errorf("expected %q; got %q", expected, got) - } + expected = "security.selinux" + got = kv.Keyword().Suffix() + if got != expected { + t.Errorf("expected %q; got %q", expected, got) + } + expected = "dW5jb25maW5lZF91Om9iamVjdF9yOnVzZXJfaG9tZV90OnMwAA==" + got = kv.Value() + if got != expected { + t.Errorf("expected %q; got %q", expected, got) + } - expected = "xattr.security.selinux=farts" - got = string(kv.NewValue("farts")) - if got != expected { - t.Errorf("expected %q; got %q", expected, got) - } + expected = "xattr.security.selinux=farts" + got = string(kv.NewValue("farts")) + if got != expected { + t.Errorf("expected %q; got %q", expected, got) + } - expected = "xattr.security.selinux=farts" - kv1 := KeyVal(got) - kv2 := kv.NewValue("farts") - if !kv2.Equal(kv1) { - t.Errorf("expected equality of %q and %q", kv1, kv2) - } + expected = "xattr.security.selinux=farts" + kv1 := KeyVal(got) + kv2 := kv.NewValue("farts") + if !kv2.Equal(kv1) { + t.Errorf("expected equality of %q and %q", kv1, kv2) + } } @@ -97,8 +102,11 @@ func TestKeywordsTimeNano(t *testing.T) { if err != nil { t.Errorf("unexpected error while parsing '%q': %q", mtime, err) } - if expected != got { - t.Errorf("keyword didn't match, expected '%s' got '%s'", expected, got) + if len(got) != 1 { + t.Errorf("expected 1 KeyVal, but got %d", len(got)) + } + if expected != got[0] { + t.Errorf("keyword didn't match, expected '%s' got '%s'", expected, got[0]) } } } @@ -124,8 +132,11 @@ func TestKeywordsTimeTar(t *testing.T) { if err != nil { t.Errorf("unexpected error while parsing '%q': %q", mtime, err) } - if expected != got { - t.Errorf("keyword didn't match, expected '%s' got '%s'", expected, got) + if len(got) != 1 { + t.Errorf("expected 1 KeyVal, but got %d", len(got)) + } + if expected != got[0] { + t.Errorf("keyword didn't match, expected '%s' got '%s'", expected, got[0]) } } } diff --git a/tar.go b/tar.go index 7f7318e..e9599e6 100644 --- a/tar.go +++ b/tar.go @@ -5,11 +5,11 @@ import ( "fmt" "io" "io/ioutil" - "log" "os" "path/filepath" "strings" + "github.com/Sirupsen/logrus" "github.com/vbatts/go-mtree/pkg/govis" ) @@ -144,16 +144,16 @@ hdrloop: // Keep track of which files are hardlinks so we can resolve them later if hdr.Typeflag == tar.TypeLink { - linkFunc := KeywordFuncs["link"] - kv, err := linkFunc(hdr.Name, hdr.FileInfo(), nil) + keyFunc := KeywordFuncs["link"] + kvs, err := keyFunc(hdr.Name, hdr.FileInfo(), nil) if err != nil { - log.Println(err) - break + logrus.Warn(err) + break // XXX is breaking an okay thing to do here? } - linkname, err := govis.Unvis(KeyVal(kv).Value(), DefaultVisFlags) + linkname, err := govis.Unvis(KeyVal(kvs[0]).Value(), DefaultVisFlags) if err != nil { - log.Println(err) - break + logrus.Warn(err) + break // XXX is breaking an okay thing to do here? } if _, ok := ts.hardlinks[linkname]; !ok { ts.hardlinks[linkname] = []string{hdr.Name} @@ -164,19 +164,19 @@ hdrloop: // now collect keywords on the file for _, keyword := range ts.keywords { - if keyFunc, ok := KeywordFuncs[keyword]; ok { + if keyFunc, ok := KeywordFuncs[keyword.Prefix()]; ok { // We can't extract directories on to disk, so "size" keyword // is irrelevant for now if hdr.FileInfo().IsDir() && keyword == "size" { continue } - val, err := keyFunc(hdr.Name, hdr.FileInfo(), tmpFile) + kvs, err := keyFunc(hdr.Name, hdr.FileInfo(), tmpFile) if err != nil { ts.setErr(err) } // for good measure, check that we actually get a value for a keyword - if val != "" { - e.Keywords = append(e.Keywords, val) + if len(kvs) > 0 && kvs[0] != "" { + e.Keywords = append(e.Keywords, kvs[0]) } // don't forget to reset the reader @@ -196,13 +196,15 @@ hdrloop: Type: SpecialType, } for _, setKW := range SetKeywords { - if keyFunc, ok := KeywordFuncs[setKW]; ok { - val, err := keyFunc(hdr.Name, hdr.FileInfo(), tmpFile) + if keyFunc, ok := KeywordFuncs[setKW.Prefix()]; ok { + kvs, err := keyFunc(hdr.Name, hdr.FileInfo(), tmpFile) if err != nil { ts.setErr(err) } - if val != "" { - s.Keywords = append(s.Keywords, val) + for _, kv := range kvs { + if kv != "" { + s.Keywords = append(s.Keywords, kv) + } } if _, err := tmpFile.Seek(0, 0); err != nil { tmpFile.Close() @@ -383,7 +385,7 @@ func resolveHardlinks(root *Entry, hardlinks map[string][]string, countlinks boo if seen, ok := originals[base]; !ok { basefile = root.Find(base) if basefile == nil { - log.Printf("%s does not exist in this tree\n", base) + logrus.Printf("%s does not exist in this tree\n", base) continue } originals[base] = basefile @@ -393,7 +395,7 @@ func resolveHardlinks(root *Entry, hardlinks map[string][]string, countlinks boo for _, link := range links { linkfile := root.Find(link) if linkfile == nil { - log.Printf("%s does not exist in this tree\n", link) + logrus.Printf("%s does not exist in this tree\n", link) continue } linkfile.Keywords = basefile.Keywords diff --git a/update.go b/update.go index ec93e02..d13d394 100644 --- a/update.go +++ b/update.go @@ -3,6 +3,8 @@ package mtree import ( "os" "sort" + + "github.com/Sirupsen/logrus" ) // DefaultUpdateKeywords is the default set of keywords that can take updates to the files on disk @@ -11,7 +13,7 @@ var DefaultUpdateKeywords = []Keyword{ "gid", "mode", "time", - // TODO xattr + "xattr", } // Update attempts to set the attributes of root directory path, given the values of `keywords` in dh DirectoryHierarchy. @@ -36,7 +38,7 @@ func Update(root string, dh *DirectoryHierarchy, keywords []Keyword, fs FsEval) } else if e.Name == "/unset" { creator.curSet = nil } - Debugf("%#v", e) + logrus.Debugf("%#v", e) continue case RelativeType, FullType: e.Set = creator.curSet @@ -46,20 +48,21 @@ func Update(root string, dh *DirectoryHierarchy, keywords []Keyword, fs FsEval) } // filter the keywords to update on the file, from the keywords available for this entry: - var toCheck []KeyVal - toCheck = keyvalSelector(e.AllKeys(), keywords) - Debugf("toCheck(%q): %v", pathname, toCheck) + var kvToUpdate []KeyVal + kvToUpdate = keyvalSelector(e.AllKeys(), keywords) + logrus.Debugf("kvToUpdate(%q): %#v", pathname, kvToUpdate) - for _, kv := range toCheck { - if !InKeywordSlice(kv.Keyword(), keywords) { + for _, kv := range kvToUpdate { + if !InKeywordSlice(kv.Keyword().Prefix(), keywordPrefixes(keywords)) { continue } - ukFunc, ok := UpdateKeywordFuncs[kv.Keyword()] + logrus.Debugf("finding function for %q (%q)", kv.Keyword(), kv.Keyword().Prefix()) + ukFunc, ok := UpdateKeywordFuncs[kv.Keyword().Prefix()] if !ok { - Debugf("no UpdateKeywordFunc for %s; skipping", kv.Keyword()) + logrus.Debugf("no UpdateKeywordFunc for %s; skipping", kv.Keyword()) continue } - if _, err := ukFunc(pathname, kv.Value()); err != nil { + if _, err := ukFunc(kv.Keyword(), pathname, kv.Value()); err != nil { results = append(results, InodeDelta{ diff: ErrorDifference, path: pathname, diff --git a/update_linux_test.go b/update_linux_test.go new file mode 100644 index 0000000..42254a8 --- /dev/null +++ b/update_linux_test.go @@ -0,0 +1,94 @@ +package mtree + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/vbatts/go-mtree/xattr" +) + +func init() { + //logrus.SetLevel(logrus.DebugLevel) +} + +func TestXattrUpdate(t *testing.T) { + content := []byte("I know half of you half as well as I ought to") + // a bit dirty to create/destory a directory in cwd, but often /tmp is + // mounted tmpfs and doesn't support xattrs + dir, err := ioutil.TempDir(".", "test.xattr.restore.") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) // clean up + + tmpfn := filepath.Join(dir, "tmpfile") + if err := ioutil.WriteFile(tmpfn, content, 0666); err != nil { + t.Fatal(err) + } + + if err := xattr.Set(dir, "user.test", []byte("directory")); err != nil { + t.Skip(fmt.Sprintf("skipping: %q does not support xattrs", dir)) + } + if err := xattr.Set(tmpfn, "user.test", []byte("regular file")); err != nil { + t.Fatal(err) + } + + // Walk this tempdir + dh, err := Walk(dir, nil, append(DefaultKeywords, []Keyword{"xattr", "sha1"}...), nil) + if err != nil { + t.Fatal(err) + } + + // Now check that we're sane + res, err := Check(dir, dh, nil, nil) + if err != nil { + t.Fatal(err) + } + if len(res) != 0 { + t.Errorf("expecting no failures, but got %q", res) + } + + if err := xattr.Set(tmpfn, "user.test", []byte("let it fly")); err != nil { + t.Fatal(err) + } + + // Now check that we fail the check + res, err = Check(dir, dh, nil, nil) + if err != nil { + t.Fatal(err) + } + if len(res) == 0 { + t.Error("expected failures (like xattrs), but got none") + } + + // restore the xattrs to original + res, err = Update(dir, dh, append(DefaultUpdateKeywords, "xattr"), nil) + if err != nil { + t.Error(err) + } + if len(res) != 0 { + t.Errorf("expecting no failures, but got %q", res) + } + + // Now check that we're sane again + res, err = Check(dir, dh, nil, nil) + if err != nil { + t.Fatal(err) + } + if len(res) != 0 { + // pretty this shit up + buf, err := json.MarshalIndent(res, "", " ") + if err != nil { + t.Errorf("expecting no failures, but got %q", res) + } else { + t.Errorf("expecting no failures, but got %s", string(buf)) + } + } + + // TODO make a test for xattr here. Likely in the user space for privileges. Even still this may be prone to error for some tmpfs don't act right with xattrs. :-\ + // I'd hate to have to t.Skip() a test rather than fail alltogether. +} diff --git a/update_test.go b/update_test.go index 6197e8a..70a3f73 100644 --- a/update_test.go +++ b/update_test.go @@ -97,8 +97,9 @@ func TestUpdate(t *testing.T) { buf, err := json.MarshalIndent(res, "", " ") if err != nil { t.Errorf("%#v", res) + } else { + t.Error(string(buf)) } - t.Error(string(buf)) } } diff --git a/updatefuncs.go b/updatefuncs.go index 1a5867e..ed44714 100644 --- a/updatefuncs.go +++ b/updatefuncs.go @@ -6,12 +6,14 @@ import ( "strconv" "strings" "time" + + "github.com/Sirupsen/logrus" ) // UpdateKeywordFunc is the signature for a function that will restore a file's // attributes. Where path is relative path to the file, and value to be // restored to. -type UpdateKeywordFunc func(path string, value string) (os.FileInfo, error) +type UpdateKeywordFunc func(keyword Keyword, path string, value string) (os.FileInfo, error) // UpdateKeywordFuncs is the registered list of functions to update file attributes. // Keyed by the keyword as it would show up in the manifest @@ -21,9 +23,10 @@ var UpdateKeywordFuncs = map[Keyword]UpdateKeywordFunc{ "tar_time": tartimeUpdateKeywordFunc, "uid": uidUpdateKeywordFunc, "gid": gidUpdateKeywordFunc, + "xattr": xattrUpdateKeywordFunc, } -func uidUpdateKeywordFunc(path, value string) (os.FileInfo, error) { +func uidUpdateKeywordFunc(keyword Keyword, path, value string) (os.FileInfo, error) { uid, err := strconv.Atoi(value) if err != nil { return nil, err @@ -34,7 +37,7 @@ func uidUpdateKeywordFunc(path, value string) (os.FileInfo, error) { return os.Lstat(path) } -func gidUpdateKeywordFunc(path, value string) (os.FileInfo, error) { +func gidUpdateKeywordFunc(keyword Keyword, path, value string) (os.FileInfo, error) { gid, err := strconv.Atoi(value) if err != nil { return nil, err @@ -45,12 +48,12 @@ func gidUpdateKeywordFunc(path, value string) (os.FileInfo, error) { return os.Lstat(path) } -func modeUpdateKeywordFunc(path, value string) (os.FileInfo, error) { +func modeUpdateKeywordFunc(keyword Keyword, path, value string) (os.FileInfo, error) { vmode, err := strconv.ParseInt(value, 8, 32) if err != nil { return nil, err } - Debugf("path: %q, value: %q, vmode: %o", path, value, vmode) + logrus.Debugf("path: %q, value: %q, vmode: %o", path, value, vmode) if err := os.Chmod(path, os.FileMode(vmode)); err != nil { return nil, err } @@ -60,7 +63,7 @@ func modeUpdateKeywordFunc(path, value string) (os.FileInfo, error) { // since tar_time will only be second level precision, then when restoring the // filepath from a tar_time, then compare the seconds first and only Chtimes if // the seconds value is different. -func tartimeUpdateKeywordFunc(path, value string) (os.FileInfo, error) { +func tartimeUpdateKeywordFunc(keyword Keyword, path, value string) (os.FileInfo, error) { info, err := os.Lstat(path) if err != nil { return nil, err @@ -89,7 +92,7 @@ func tartimeUpdateKeywordFunc(path, value string) (os.FileInfo, error) { } // this is nano second precision -func timeUpdateKeywordFunc(path, value string) (os.FileInfo, error) { +func timeUpdateKeywordFunc(keyword Keyword, path, value string) (os.FileInfo, error) { v := strings.SplitN(value, ".", 2) if len(v) != 2 { return nil, fmt.Errorf("expected a number like 1469104727.871937272") @@ -98,7 +101,7 @@ func timeUpdateKeywordFunc(path, value string) (os.FileInfo, error) { if err != nil { return nil, fmt.Errorf("expected nano seconds, but got %q", v[0]+v[1]) } - Debugf("arg: %q; nsec: %q", v[0]+v[1], nsec) + logrus.Debugf("arg: %q; nsec: %q", v[0]+v[1], nsec) vtime := time.Unix(0, nsec) if err := os.Chtimes(path, vtime, vtime); err != nil { diff --git a/updatefuncs_linux.go b/updatefuncs_linux.go new file mode 100644 index 0000000..7779ad8 --- /dev/null +++ b/updatefuncs_linux.go @@ -0,0 +1,21 @@ +// +build linux + +package mtree + +import ( + "encoding/base64" + "os" + + "github.com/vbatts/go-mtree/xattr" +) + +func xattrUpdateKeywordFunc(keyword Keyword, path, value string) (os.FileInfo, error) { + buf, err := base64.StdEncoding.DecodeString(value) + if err != nil { + return nil, err + } + if err := xattr.Set(path, keyword.Suffix(), buf); err != nil { + return nil, err + } + return os.Lstat(path) +} diff --git a/updatefuncs_unsupported.go b/updatefuncs_unsupported.go new file mode 100644 index 0000000..b378b07 --- /dev/null +++ b/updatefuncs_unsupported.go @@ -0,0 +1,9 @@ +// +build !linux + +package mtree + +import "os" + +func xattrUpdateKeywordFunc(keyword Keyword, path, value string) (os.FileInfo, error) { + return os.Lstat(path) +} diff --git a/walk.go b/walk.go index 16cafe1..56b93dc 100644 --- a/walk.go +++ b/walk.go @@ -101,15 +101,19 @@ func Walk(root string, excludes []ExcludeFunc, keywords []Keyword, fsEval FsEval defer fh.Close() r = fh } - keywordFunc, ok := KeywordFuncs[keyword] + keyFunc, ok := KeywordFuncs[keyword.Prefix()] if !ok { - return fmt.Errorf("Unknown keyword %q for file %q", keyword, path) + return fmt.Errorf("Unknown keyword %q for file %q", keyword.Prefix(), path) } - if str, err := creator.fs.KeywordFunc(keywordFunc)(path, info, r); err == nil && str != "" { - e.Keywords = append(e.Keywords, str) - } else if err != nil { + kvs, err := creator.fs.KeywordFunc(keyFunc)(path, info, r) + if err != nil { return err } + for _, kv := range kvs { + if kv != "" { + e.Keywords = append(e.Keywords, kv) + } + } return nil }() if err != nil { @@ -132,16 +136,18 @@ func Walk(root string, excludes []ExcludeFunc, keywords []Keyword, fsEval FsEval defer fh.Close() r = fh } - keywordFunc, ok := KeywordFuncs[keyword] + keyFunc, ok := KeywordFuncs[keyword.Prefix()] if !ok { - return fmt.Errorf("Unknown keyword %q for file %q", keyword, path) + return fmt.Errorf("Unknown keyword %q for file %q", keyword.Prefix(), path) } - str, err := creator.fs.KeywordFunc(keywordFunc)(path, info, r) + kvs, err := creator.fs.KeywordFunc(keyFunc)(path, info, r) if err != nil { return err } - if str != "" { - klist = append(klist, str) + for _, kv := range kvs { + if kv != "" { + klist = append(klist, kv) + } } return nil }() @@ -190,16 +196,18 @@ func Walk(root string, excludes []ExcludeFunc, keywords []Keyword, fsEval FsEval defer fh.Close() r = fh } - keywordFunc, ok := KeywordFuncs[keyword] + keyFunc, ok := KeywordFuncs[keyword.Prefix()] if !ok { - return fmt.Errorf("Unknown keyword %q for file %q", keyword, path) + return fmt.Errorf("Unknown keyword %q for file %q", keyword.Prefix(), path) } - str, err := creator.fs.KeywordFunc(keywordFunc)(path, info, r) + kvs, err := creator.fs.KeywordFunc(keyFunc)(path, info, r) if err != nil { return err } - if str != "" && !inKeyValSlice(str, creator.curSet.Keywords) { - e.Keywords = append(e.Keywords, str) + for _, kv := range kvs { + if kv != "" && !inKeyValSlice(kv, creator.curSet.Keywords) { + e.Keywords = append(e.Keywords, kv) + } } return nil }()