From 05161ec9dfb69074e0e2176aab3f468887ad9784 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Sat, 23 Jul 2016 13:42:03 +1000 Subject: [PATCH 1/3] cmd: rename --results-format=path to text In order to provide enough context for the failure type to the caller, we have to change this format. Signed-off-by: Aleksa Sarai --- cmd/gomtree/main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/gomtree/main.go b/cmd/gomtree/main.go index 3352ba6..a3fa228 100644 --- a/cmd/gomtree/main.go +++ b/cmd/gomtree/main.go @@ -19,7 +19,7 @@ var ( flAddKeywords = flag.String("K", "", "Add the specified (delimited by comma or space) keywords to the current set of keywords") flUseKeywords = flag.String("k", "", "Use the specified (delimited by comma or space) keywords as the current set of keywords") flListKeywords = flag.Bool("list-keywords", false, "List the keywords available") - flResultFormat = flag.String("result-format", "bsd", "output the validation results using the given format (bsd, json, path)") + flResultFormat = flag.String("result-format", "bsd", "output the validation results using the given format (bsd, json, text)") ) var formats = map[string]func(*mtree.Result) string{ @@ -41,11 +41,11 @@ var formats = map[string]func(*mtree.Result) string{ return buffer.String() }, - // Outputs only the paths which failed to validate. - "path": func(r *mtree.Result) string { + // Outputs only the failure type and paths which failed to validate. + "text": func(r *mtree.Result) string { var buffer bytes.Buffer for _, fail := range r.Failures { - fmt.Fprintln(&buffer, fail.Path) + fmt.Fprintf(&buffer, "modified\t%s\n", fail.Path) } return buffer.String() }, From 37060d602b208865bcba80859512f93867a9974c Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Sat, 23 Jul 2016 13:44:16 +1000 Subject: [PATCH 2/3] check: implement "missing" failure type If a file is missing, previously the check would simply fail. This allows you to get information even if files are missing from the tree. This also implements some of the wrappers required to add more failure states. Signed-off-by: Aleksa Sarai --- check.go | 124 ++++++++++++++++++++++++++++++++++++++++---- cmd/gomtree/main.go | 2 +- 2 files changed, 114 insertions(+), 12 deletions(-) diff --git a/check.go b/check.go index 87daf54..17be735 100644 --- a/check.go +++ b/check.go @@ -1,6 +1,7 @@ package mtree import ( + "encoding/json" "fmt" "os" "path/filepath" @@ -13,17 +14,108 @@ type Result struct { Failures []Failure `json:"failures"` } -// Failure of a particular keyword for a path -type Failure struct { - Path string `json:"path"` - Keyword string `json:"keyword"` - Expected string `json:"expected"` - Got string `json:"got"` +// FailureType represents a type of Failure encountered when checking for +// discrepancies between a manifest and a directory state. +type FailureType string + +const ( + // None means no discrepancy (unused). + None FailureType = "none" + + // Missing represents a discrepancy where an object is present in the + // manifest but is not present in the directory state being checked. + Missing FailureType = "missing" + + // Modified represents a discrepancy where one or more of the keywords + // present in the manifest do not match the keywords generated for the same + // object in the directory state being checked. + Modified FailureType = "modified" +) + +// Failure represents a discrepancy between a manifest and the state it is +// being compared against. The discrepancy may take the form of a missing, +// erroneous or otherwise modified object. +type Failure interface { + // String returns a "pretty" formatting for a Failure. It's based on the + // BSD mtree(8) format, so that we are compatible. + String() string + + // MarshalJSON ensures that we fulfil the JSON Marshaler interface. + MarshalJSON() ([]byte, error) + + // Path returns the path (relative to the root of the tree) that this + // discrepancy refers to. + Path() string + + // Type returns the type of failure that occurred. + Type() FailureType } -// String returns a "pretty" formatting for a Failure -func (f Failure) String() string { - return fmt.Sprintf("%q: keyword %q: expected %s; got %s", f.Path, f.Keyword, f.Expected, f.Got) +// An object modified from the manifest state. +type modified struct { + path string + keyword string + expected string + got string +} + +func (m modified) String() string { + return fmt.Sprintf("%q: keyword %q: expected %s; got %s", m.path, m.keyword, m.expected, m.got) +} + +func (m modified) MarshalJSON() ([]byte, error) { + // Because of Go's reflection policies, we have to make an anonymous struct + // with the fields exported. + return json.Marshal(struct { + Type FailureType `json:"type"` + Path string `json:"path"` + Keyword string `json:"keyword"` + Expected string `json:"expected"` + Got string `json:"got"` + }{ + Type: m.Type(), + Path: m.path, + Keyword: m.keyword, + Expected: m.expected, + Got: m.got, + }) +} + +func (m modified) Path() string { + return m.path +} + +func (m modified) Type() FailureType { + return Modified +} + +// An object that is listed in the manifest but is not present in the state. +type missing struct { + path string +} + +func (m missing) String() string { + return fmt.Sprintf("%q: expected object missing", m.path) +} + +func (m missing) MarshalJSON() ([]byte, error) { + // Because of Go's reflection policies, we have to make an anonymous struct + // with the fields exported. + return json.Marshal(struct { + Type FailureType `json:"type"` + Path string `json:"path"` + }{ + Type: m.Type(), + Path: m.path, + }) +} + +func (m missing) Path() string { + return m.path +} + +func (m missing) Type() FailureType { + return Missing } // Check a root directory path against the DirectoryHierarchy, regarding only @@ -53,6 +145,12 @@ func Check(root string, dh *DirectoryHierarchy, keywords []string) (*Result, err case RelativeType, FullType: info, err := os.Lstat(e.Path()) if err != nil { + if os.IsNotExist(err) { + result.Failures = append(result.Failures, missing{ + path: e.Path(), + }) + continue + } return nil, err } @@ -76,8 +174,12 @@ func Check(root string, dh *DirectoryHierarchy, keywords []string) (*Result, err return nil, err } 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) + result.Failures = append(result.Failures, modified{ + path: e.Path(), + keyword: kv.Keyword(), + expected: kv.Value(), + got: KeyVal(curKeyVal).Value(), + }) } } } diff --git a/cmd/gomtree/main.go b/cmd/gomtree/main.go index a3fa228..46a2f6c 100644 --- a/cmd/gomtree/main.go +++ b/cmd/gomtree/main.go @@ -45,7 +45,7 @@ var formats = map[string]func(*mtree.Result) string{ "text": func(r *mtree.Result) string { var buffer bytes.Buffer for _, fail := range r.Failures { - fmt.Fprintf(&buffer, "modified\t%s\n", fail.Path) + fmt.Fprintf(&buffer, "%s\t%s\n", fail.Type(), fail.Path()) } return buffer.String() }, From 2cdb8c693009b1b3f5cb4b41c2aa42f1f5f54446 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Sat, 23 Jul 2016 03:36:04 +1000 Subject: [PATCH 3/3] check: implement "present" failure type If an object is present, previously we would ignore any new objects. Now, we will fail if a directory has objects that were not present in the manifest. Signed-off-by: Aleksa Sarai --- check.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/check.go b/check.go index 17be735..c1cb4ab 100644 --- a/check.go +++ b/check.go @@ -30,6 +30,10 @@ const ( // present in the manifest do not match the keywords generated for the same // object in the directory state being checked. Modified FailureType = "modified" + + // Present represents a discrepancy where an object is not present in the + // manifest but is present in the directory state being checked. + Present FailureType = "present" ) // Failure represents a discrepancy between a manifest and the state it is @@ -118,6 +122,35 @@ func (m missing) Type() FailureType { return Missing } +// An object that is not listed in the manifest but is present in the state. +type present struct { + path string +} + +func (p present) String() string { + return fmt.Sprintf("%q: unexpected object present", p.path) +} + +func (p present) MarshalJSON() ([]byte, error) { + // Because of Go's reflection policies, we have to make an anonymous struct + // with the fields exported. + return json.Marshal(struct { + Type FailureType `json:"type"` + Path string `json:"path"` + }{ + Type: p.Type(), + Path: p.path, + }) +} + +func (p present) Path() string { + return p.path +} + +func (p present) Type() FailureType { + return Missing +} + // Check a root directory path against the DirectoryHierarchy, regarding only // the available keywords from the list and each entry in the hierarchy. // If keywords is nil, the check all present in the DirectoryHierarchy @@ -134,6 +167,9 @@ func Check(root string, dh *DirectoryHierarchy, keywords []string) (*Result, err sort.Sort(byPos(creator.DH.Entries)) var result Result + seen := map[string]struct{}{ + ".": struct{}{}, + } for i, e := range creator.DH.Entries { switch e.Type { case SpecialType: @@ -154,6 +190,8 @@ func Check(root string, dh *DirectoryHierarchy, keywords []string) (*Result, err return nil, err } + seen[e.Path()] = struct{}{} + var kvs KeyVals if creator.curSet != nil { kvs = MergeSet(creator.curSet.Keywords, e.Keywords) @@ -184,5 +222,24 @@ func Check(root string, dh *DirectoryHierarchy, keywords []string) (*Result, err } } } + + // To find any paths we haven't seen already. + if err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error { + /* We shouldn't get not-found errors. */ + if err != nil { + return err + } + + if _, ok := seen[path]; !ok { + result.Failures = append(result.Failures, present{ + path: path, + }) + } + + return nil + }); err != nil { + return nil, fmt.Errorf("Unexpected error ocurred while walking directory: %s", err) + } + return &result, nil }