2016-03-10 21:15:55 +00:00
package main
import (
2016-07-20 22:08:24 +00:00
"bytes"
2016-07-20 22:09:27 +00:00
"encoding/json"
2016-03-10 21:15:55 +00:00
"flag"
"fmt"
2016-06-28 20:40:35 +00:00
"io"
"io/ioutil"
2016-03-10 21:15:55 +00:00
"os"
"strings"
2016-03-24 20:31:36 +00:00
2017-11-03 15:10:58 +00:00
"github.com/sirupsen/logrus"
2016-03-31 18:03:56 +00:00
"github.com/vbatts/go-mtree"
2016-03-24 20:31:36 +00:00
)
var (
2016-11-18 03:00:23 +00:00
// Flags common with mtree(8)
2016-08-02 19:57:51 +00:00
flCreate = flag . Bool ( "c" , false , "create a directory hierarchy spec" )
flFile = flag . String ( "f" , "" , "directory hierarchy spec to validate" )
flPath = flag . String ( "p" , "" , "root path that the hierarchy spec is relative to" )
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" )
flDirectoryOnly = flag . Bool ( "d" , false , "Ignore everything except directory type files" )
flUpdateAttributes = flag . Bool ( "u" , false , "Modify the owner, group, permissions and xattrs of files, symbolic links and devices, to match the provided specification. This is not compatible with '-T'." )
2016-11-18 03:00:23 +00:00
// Flags unique to gomtree
2016-07-29 14:18:17 +00:00
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)" )
2016-08-11 18:07:07 +00:00
flTar = flag . String ( "T" , "" , "use tar archive to create or validate a directory hierarchy spec (\"-\" indicates stdin)" )
2016-07-29 14:18:17 +00:00
flBsdKeywords = flag . Bool ( "bsd-keywords" , false , "only operate on keywords that are supported by upstream mtree(8)" )
flListUsedKeywords = flag . Bool ( "list-used" , false , "list all the keywords found in a validation manifest" )
flDebug = flag . Bool ( "debug" , false , "output debug info to STDERR" )
2016-08-16 15:37:11 +00:00
flVersion = flag . Bool ( "version" , false , "display the version of this tool" )
2016-03-10 21:15:55 +00:00
)
2016-11-16 19:15:42 +00:00
func main ( ) {
// so that defers cleanly exec
if err := app ( ) ; err != nil {
2017-06-15 18:54:58 +00:00
logrus . Fatal ( err )
2016-11-10 01:41:20 +00:00
}
}
2016-11-16 19:15:42 +00:00
func app ( ) error {
2016-03-10 21:15:55 +00:00
flag . Parse ( )
2016-03-24 20:31:36 +00:00
2016-07-20 17:28:08 +00:00
if * flDebug {
os . Setenv ( "DEBUG" , "1" )
2017-06-15 18:54:58 +00:00
logrus . SetLevel ( logrus . DebugLevel )
2016-07-20 17:28:08 +00:00
}
2016-08-16 15:37:11 +00:00
if * flVersion {
2016-11-16 19:15:42 +00:00
fmt . Printf ( "%s :: %s\n" , mtree . AppName , mtree . Version )
return nil
2016-08-16 15:37:11 +00:00
}
2016-07-26 18:21:44 +00:00
// -list-keywords
2016-03-24 20:31:36 +00:00
if * flListKeywords {
fmt . Println ( "Available keywords:" )
for k := range mtree . KeywordFuncs {
2016-07-26 18:21:44 +00:00
fmt . Print ( " " )
fmt . Print ( k )
if mtree . Keyword ( k ) . Default ( ) {
fmt . Print ( " (default)" )
2016-03-10 21:15:55 +00:00
}
2016-07-26 18:21:44 +00:00
if ! mtree . Keyword ( k ) . Bsd ( ) {
fmt . Print ( " (not upstream)" )
}
fmt . Print ( "\n" )
2016-03-24 20:31:36 +00:00
}
2016-11-16 19:15:42 +00:00
return nil
2016-03-24 20:31:36 +00:00
}
2016-07-26 18:21:44 +00:00
// --result-format
2016-07-20 22:08:24 +00:00
formatFunc , ok := formats [ * flResultFormat ]
if ! ok {
2016-11-16 19:15:42 +00:00
return fmt . Errorf ( "invalid output format: %s" , * flResultFormat )
2016-07-20 22:08:24 +00:00
}
2016-07-26 18:31:39 +00:00
var (
2016-10-30 13:48:49 +00:00
err error
2016-11-18 00:47:31 +00:00
tmpKeywords [ ] mtree . Keyword
currentKeywords [ ] mtree . Keyword
2016-07-26 18:31:39 +00:00
)
2016-10-30 13:48:49 +00:00
2016-03-24 20:31:36 +00:00
// -k <keywords>
if * flUseKeywords != "" {
2016-07-26 18:31:39 +00:00
tmpKeywords = splitKeywordsArg ( * flUseKeywords )
2016-11-18 00:47:31 +00:00
if ! mtree . InKeywordSlice ( "type" , tmpKeywords ) {
tmpKeywords = append ( [ ] mtree . Keyword { "type" } , tmpKeywords ... )
2016-07-15 13:59:18 +00:00
}
2016-03-24 20:31:36 +00:00
} else {
2016-07-22 22:01:54 +00:00
if * flTar != "" {
2016-07-26 18:31:39 +00:00
tmpKeywords = mtree . DefaultTarKeywords [ : ]
2016-07-22 22:01:54 +00:00
} else {
2016-07-26 18:31:39 +00:00
tmpKeywords = mtree . DefaultKeywords [ : ]
2016-07-22 22:01:54 +00:00
}
2016-03-24 20:31:36 +00:00
}
2016-07-26 18:31:39 +00:00
2016-03-24 20:31:36 +00:00
// -K <keywords>
if * flAddKeywords != "" {
2016-07-25 22:29:10 +00:00
for _ , kw := range splitKeywordsArg ( * flAddKeywords ) {
2016-11-18 00:47:31 +00:00
if ! mtree . InKeywordSlice ( kw , tmpKeywords ) {
2016-07-26 18:31:39 +00:00
tmpKeywords = append ( tmpKeywords , kw )
}
}
}
// -bsd-keywords
if * flBsdKeywords {
for _ , k := range tmpKeywords {
if mtree . Keyword ( k ) . Bsd ( ) {
currentKeywords = append ( currentKeywords , k )
} else {
fmt . Fprintf ( os . Stderr , "INFO: ignoring %q as it is not an upstream keyword\n" , k )
2016-07-25 22:29:10 +00:00
}
}
2016-07-26 18:31:39 +00:00
} else {
currentKeywords = tmpKeywords
2016-03-24 20:31:36 +00:00
}
2016-10-30 13:48:49 +00:00
// Check mutual exclusivity of keywords.
// TODO(cyphar): Abstract this inside keywords.go.
2016-11-18 00:47:31 +00:00
if mtree . InKeywordSlice ( "tar_time" , currentKeywords ) && mtree . InKeywordSlice ( "time" , currentKeywords ) {
2016-11-16 19:15:42 +00:00
return fmt . Errorf ( "tar_time and time are mutually exclusive keywords" )
2016-10-30 13:48:49 +00:00
}
// If we're doing a comparison, we always are comparing between a spec and
// state DH. If specDh is nil, we are generating a new one.
var (
specDh * mtree . DirectoryHierarchy
stateDh * mtree . DirectoryHierarchy
2016-11-18 00:47:31 +00:00
specKeywords [ ] mtree . Keyword
2016-10-30 13:48:49 +00:00
)
2016-03-24 20:31:36 +00:00
// -f <file>
if * flFile != "" && ! * flCreate {
// load the hierarchy, if we're not creating a new spec
fh , err := os . Open ( * flFile )
if err != nil {
2016-11-16 19:15:42 +00:00
return err
2016-03-24 20:31:36 +00:00
}
2016-10-30 13:48:49 +00:00
specDh , err = mtree . ParseSpec ( fh )
2016-03-24 20:31:36 +00:00
fh . Close ( )
if err != nil {
2016-11-16 19:15:42 +00:00
return err
2016-03-24 20:31:36 +00:00
}
2016-10-30 13:48:49 +00:00
// We can't check against more fields than in the specKeywords list, so
// currentKeywords can only have a subset of specKeywords.
2016-11-16 19:43:05 +00:00
specKeywords = specDh . UsedKeywords ( )
2016-03-24 20:31:36 +00:00
}
2016-07-29 14:18:17 +00:00
// -list-used
if * flListUsedKeywords {
2016-10-30 13:48:49 +00:00
if specDh == nil {
2016-11-16 19:15:42 +00:00
return fmt . Errorf ( "no specification provided. please provide a validation manifest" )
2016-07-29 14:18:17 +00:00
}
2016-10-30 13:48:49 +00:00
gomtree: `-list-used` can output JSON
Piggybacking on `-result-format`:
```bash
$ tar c .git/ | gomtree -c -T - > git.mtree
$ gomtree -result-format=json -list-used -f ./git.mtree
{
"./git.mtree": [
"type",
"mode",
"uid",
"gid",
"tar_time",
"size"
]
}
$ tar c .git/ | gomtree -c -T - -K sha512digest > git.mtree
$ gomtree -result-format=json -list-used -f ./git.mtree
{
"./git.mtree": [
"type",
"mode",
"uid",
"gid",
"tar_time",
"size",
"sha512digest"
]
}
```
Signed-off-by: Vincent Batts <vbatts@hashbangbash.com>
2016-08-11 18:50:20 +00:00
if * flResultFormat == "json" {
// if they're asking for json, give it to them
2016-11-18 00:47:31 +00:00
data := map [ string ] [ ] mtree . Keyword { * flFile : specKeywords }
gomtree: `-list-used` can output JSON
Piggybacking on `-result-format`:
```bash
$ tar c .git/ | gomtree -c -T - > git.mtree
$ gomtree -result-format=json -list-used -f ./git.mtree
{
"./git.mtree": [
"type",
"mode",
"uid",
"gid",
"tar_time",
"size"
]
}
$ tar c .git/ | gomtree -c -T - -K sha512digest > git.mtree
$ gomtree -result-format=json -list-used -f ./git.mtree
{
"./git.mtree": [
"type",
"mode",
"uid",
"gid",
"tar_time",
"size",
"sha512digest"
]
}
```
Signed-off-by: Vincent Batts <vbatts@hashbangbash.com>
2016-08-11 18:50:20 +00:00
buf , err := json . MarshalIndent ( data , "" , " " )
if err != nil {
2016-11-16 19:15:42 +00:00
return err
gomtree: `-list-used` can output JSON
Piggybacking on `-result-format`:
```bash
$ tar c .git/ | gomtree -c -T - > git.mtree
$ gomtree -result-format=json -list-used -f ./git.mtree
{
"./git.mtree": [
"type",
"mode",
"uid",
"gid",
"tar_time",
"size"
]
}
$ tar c .git/ | gomtree -c -T - -K sha512digest > git.mtree
$ gomtree -result-format=json -list-used -f ./git.mtree
{
"./git.mtree": [
"type",
"mode",
"uid",
"gid",
"tar_time",
"size",
"sha512digest"
]
}
```
Signed-off-by: Vincent Batts <vbatts@hashbangbash.com>
2016-08-11 18:50:20 +00:00
}
fmt . Println ( string ( buf ) )
} else {
fmt . Printf ( "Keywords used in [%s]:\n" , * flFile )
2016-10-30 13:48:49 +00:00
for _ , kw := range specKeywords {
gomtree: `-list-used` can output JSON
Piggybacking on `-result-format`:
```bash
$ tar c .git/ | gomtree -c -T - > git.mtree
$ gomtree -result-format=json -list-used -f ./git.mtree
{
"./git.mtree": [
"type",
"mode",
"uid",
"gid",
"tar_time",
"size"
]
}
$ tar c .git/ | gomtree -c -T - -K sha512digest > git.mtree
$ gomtree -result-format=json -list-used -f ./git.mtree
{
"./git.mtree": [
"type",
"mode",
"uid",
"gid",
"tar_time",
"size",
"sha512digest"
]
}
```
Signed-off-by: Vincent Batts <vbatts@hashbangbash.com>
2016-08-11 18:50:20 +00:00
fmt . Printf ( " %s" , kw )
if _ , ok := mtree . KeywordFuncs [ kw ] ; ! ok {
fmt . Print ( " (unsupported)" )
}
fmt . Printf ( "\n" )
2016-07-29 14:18:17 +00:00
}
}
2016-11-16 19:15:42 +00:00
return nil
2016-07-29 14:18:17 +00:00
}
2016-10-30 13:48:49 +00:00
if specKeywords != nil {
// If we didn't actually change the set of keywords, we can just use specKeywords.
if * flUseKeywords == "" && * flAddKeywords == "" {
currentKeywords = specKeywords
}
for _ , keyword := range currentKeywords {
// As always, time is a special case.
// TODO: Fix that.
2016-11-18 00:47:31 +00:00
if ( keyword == "time" && mtree . InKeywordSlice ( "tar_time" , specKeywords ) ) || ( keyword == "tar_time" && mtree . InKeywordSlice ( "time" , specKeywords ) ) {
2016-10-30 13:48:49 +00:00
continue
}
}
}
// -p and -T are mutually exclusive
if * flPath != "" && * flTar != "" {
2016-11-16 19:15:42 +00:00
return fmt . Errorf ( "options -T and -p are mutually exclusive" )
2016-10-30 13:48:49 +00:00
}
2016-03-24 20:31:36 +00:00
// -p <path>
2016-07-21 17:40:48 +00:00
var rootPath = "."
2016-03-24 20:31:36 +00:00
if * flPath != "" {
rootPath = * flPath
}
2016-11-18 03:43:02 +00:00
excludes := [ ] mtree . ExcludeFunc { }
// -d
if * flDirectoryOnly {
excludes = append ( excludes , mtree . ExcludeNonDirectories )
}
2016-08-02 19:57:51 +00:00
// -u
// Failing early here. Processing is done below.
if * flUpdateAttributes && * flTar != "" {
return fmt . Errorf ( "ERROR: -u can not be used with -T" )
}
2016-06-28 20:40:35 +00:00
// -T <tar file>
if * flTar != "" {
2016-08-11 18:07:07 +00:00
var input io . Reader
if * flTar == "-" {
input = os . Stdin
} else {
fh , err := os . Open ( * flTar )
if err != nil {
2016-11-16 19:15:42 +00:00
return err
2016-08-11 18:07:07 +00:00
}
defer fh . Close ( )
input = fh
2016-06-28 20:40:35 +00:00
}
2016-11-18 03:43:02 +00:00
ts := mtree . NewTarStreamer ( input , excludes , currentKeywords )
2016-06-28 20:40:35 +00:00
if _ , err := io . Copy ( ioutil . Discard , ts ) ; err != nil && err != io . EOF {
2016-11-16 19:15:42 +00:00
return err
2016-06-28 20:40:35 +00:00
}
if err := ts . Close ( ) ; err != nil {
2016-11-16 19:15:42 +00:00
return err
2016-06-28 20:40:35 +00:00
}
2016-08-11 18:07:07 +00:00
var err error
2016-10-30 13:48:49 +00:00
stateDh , err = ts . Hierarchy ( )
if err != nil {
2016-11-16 19:15:42 +00:00
return err
2016-10-30 13:48:49 +00:00
}
} else {
// with a root directory
2016-11-18 07:53:26 +00:00
stateDh , err = mtree . Walk ( rootPath , excludes , currentKeywords , nil )
2016-03-24 20:31:36 +00:00
if err != nil {
2016-11-16 19:15:42 +00:00
return err
2016-03-24 20:31:36 +00:00
}
2016-06-28 20:40:35 +00:00
}
2016-07-26 18:21:44 +00:00
2016-08-02 19:57:51 +00:00
// -u
if * flUpdateAttributes && stateDh != nil {
// -u
// this comes before the next case, intentionally.
2017-06-26 19:12:31 +00:00
result , err := mtree . Update ( rootPath , specDh , mtree . DefaultUpdateKeywords , nil )
if err != nil {
return err
}
if result != nil && len ( result ) > 0 {
fmt . Printf ( "%#v\n" , result )
}
2016-08-02 19:57:51 +00:00
2017-06-26 19:12:31 +00:00
var res [ ] mtree . InodeDelta
// only check the keywords that we just updated
res , err = mtree . Check ( rootPath , specDh , mtree . DefaultUpdateKeywords , nil )
2016-08-02 19:57:51 +00:00
if err != nil {
return err
}
2017-06-26 19:12:31 +00:00
if res != nil {
out := formatFunc ( res )
if _ , err := os . Stdout . Write ( [ ] byte ( out ) ) ; err != nil {
return err
}
2016-08-02 19:57:51 +00:00
2017-06-26 19:12:31 +00:00
// TODO: This should be a flag. Allowing files to be added and
// removed and still returning "it's all good" is simply
// unsafe IMO.
for _ , diff := range res {
if diff . Type ( ) == mtree . Modified {
return fmt . Errorf ( "mainfest validation failed" )
}
}
2016-08-02 19:57:51 +00:00
}
2017-06-26 19:12:31 +00:00
2016-08-02 19:57:51 +00:00
return nil
}
2016-06-28 20:40:35 +00:00
// -c
if * flCreate {
2016-10-30 13:48:49 +00:00
fh := os . Stdout
if * flFile != "" {
fh , err = os . Create ( * flFile )
2016-06-28 20:40:35 +00:00
if err != nil {
2016-11-16 19:15:42 +00:00
return err
2016-06-28 20:40:35 +00:00
}
}
2016-10-30 13:48:49 +00:00
// output stateDh
stateDh . WriteTo ( fh )
2016-11-16 19:15:42 +00:00
return nil
2016-10-30 13:48:49 +00:00
}
2017-01-20 18:13:59 +00:00
// no spec manifest has been provided yet, so look for it on stdin
if specDh == nil {
// load the hierarchy
specDh , err = mtree . ParseSpec ( os . Stdin )
if err != nil {
return err
}
// We can't check against more fields than in the specKeywords list, so
// currentKeywords can only have a subset of specKeywords.
specKeywords = specDh . UsedKeywords ( )
}
2016-10-30 13:48:49 +00:00
// This is a validation.
if specDh != nil && stateDh != nil {
var res [ ] mtree . InodeDelta
res , err = mtree . Compare ( specDh , stateDh , currentKeywords )
2016-03-24 20:31:36 +00:00
if err != nil {
2016-11-16 19:15:42 +00:00
return err
2016-03-24 20:31:36 +00:00
}
2016-10-30 13:48:49 +00:00
if res != nil {
2016-11-10 01:41:20 +00:00
if isTarSpec ( specDh ) || * flTar != "" {
res = filterMissingKeywords ( res )
}
2016-10-30 13:48:49 +00:00
2016-07-20 22:08:24 +00:00
out := formatFunc ( res )
if _ , err := os . Stdout . Write ( [ ] byte ( out ) ) ; err != nil {
2016-11-16 19:15:42 +00:00
return err
2016-04-05 15:44:55 +00:00
}
2017-01-14 07:01:21 +00:00
// TODO: This should be a flag. Allowing files to be added and
// removed and still returning "it's all good" is simply
// unsafe IMO.
for _ , diff := range res {
if diff . Type ( ) == mtree . Modified {
return fmt . Errorf ( "mainfest validation failed" )
}
}
2016-07-22 22:01:54 +00:00
}
2016-07-26 00:35:38 +00:00
} else {
2016-11-16 19:15:42 +00:00
return fmt . Errorf ( "neither validating or creating a manifest. Please provide additional arguments" )
}
return nil
}
var formats = map [ string ] func ( [ ] mtree . InodeDelta ) string {
// Outputs the errors in the BSD format.
"bsd" : func ( d [ ] mtree . InodeDelta ) string {
var buffer bytes . Buffer
for _ , delta := range d {
2017-01-14 07:01:21 +00:00
fmt . Fprintln ( & buffer , delta )
2016-11-16 19:15:42 +00:00
}
return buffer . String ( )
} ,
// Outputs the full result struct in JSON.
"json" : func ( d [ ] mtree . InodeDelta ) string {
var buffer bytes . Buffer
if err := json . NewEncoder ( & buffer ) . Encode ( d ) ; err != nil {
panic ( err )
}
return buffer . String ( )
} ,
// Outputs only the paths which failed to validate.
"path" : func ( d [ ] mtree . InodeDelta ) string {
var buffer bytes . Buffer
for _ , delta := range d {
if delta . Type ( ) == mtree . Modified {
fmt . Fprintln ( & buffer , delta . Path ( ) )
}
}
return buffer . String ( )
} ,
}
// isDirEntry returns wheter an mtree.Entry describes a directory.
func isDirEntry ( e mtree . Entry ) bool {
for _ , kw := range e . Keywords {
kv := mtree . KeyVal ( kw )
if kv . Keyword ( ) == "type" {
return kv . Value ( ) == "dir"
}
2016-03-24 20:31:36 +00:00
}
2016-11-16 19:15:42 +00:00
// Shouldn't be reached.
return false
}
// filterMissingKeywords is a fairly annoying hack to get around the fact that
// tar archive manifest generation has certain unsolveable problems regarding
// certain keywords. For example, the size=... keyword cannot be implemented
// for directories in a tar archive (which causes Missing errors for that
// keyword).
//
// This function just removes all instances of Missing errors for keywords.
// This makes certain assumptions about the type of issues tar archives have.
// Only call this on tar archive manifest comparisons.
func filterMissingKeywords ( diffs [ ] mtree . InodeDelta ) [ ] mtree . InodeDelta {
newDiffs := [ ] mtree . InodeDelta { }
loop :
for _ , diff := range diffs {
if diff . Type ( ) == mtree . Modified {
// We only apply this filtering to directories.
// NOTE: This will probably break if someone drops the size keyword.
if isDirEntry ( * diff . Old ( ) ) || isDirEntry ( * diff . New ( ) ) {
// If this applies to '.' then we just filter everything
// (meaning we remove this entry). This is because note all tar
// archives include a '.' entry. Which makes checking this not
// practical.
if diff . Path ( ) == "." {
continue
}
// Only filter out the size keyword.
// NOTE: This currently takes advantage of the fact the
// diff.Diff() returns the actual slice to diff.keys.
keys := diff . Diff ( )
for idx , k := range keys {
// Delete the key if it's "size". Unfortunately in Go you
// can't delete from a slice without reassigning it. So we
// just overwrite it with the last value.
if k . Name ( ) == "size" {
if len ( keys ) < 2 {
continue loop
}
keys [ idx ] = keys [ len ( keys ) - 1 ]
}
}
}
}
// If we got here, append to the new set.
newDiffs = append ( newDiffs , diff )
}
return newDiffs
}
// isTarSpec returns whether the spec provided came from the tar generator.
// This takes advantage of an unsolveable problem in tar generation.
func isTarSpec ( spec * mtree . DirectoryHierarchy ) bool {
// Find a directory and check whether it's missing size=...
// NOTE: This will definitely break if someone drops the size=... keyword.
for _ , e := range spec . Entries {
if ! isDirEntry ( e ) {
continue
}
for _ , kw := range e . Keywords {
kv := mtree . KeyVal ( kw )
if kv . Keyword ( ) == "size" {
return false
}
}
return true
}
// Should never be reached.
return false
2016-03-24 20:31:36 +00:00
}
2016-11-18 00:47:31 +00:00
func splitKeywordsArg ( str string ) [ ] mtree . Keyword {
keywords := [ ] mtree . Keyword { }
2016-11-16 20:42:53 +00:00
for _ , kw := range strings . Fields ( strings . Replace ( str , "," , " " , - 1 ) ) {
keywords = append ( keywords , mtree . KeywordSynonym ( kw ) )
}
return keywords
2016-03-24 20:31:36 +00:00
}