From a80397fdf6887ac4709d6fbfca5a06df0df6f73f Mon Sep 17 00:00:00 2001 From: Jeffrey Paul Date: Thu, 3 Oct 2019 05:43:02 -0700 Subject: [PATCH] seems to work now! needs more features --- Makefile | 2 +- README.md | 10 ++++-- main.go | 97 +++++++++++++++++++++++++++++++++++++++---------------- 3 files changed, 78 insertions(+), 31 deletions(-) diff --git a/Makefile b/Makefile index c099c92..4fbdb05 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ GOFLAGS = -ldflags "$(GOLDFLAGS)" default: rundebug rundebug: build - ./xsum -d + ./xsum -v run: build ./xsum diff --git a/README.md b/README.md index aee5a10..d55bccf 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ # xsum -this stores a file mtime and cryptographically secure content checksum +This stores a file mtime, a size, and a cryptographically secure content checksum in an xattr, so that you can verify that your files aren't corrupted on filesystems that are dumb and don't include data checksums (e.g. apfs) +# background + +You can dd a few random bytes into the middle of an hfs+ or apfs filesystem, +and, if they land in file data, an fsck/Disk First Aid on the filesystem +will pass with flying colors. There is no file content checksum. + # author -sneak +Jeffrey Paul [sneak@sneak.berlin](mailto:sneak@sneak.berlin) diff --git a/main.go b/main.go index eb53a76..791b30e 100644 --- a/main.go +++ b/main.go @@ -4,12 +4,12 @@ package main import "crypto/sha256" import "fmt" -import "github.com/sirupsen/logrus" -import "github.com/multiformats/go-multihash" +import "github.com/jessevdk/go-flags" import "github.com/mr-tron/base58" +import "github.com/multiformats/go-multihash" import "github.com/pkg/xattr" +import "github.com/sirupsen/logrus" -//import "errors" import "os" import "io" import "flag" @@ -40,9 +40,21 @@ func xsum() int { log = logrus.New() log.SetLevel(logrus.ErrorLevel) log.SetReportCaller(false) - debugPtr := flag.Bool("d", false, "debug mode") - flag.Parse() - if *debugPtr == true { + + var opts struct { + // Slice of bool will append 'true' each time the option + // is encountered (can be set multiple times, like -vvv) + Verbose bool `short:"v" long:"verbose" description:"Show verbose debug information"` + } + + args, err := flags.Parse(&opts) + + if err != nil { + usage() + return -1 + } + + if opts.Verbose == true { log.SetReportCaller(true) log.SetLevel(logrus.DebugLevel) } @@ -55,15 +67,20 @@ func xsum() int { Builduser, ) - paths := flag.Args() - if len(paths) > 1 { - paths = paths[1:] + if len(args) < 2 { + usage() + return -1 } - switch flag.Arg(0) { + + mode := args[0] + + paths := args[1:] + + switch mode { case "cron": x := xsfCheckAndUpdate(paths) if x != nil { - log.Error(x) + log.Debug(x) return -1 } else { return 0 @@ -71,7 +88,7 @@ func xsum() int { case "check-and-update": x := xsfCheckAndUpdate(paths) if x != nil { - log.Error(x) + log.Debug(x) return -1 } else { return 0 @@ -79,7 +96,7 @@ func xsum() int { case "check": x := xsfCheck(paths) if x != nil { - log.Error(x) + log.Debug(x) return -1 } else { return 0 @@ -87,7 +104,7 @@ func xsum() int { case "update": x := xsfUpdate(paths) if x != nil { - log.Error(x) + log.Debug(x) return -1 } else { return 0 @@ -99,7 +116,7 @@ func xsum() int { } func usage() { - fmt.Fprintf(os.Stderr, "usage: %s [-d] [path2] [...]\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "usage: %s [-v] [path2] [...]\n", os.Args[0]) flag.PrintDefaults() } @@ -109,7 +126,10 @@ func xsfCheck(paths []string) error { x := newXsf(path) err := x.Check() if err != nil { + fmt.Printf("%s\tERROR (expected=%s actual=%s)\n", x.path, x.xmultihash, x.multihash) return err + } else { + fmt.Printf("%s\tOK (hash=%s)\n", x.path, x.multihash) } } return nil @@ -125,8 +145,9 @@ func xsfUpdate(paths []string) error { x := newXsf(path) err := x.Update() if err != nil { - showError(err) - return err + failure := fmt.Errorf("%s\tERROR (error=%s)\n", x.path, err) + log.Error(failure) + return failure } } return nil @@ -134,9 +155,12 @@ func xsfUpdate(paths []string) error { func xsfCheckAndUpdate(paths []string) error { log.Debugf("check-and-update") - r := xsfCheck(paths) - if r != nil { - return r + err := xsfCheck(paths) + if err != nil { + //xsfCheck() does the printing of errors itself, we just need to + //bubble it up and not update + log.Error(err) + return err } return xsfUpdate(paths) } @@ -252,12 +276,10 @@ func (x *xsf) readSizeXattr() error { if err != nil { return err } - a, b := strconv.ParseInt(string(v), 10, 64) if b != nil { return b } - x.xsize = uint64(a) return nil } @@ -358,8 +380,8 @@ func (x *xsf) Check() error { actualHash := x.multihash if predictedHash != actualHash { - log.Errorf("file corruption detected: expected=%s actual=%s", predictedHash, actualHash) - return err + failure := fmt.Errorf("file corruption detected: expected=%s actual=%s", predictedHash, actualHash) + return failure } else { log.Infof("file OK hash=%s", actualHash) return nil @@ -386,22 +408,37 @@ func (x *xsf) missingXattrs() bool { } func (x *xsf) needsUpdate() bool { + log.Debugf("checking if file needs update") // this expects stat() to have been called on the xsf // by Update already, so we have x.mtime et al populated from the // filesystem // if the file doesn't have all 3 xattrs, it needs an update. - if x.missingXattrs() == false { + if x.missingXattrs() == true { + log.Debugf("file is missing xattrs") return true } // if the size doesn't match, it needs an update + e := x.readSizeXattr() + if e != nil { + log.Debugf("unable to read file size attribute") + return true + } + if x.size != x.xsize { log.Debugf("file needs update, size is %s, xattr size is %s", x.size, x.xsize) return true } // if the mtime is not the same, it needs an update + + e2 := x.readMtimeXattr() + if e2 != nil { + log.Debugf("unable to read file mtime attribute") + return true + } + if x.mtime != x.xmtime { log.Debugf("file needs update, mtime is %s, xattr mtime is %s", x.mtime, x.xmtime) return true @@ -419,13 +456,17 @@ func (x *xsf) Update() error { return err } - //FIXME check if size/mtime are defined first - //and skip update if match (and key 'multihash' exists) - if err = x.stat(); err != nil { return err } + // reminder: needsUpdate() must be called after stat() so that the + // struct is populated + if x.needsUpdate() == false { + log.Debugf("skipping update on already hashed file %s", x.path) + return nil + } + if err = x.hash(); err != nil { return err }