diff --git a/check.go b/check.go index 44c0ea2..65e1128 100644 --- a/check.go +++ b/check.go @@ -50,7 +50,8 @@ func Check(root string, dh *DirectoryHierarchy, keywords []string) (*Result, err creator.curSet = nil } case RelativeType, FullType: - info, err := os.Lstat(filepath.Join(root, e.Path())) + filename := filepath.Join(root, e.Path()) + info, err := os.Lstat(filename) if err != nil { return nil, err } @@ -70,10 +71,16 @@ func Check(root string, dh *DirectoryHierarchy, keywords []string) (*Result, err if !ok { return nil, fmt.Errorf("Unknown keyword %q for file %q", kv.Keyword(), e.Path()) } - curKeyVal, err := keywordFunc(filepath.Join(root, e.Path()), info) + fh, err := os.Open(filename) if err != nil { return nil, err } + curKeyVal, err := keywordFunc(filename, info, fh) + if err != nil { + fh.Close() + return nil, err + } + fh.Close() if string(kv) != curKeyVal { failure := Failure{Path: e.Path(), Keyword: kv.Keyword(), Expected: kv.Value(), Got: KeyVal(curKeyVal).Value()} result.Failures = append(result.Failures, failure) diff --git a/creator.go b/creator.go new file mode 100644 index 0000000..2a23a22 --- /dev/null +++ b/creator.go @@ -0,0 +1,9 @@ +package mtree + +// dhCreator is used in when building a DirectoryHierarchy +type dhCreator struct { + DH *DirectoryHierarchy + curSet *Entry + curDir *Entry + curEnt *Entry +} diff --git a/entry.go b/entry.go new file mode 100644 index 0000000..32b7678 --- /dev/null +++ b/entry.go @@ -0,0 +1,80 @@ +package mtree + +import ( + "fmt" + "path/filepath" + "strings" +) + +type byPos []Entry + +func (bp byPos) Len() int { return len(bp) } +func (bp byPos) Less(i, j int) bool { return bp[i].Pos < bp[j].Pos } +func (bp byPos) Swap(i, j int) { bp[i], bp[j] = bp[j], bp[i] } + +// Entry is each component of content in the mtree spec file +type Entry struct { + Parent *Entry // up + Children []*Entry // down + Prev, Next *Entry // left, right + Set *Entry // current `/set` for additional keywords + Pos int // order in the spec + Raw string // file or directory name + Name string // file or directory name + Keywords []string // TODO(vbatts) maybe a keyword typed set of values? + Type EntryType +} + +// Path provides the full path of the file, despite RelativeType or FullType +func (e Entry) Path() string { + if e.Parent == nil || e.Type == FullType { + return filepath.Clean(e.Name) + } + return filepath.Clean(filepath.Join(e.Parent.Path(), e.Name)) +} + +func (e Entry) String() string { + if e.Raw != "" { + return e.Raw + } + if e.Type == BlankType { + return "" + } + if e.Type == DotDotType { + return e.Name + } + // TODO(vbatts) if type is RelativeType and a keyword of not type=dir + if e.Type == SpecialType || e.Type == FullType || inSlice("type=dir", e.Keywords) { + return fmt.Sprintf("%s %s", e.Name, strings.Join(e.Keywords, " ")) + } + return fmt.Sprintf(" %s %s", e.Name, strings.Join(e.Keywords, " ")) +} + +// EntryType are the formats of lines in an mtree spec file +type EntryType int + +// The types of lines to be found in an mtree spec file +const ( + SignatureType EntryType = iota // first line of the file, like `#mtree v2.0` + BlankType // blank lines are ignored + CommentType // Lines beginning with `#` are ignored + SpecialType // line that has `/` prefix issue a "special" command (currently only /set and /unset) + RelativeType // if the first white-space delimited word does not have a '/' in it. Options/keywords are applied. + DotDotType // .. - A relative path step. keywords/options are ignored + FullType // if the first word on the line has a `/` after the first character, it interpretted as a file pathname with options +) + +// String returns the name of the EntryType +func (et EntryType) String() string { + return typeNames[et] +} + +var typeNames = map[EntryType]string{ + SignatureType: "SignatureType", + BlankType: "BlankType", + CommentType: "CommentType", + SpecialType: "SpecialType", + RelativeType: "RelativeType", + DotDotType: "DotDotType", + FullType: "FullType", +} diff --git a/hierarchy.go b/hierarchy.go index b591773..9f66056 100644 --- a/hierarchy.go +++ b/hierarchy.go @@ -1,11 +1,8 @@ package mtree import ( - "fmt" "io" - "path/filepath" "sort" - "strings" ) // DirectoryHierarchy is the mapped structure for an mtree directory hierarchy @@ -27,75 +24,3 @@ func (dh DirectoryHierarchy) WriteTo(w io.Writer) (n int64, err error) { } return sum, nil } - -type byPos []Entry - -func (bp byPos) Len() int { return len(bp) } -func (bp byPos) Less(i, j int) bool { return bp[i].Pos < bp[j].Pos } -func (bp byPos) Swap(i, j int) { bp[i], bp[j] = bp[j], bp[i] } - -// Entry is each component of content in the mtree spec file -type Entry struct { - Parent, Child *Entry // up, down - Prev, Next *Entry // left, right - Set *Entry // current `/set` for additional keywords - Pos int // order in the spec - Raw string // file or directory name - Name string // file or directory name - Keywords []string // TODO(vbatts) maybe a keyword typed set of values? - Type EntryType -} - -// Path provides the full path of the file, despite RelativeType or FullType -func (e Entry) Path() string { - if e.Parent == nil || e.Type == FullType { - return filepath.Clean(e.Name) - } - return filepath.Clean(filepath.Join(e.Parent.Path(), e.Name)) -} - -func (e Entry) String() string { - if e.Raw != "" { - return e.Raw - } - if e.Type == BlankType { - return "" - } - if e.Type == DotDotType { - return e.Name - } - // TODO(vbatts) if type is RelativeType and a keyword of not type=dir - if e.Type == SpecialType || e.Type == FullType || inSlice("type=dir", e.Keywords) { - return fmt.Sprintf("%s %s", e.Name, strings.Join(e.Keywords, " ")) - } - return fmt.Sprintf(" %s %s", e.Name, strings.Join(e.Keywords, " ")) -} - -// EntryType are the formats of lines in an mtree spec file -type EntryType int - -// The types of lines to be found in an mtree spec file -const ( - SignatureType EntryType = iota // first line of the file, like `#mtree v2.0` - BlankType // blank lines are ignored - CommentType // Lines beginning with `#` are ignored - SpecialType // line that has `/` prefix issue a "special" command (currently only /set and /unset) - RelativeType // if the first white-space delimited word does not have a '/' in it. Options/keywords are applied. - DotDotType // .. - A relative path step. keywords/options are ignored - FullType // if the first word on the line has a `/` after the first character, it interpretted as a file pathname with options -) - -// String returns the name of the EntryType -func (et EntryType) String() string { - return typeNames[et] -} - -var typeNames = map[EntryType]string{ - SignatureType: "SignatureType", - BlankType: "BlankType", - CommentType: "CommentType", - SpecialType: "SpecialType", - RelativeType: "RelativeType", - DotDotType: "DotDotType", - FullType: "FullType", -} diff --git a/keywords.go b/keywords.go index 8b01c80..8e199ce 100644 --- a/keywords.go +++ b/keywords.go @@ -1,6 +1,7 @@ package mtree import ( + "archive/tar" "crypto/md5" "crypto/sha1" "crypto/sha256" @@ -17,7 +18,10 @@ import ( // KeywordFunc is the type of a function called on each file to be included in // a DirectoryHierarchy, that will produce the string output of the keyword to // be included for the file entry. Otherwise, empty string. -type KeywordFunc func(path string, info os.FileInfo) (string, error) +// 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) (string, error) // KeyVal is a "keyword=value" type KeyVal string @@ -165,55 +169,46 @@ var ( ) var ( - modeKeywordFunc = func(path string, info os.FileInfo) (string, error) { + modeKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (string, error) { return fmt.Sprintf("mode=%#o", info.Mode().Perm()), nil } - sizeKeywordFunc = func(path string, info os.FileInfo) (string, error) { + sizeKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (string, error) { return fmt.Sprintf("size=%d", info.Size()), nil } - cksumKeywordFunc = func(path string, info os.FileInfo) (string, error) { + cksumKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (string, error) { if !info.Mode().IsRegular() { return "", nil } - - fh, err := os.Open(path) - if err != nil { - return "", err - } - defer fh.Close() - sum, _, err := cksum(fh) + sum, _, err := cksum(r) if err != nil { return "", err } return fmt.Sprintf("cksum=%d", sum), nil } hasherKeywordFunc = func(name string, newHash func() hash.Hash) KeywordFunc { - return func(path string, info os.FileInfo) (string, error) { + return func(path string, info os.FileInfo, r io.Reader) (string, error) { if !info.Mode().IsRegular() { return "", nil } - - fh, err := os.Open(path) - if err != nil { - return "", err - } - defer fh.Close() - h := newHash() - if _, err := io.Copy(h, fh); err != nil { + if _, err := io.Copy(h, r); err != nil { return "", err } return fmt.Sprintf("%s=%x", name, h.Sum(nil)), nil } } - timeKeywordFunc = func(path string, info os.FileInfo) (string, error) { + timeKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (string, error) { t := info.ModTime().UnixNano() if t == 0 { return "time=0.000000000", nil } return fmt.Sprintf("time=%d.%9.9d", (t / 1e9), (t % (t / 1e9))), nil } - linkKeywordFunc = func(path string, info os.FileInfo) (string, error) { + linkKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (string, error) { + if sys, ok := info.Sys().(*tar.Header); ok { + return sys.Linkname, nil + } + if info.Mode()&os.ModeSymlink != 0 { str, err := os.Readlink(path) if err != nil { @@ -223,7 +218,7 @@ var ( } return "", nil } - typeKeywordFunc = func(path string, info os.FileInfo) (string, error) { + typeKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (string, error) { if info.Mode().IsDir() { return "type=dir", nil } diff --git a/keywords_linux.go b/keywords_linux.go index 5683dfd..26bfc40 100644 --- a/keywords_linux.go +++ b/keywords_linux.go @@ -3,8 +3,10 @@ package mtree import ( + "archive/tar" "crypto/sha1" "fmt" + "io" "os" "os/user" "strings" @@ -14,7 +16,11 @@ import ( ) var ( - unameKeywordFunc = func(path string, info os.FileInfo) (string, error) { + unameKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (string, error) { + if hdr, ok := info.Sys().(*tar.Header); ok { + return 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 { @@ -22,19 +28,40 @@ var ( } return fmt.Sprintf("uname=%s", u.Username), nil } - uidKeywordFunc = func(path string, info os.FileInfo) (string, error) { + uidKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (string, error) { + if hdr, ok := info.Sys().(*tar.Header); ok { + return fmt.Sprintf("uid=%d", hdr.Uid), nil + } stat := info.Sys().(*syscall.Stat_t) return fmt.Sprintf("uid=%d", stat.Uid), nil } - gidKeywordFunc = func(path string, info os.FileInfo) (string, error) { - stat := info.Sys().(*syscall.Stat_t) - return fmt.Sprintf("gid=%d", stat.Gid), nil + gidKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (string, error) { + if hdr, ok := info.Sys().(*tar.Header); ok { + return fmt.Sprintf("gid=%d", hdr.Gid), nil + } + if stat, ok := info.Sys().(*syscall.Stat_t); ok { + return fmt.Sprintf("gid=%d", stat.Gid), nil + } + return "", nil } - nlinkKeywordFunc = func(path string, info os.FileInfo) (string, error) { - stat := info.Sys().(*syscall.Stat_t) - return fmt.Sprintf("nlink=%d", stat.Nlink), nil + nlinkKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (string, error) { + if stat, ok := info.Sys().(*syscall.Stat_t); ok { + return fmt.Sprintf("nlink=%d", stat.Nlink), nil + } + return "", nil } - xattrKeywordFunc = func(path string, info os.FileInfo) (string, error) { + xattrKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (string, error) { + if hdr, ok := info.Sys().(*tar.Header); ok { + if len(hdr.Xattrs) == 0 { + return "", nil + } + klist := []string{} + for k, v := range hdr.Xattrs { + klist = append(klist, fmt.Sprintf("xattr.%s=%x", k, sha1.Sum([]byte(v)))) + } + return strings.Join(klist, " "), nil + } + xlist, err := xattr.List(path) if err != nil { return "", err diff --git a/keywords_unsupported.go b/keywords_unsupported.go index ba61845..3234532 100644 --- a/keywords_unsupported.go +++ b/keywords_unsupported.go @@ -2,22 +2,36 @@ package mtree -import "os" +import ( + "archive/tar" + "fmt" + "io" + "os" +) var ( - unameKeywordFunc = func(path string, info os.FileInfo) (string, error) { + unameKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (string, error) { + if hdr, ok := info.Sys().(*tar.Header); ok { + return fmt.Sprintf("uname=%s", hdr.Uname), nil + } return "", nil } - uidKeywordFunc = func(path string, info os.FileInfo) (string, error) { + uidKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (string, error) { + if hdr, ok := info.Sys().(*tar.Header); ok { + return fmt.Sprintf("uid=%d", hdr.Uid), nil + } return "", nil } - gidKeywordFunc = func(path string, info os.FileInfo) (string, error) { + gidKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (string, error) { + if hdr, ok := info.Sys().(*tar.Header); ok { + return fmt.Sprintf("gid=%d", hdr.Gid), nil + } return "", nil } - nlinkKeywordFunc = func(path string, info os.FileInfo) (string, error) { + nlinkKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (string, error) { return "", nil } - xattrKeywordFunc = func(path string, info os.FileInfo) (string, error) { + xattrKeywordFunc = func(path string, info os.FileInfo, r io.Reader) (string, error) { return "", nil } ) diff --git a/tar.go b/tar.go new file mode 100644 index 0000000..97c6a33 --- /dev/null +++ b/tar.go @@ -0,0 +1,181 @@ +package mtree + +import ( + "archive/tar" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" +) + +type Streamer interface { + io.ReadCloser + Hierarchy() (*DirectoryHierarchy, error) +} + +func NewTarStreamer(r io.Reader, keywords []string) Streamer { + pR, pW := io.Pipe() + ts := &tarStream{ + pipeReader: pR, + pipeWriter: pW, + creator: dhCreator{DH: &DirectoryHierarchy{}}, + teeReader: io.TeeReader(r, pW), + tarReader: tar.NewReader(pR), + keywords: keywords, + } + go ts.readHeaders() // I don't like this + return ts +} + +type tarStream struct { + creator dhCreator + pipeReader *io.PipeReader + pipeWriter *io.PipeWriter + teeReader io.Reader + tarReader *tar.Reader + keywords []string + err error +} + +func (ts *tarStream) readHeaders() { + // We have to start with the directory we're in, and anything beyond these + // items is determined at the time a tar is extracted. + e := Entry{ + Name: ".", + Keywords: []string{"size=0", "type=dir"}, + } + ts.creator.curDir = &e + ts.creator.DH.Entries = append(ts.creator.DH.Entries, e) + for { + hdr, err := ts.tarReader.Next() + if err != nil { + ts.pipeReader.CloseWithError(err) + return + } + + // Because the content of the file may need to be read by several + // KeywordFuncs, it needs to be an io.Seeker as well. So, just reading from + // ts.tarReader is not enough. + tmpFile, err := ioutil.TempFile("", "ts.payload.") + if err != nil { + ts.pipeReader.CloseWithError(err) + return + } + // for good measure + if err := tmpFile.Chmod(0600); err != nil { + tmpFile.Close() + os.Remove(tmpFile.Name()) + ts.pipeReader.CloseWithError(err) + return + } + if _, err := io.Copy(tmpFile, ts.tarReader); err != nil { + tmpFile.Close() + os.Remove(tmpFile.Name()) + ts.pipeReader.CloseWithError(err) + return + } + + // Alright, it's either file or directory + e := Entry{ + Name: filepath.Base(hdr.Name), + Pos: len(ts.creator.DH.Entries), + Type: RelativeType, + } + // now collect keywords on the file + for _, keyword := range ts.keywords { + if keyFunc, ok := KeywordFuncs[keyword]; ok { + val, err := keyFunc(hdr.Name, hdr.FileInfo(), tmpFile) + if err != nil { + ts.setErr(err) + } + e.Keywords = append(e.Keywords, val) + + // don't forget to reset the reader + if _, err := tmpFile.Seek(0, 0); err != nil { + tmpFile.Close() + os.Remove(tmpFile.Name()) + ts.pipeReader.CloseWithError(err) + return + } + } + } + tmpFile.Close() + os.Remove(tmpFile.Name()) + + // compare directories, to determine parent of the current entry + cd := compareDir(filepath.Dir(hdr.Name), ts.creator.curDir.Path()) + switch { + case cd == sameDir: + e.Parent = ts.creator.curDir + if e.Parent != nil { + e.Parent.Children = append(e.Parent.Children, &e) + } + case cd == parentDir: + e.Parent = ts.creator.curDir.Parent + if e.Parent != nil { + e.Parent.Children = append(e.Parent.Children, &e) + } + } + + if hdr.FileInfo().IsDir() { + ts.creator.curDir = &e + } + // TODO getting the parent child relationship of these entries! + if hdr.FileInfo().IsDir() { + log.Println(strings.Split(hdr.Name, "/"), strings.Split(ts.creator.curDir.Path(), "/")) + } + + ts.creator.DH.Entries = append(ts.creator.DH.Entries, e) + + // Now is the wacky part of building out the entries. Since we can not + // control how the archive was assembled, can only take in the order given. + // Using `/set` will be tough. Hopefully i can do the directory stepping + // with relative paths, but even then I may get a new directory, and not + // the files first, but its directories first. :-\ + } +} + +type relationship int + +const ( + unknownDir relationship = iota + sameDir + childDir + parentDir +) + +func compareDir(curDir, prevDir string) relationship { + curDir = filepath.Clean(curDir) + prevDir = filepath.Clean(prevDir) + if curDir == prevDir { + return sameDir + } + if filepath.Dir(curDir) == prevDir { + return childDir + } + if curDir == filepath.Dir(prevDir) { + return parentDir + } + return unknownDir +} + +func (ts *tarStream) setErr(err error) { + ts.err = err +} + +func (ts *tarStream) Read(p []byte) (n int, err error) { + return ts.teeReader.Read(p) +} + +func (ts *tarStream) Close() error { + return ts.pipeReader.Close() +} + +func (ts *tarStream) Hierarchy() (*DirectoryHierarchy, error) { + if ts.err != nil && ts.err != io.EOF { + return nil, ts.err + } + return ts.creator.DH, nil +} diff --git a/tar_test.go b/tar_test.go new file mode 100644 index 0000000..547e070 --- /dev/null +++ b/tar_test.go @@ -0,0 +1,119 @@ +package mtree + +import ( + "archive/tar" + "bytes" + "io" + "io/ioutil" + "os" + "testing" +) + +func ExampleTar() { + fh, err := os.Open("./testdata/test.tar") + if err != nil { + // handle error ... + } + str := NewTarStreamer(fh, nil) + if err := extractTar("/tmp/dir", str); err != nil { + // handle error ... + } + + dh, err := str.Hierarchy() + if err != nil { + // handle error ... + } + + res, err := Check("/tmp/dir/", dh, nil) + if err != nil { + // handle error ... + } + if len(res.Failures) > 0 { + // handle validation issue ... + } +} +func extractTar(root string, tr io.Reader) error { + return nil +} + +func TestTar(t *testing.T) { + /* + data, err := makeTarStream() + if err != nil { + t.Fatal(err) + } + buf := bytes.NewBuffer(data) + str := NewTarStreamer(buf, append(DefaultKeywords, "sha1")) + */ + fh, err := os.Open("./testdata/test.tar") + if err != nil { + t.Fatal(err) + } + str := NewTarStreamer(fh, append(DefaultKeywords, "sha1")) + + if _, err := io.Copy(ioutil.Discard, str); err != nil && err != io.EOF { + t.Fatal(err) + } + if err := str.Close(); err != nil { + t.Fatal(err) + } + defer fh.Close() + + /* + fi, err := fh.Stat() + if err != nil { + t.Fatal(err) + } + if i != fi.Size() { + t.Errorf("expected length %d; got %d", fi.Size(), i) + } + */ + dh, err := str.Hierarchy() + if err != nil { + t.Fatal(err) + } + if dh == nil { + t.Fatal("expected a DirectoryHierarchy struct, but got nil") + } + //dh.WriteTo(os.Stdout) +} + +// minimal tar archive stream that mimics what is in ./testdata/test.tar +func makeTarStream() ([]byte, error) { + buf := new(bytes.Buffer) + + // Create a new tar archive. + tw := tar.NewWriter(buf) + + // Add some files to the archive. + var files = []struct { + Name, Body string + Mode int64 + Type byte + Xattrs map[string]string + }{ + {"x/", "", 0755, '5', nil}, + {"x/files", "howdy\n", 0644, '0', nil}, + } + for _, file := range files { + hdr := &tar.Header{ + Name: file.Name, + Mode: file.Mode, + Size: int64(len(file.Body)), + Xattrs: file.Xattrs, + } + if err := tw.WriteHeader(hdr); err != nil { + return nil, err + } + if len(file.Body) > 0 { + if _, err := tw.Write([]byte(file.Body)); err != nil { + return nil, err + } + } + } + // Make sure to check the error on Close. + if err := tw.Close(); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/testdata/test.tar b/testdata/test.tar new file mode 100644 index 0000000..cc3de57 Binary files /dev/null and b/testdata/test.tar differ diff --git a/walk.go b/walk.go index a1ef434..a3f3930 100644 --- a/walk.go +++ b/walk.go @@ -1,6 +1,7 @@ package mtree import ( + "io" "os" "path/filepath" "sort" @@ -11,13 +12,6 @@ import ( // returns true, then the path is not included in the spec. type ExcludeFunc func(path string, info os.FileInfo) bool -type dhCreator struct { - DH *DirectoryHierarchy - curSet *Entry - curDir *Entry - curEnt *Entry -} - var defaultSetKeywords = []string{"type=file", "nlink=1", "flags=none", "mode=0664"} // Walk from root directory and assemble the DirectoryHierarchy. excludes @@ -68,9 +62,24 @@ func Walk(root string, exlcudes []ExcludeFunc, keywords []string) (*DirectoryHie Keywords: keywordSelector(defaultSetKeywords, keywords), } for _, keyword := range SetKeywords { - if str, err := KeywordFuncs[keyword](path, info); err == nil && str != "" { - e.Keywords = append(e.Keywords, str) - } else if err != nil { + err := func() error { + var r io.Reader + if info.Mode().IsRegular() { + fh, err := os.Open(path) + if err != nil { + return err + } + defer fh.Close() + r = fh + } + if str, err := KeywordFuncs[keyword](path, info, r); err == nil && str != "" { + e.Keywords = append(e.Keywords, str) + } else if err != nil { + return err + } + return nil + }() + if err != nil { return err } } @@ -80,9 +89,26 @@ func Walk(root string, exlcudes []ExcludeFunc, keywords []string) (*DirectoryHie // check the attributes of the /set keywords and re-set if changed klist := []string{} for _, keyword := range SetKeywords { - if str, err := KeywordFuncs[keyword](path, info); err == nil && str != "" { - klist = append(klist, str) - } else if err != nil { + err := func() error { + var r io.Reader + if info.Mode().IsRegular() { + fh, err := os.Open(path) + if err != nil { + return err + } + defer fh.Close() + r = fh + } + str, err := KeywordFuncs[keyword](path, info, r) + if err != nil { + return err + } + if str != "" { + klist = append(klist, str) + } + return nil + }() + if err != nil { return err } } @@ -114,11 +140,26 @@ func Walk(root string, exlcudes []ExcludeFunc, keywords []string) (*DirectoryHie Parent: creator.curDir, } for _, keyword := range keywords { - if str, err := KeywordFuncs[keyword](path, info); err == nil && str != "" { - if !inSlice(str, creator.curSet.Keywords) { + err := func() error { + var r io.Reader + if info.Mode().IsRegular() { + fh, err := os.Open(path) + if err != nil { + return err + } + defer fh.Close() + r = fh + } + str, err := KeywordFuncs[keyword](path, info, r) + if err != nil { + return err + } + if str != "" && !inSlice(str, creator.curSet.Keywords) { e.Keywords = append(e.Keywords, str) } - } else if err != nil { + return nil + }() + if err != nil { return err } }