diff --git a/check.go b/check.go index 87daf54..c1cb4ab 100644 --- a/check.go +++ b/check.go @@ -1,6 +1,7 @@ package mtree import ( + "encoding/json" "fmt" "os" "path/filepath" @@ -13,17 +14,141 @@ 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" + + // 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 +// 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 +} + +// 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 @@ -42,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: @@ -53,9 +181,17 @@ 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 } + seen[e.Path()] = struct{}{} + var kvs KeyVals if creator.curSet != nil { kvs = MergeSet(creator.curSet.Keywords, e.Keywords) @@ -76,11 +212,34 @@ 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(), + }) } } } } + + // 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 } diff --git a/cmd/gomtree/main.go b/cmd/gomtree/main.go index 3352ba6..46a2f6c 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, "%s\t%s\n", fail.Type(), fail.Path()) } return buffer.String() },