diff --git a/keywordfunc.go b/keywordfunc.go index d23a26f..14ce44d 100644 --- a/keywordfunc.go +++ b/keywordfunc.go @@ -34,6 +34,7 @@ var ( "gid": gidKeywordFunc, // The file group as a numeric value "nlink": nlinkKeywordFunc, // The number of hard links the file is expected to have "uname": unameKeywordFunc, // The file owner as a symbolic name + "gname": gnameKeywordFunc, // The file group as a symbolic name "mode": modeKeywordFunc, // The current file's permissions as a numeric (octal) or symbolic value "cksum": cksumKeywordFunc, // The checksum of the file using the default algorithm specified by the cksum(1) utility "md5": hasherKeywordFunc("md5digest", md5.New), // The MD5 message digest of the file diff --git a/keywords_bsd.go b/keywordfuncs_bsd.go similarity index 81% rename from keywords_bsd.go rename to keywordfuncs_bsd.go index 43a8073..8de2608 100644 --- a/keywords_bsd.go +++ b/keywordfuncs_bsd.go @@ -29,6 +29,18 @@ var ( } return KeyVal(fmt.Sprintf("uname=%s", u.Username)), nil } + 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 + } + + stat := info.Sys().(*syscall.Stat_t) + g, err := lookupGroupID(fmt.Sprintf("%d", stat.Gid)) + if err != nil { + return emptyKV, err + } + return KeyVal(fmt.Sprintf("gname=%s", g.Name)), nil + } 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 diff --git a/keywordfuncs_linux.go b/keywordfuncs_linux.go index bab7a8d..c45e1ab 100644 --- a/keywordfuncs_linux.go +++ b/keywordfuncs_linux.go @@ -34,6 +34,18 @@ var ( } return KeyVal(fmt.Sprintf("uname=%s", u.Username)), nil } + 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 + } + + stat := info.Sys().(*syscall.Stat_t) + g, err := lookupGroupID(fmt.Sprintf("%d", stat.Gid)) + if err != nil { + return emptyKV, err + } + return KeyVal(fmt.Sprintf("gname=%s", g.Name)), nil + } 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 diff --git a/keywordfuncs_unsupported.go b/keywordfuncs_unsupported.go index e890ccb..a582f79 100644 --- a/keywordfuncs_unsupported.go +++ b/keywordfuncs_unsupported.go @@ -20,6 +20,12 @@ var ( } return emptyKV, nil } + 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 emptyKV, nil + } 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 diff --git a/lookup_new.go b/lookup_new.go new file mode 100644 index 0000000..c8baae7 --- /dev/null +++ b/lookup_new.go @@ -0,0 +1,9 @@ +// +build go1.7 + +package mtree + +import ( + "os/user" +) + +var lookupGroupID = user.LookupGroupId diff --git a/lookup_old.go b/lookup_old.go new file mode 100644 index 0000000..8c22e2b --- /dev/null +++ b/lookup_old.go @@ -0,0 +1,102 @@ +// +build !go1.7 + +package mtree + +import ( + "bufio" + "bytes" + "io" + "os" + "strconv" + "strings" +) + +const groupFile = "/etc/group" + +var colon = []byte{':'} + +// Group represents a grouping of users. +// +// On POSIX systems Gid contains a decimal number representing the group ID. +type Group struct { + Gid string // group ID + Name string // group name +} + +func lookupGroupID(id string) (*Group, error) { + f, err := os.Open(groupFile) + if err != nil { + return nil, err + } + defer f.Close() + return findGroupID(id, f) +} + +func findGroupID(id string, r io.Reader) (*Group, error) { + if v, err := readColonFile(r, matchGroupIndexValue(id, 2)); err != nil { + return nil, err + } else if v != nil { + return v.(*Group), nil + } + return nil, UnknownGroupIDError(id) +} + +// lineFunc returns a value, an error, or (nil, nil) to skip the row. +type lineFunc func(line []byte) (v interface{}, err error) + +// readColonFile parses r as an /etc/group or /etc/passwd style file, running +// fn for each row. readColonFile returns a value, an error, or (nil, nil) if +// the end of the file is reached without a match. +func readColonFile(r io.Reader, fn lineFunc) (v interface{}, err error) { + bs := bufio.NewScanner(r) + for bs.Scan() { + line := bs.Bytes() + // There's no spec for /etc/passwd or /etc/group, but we try to follow + // the same rules as the glibc parser, which allows comments and blank + // space at the beginning of a line. + line = bytes.TrimSpace(line) + if len(line) == 0 || line[0] == '#' { + continue + } + v, err = fn(line) + if v != nil || err != nil { + return + } + } + return nil, bs.Err() +} + +func matchGroupIndexValue(value string, idx int) lineFunc { + var leadColon string + if idx > 0 { + leadColon = ":" + } + substr := []byte(leadColon + value + ":") + return func(line []byte) (v interface{}, err error) { + if !bytes.Contains(line, substr) || bytes.Count(line, colon) < 3 { + return + } + // wheel:*:0:root + parts := strings.SplitN(string(line), ":", 4) + if len(parts) < 4 || parts[0] == "" || parts[idx] != value || + // If the file contains +foo and you search for "foo", glibc + // returns an "invalid argument" error. Similarly, if you search + // for a gid for a row where the group name starts with "+" or "-", + // glibc fails to find the record. + parts[0][0] == '+' || parts[0][0] == '-' { + return + } + if _, err := strconv.Atoi(parts[2]); err != nil { + return nil, nil + } + return &Group{Name: parts[0], Gid: parts[2]}, nil + } +} + +// UnknownGroupIDError is returned by LookupGroupId when +// a group cannot be found. +type UnknownGroupIDError string + +func (e UnknownGroupIDError) Error() string { + return "group: unknown groupid " + string(e) +}