From 7d6c06b2518c174c2b0200a109535079e4c24c08 Mon Sep 17 00:00:00 2001 From: dtluna Date: Wed, 27 Mar 2019 13:50:52 +0300 Subject: [PATCH] Implement md5sum --- README.md | 2 +- error.go | 2 +- go.sum | 1 + hashsum.go | 199 ++++++++++++++++++++++++++++++++++++++++ hashsum_test.go | 69 ++++++++++++++ md5sum/md5sum.go | 134 +-------------------------- test_data/some_file.txt | 1 + 7 files changed, 273 insertions(+), 135 deletions(-) create mode 100644 hashsum.go create mode 100644 hashsum_test.go create mode 100644 test_data/some_file.txt diff --git a/README.md b/README.md index 7e1c514..4137675 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Utilities: * [ ] `logger` * [ ] `logname` * [ ] `ls` - * [ ] `md5sum` + * [x] `md5sum` * [ ] `mkdir` * [ ] `mkfifo` * [ ] `mktemp` diff --git a/error.go b/error.go index b974460..74c8857 100644 --- a/error.go +++ b/error.go @@ -1,4 +1,4 @@ -package common +package coreutils import ( "fmt" diff --git a/go.sum b/go.sum index c0c7cc9..183a049 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,7 @@ github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= diff --git a/hashsum.go b/hashsum.go new file mode 100644 index 0000000..341c571 --- /dev/null +++ b/hashsum.go @@ -0,0 +1,199 @@ +package coreutils + +import ( + "bufio" + "crypto/md5" + "errors" + "fmt" + "hash" + "io" + "os" + "regexp" + + "github.com/alexflint/go-arg" +) + +var MD5Regex = regexp.MustCompile("^(?P[0-9a-f]{32}) (?P.*)$") + +func MD5Sum(r io.Reader) (hash.Hash, error) { + h := md5.New() + if _, err := io.Copy(h, r); err != nil { + return nil, err + } + return h, nil +} + +type CheckingResults struct { + ImproperlyFormattedCount uint + InvalidChecksumCount uint + FilesNotRead uint +} + +// SumFunc is a type of function that computes a hash.Hash for data in io.Reader +type SumFunc func(io.Reader) (hash.Hash, error) + +// ImproperlyFormattedErr is an error return when a line for checking +// has an incorrect number or set of characters +var ImproperlyFormattedErr = errors.New("improperly formatted line") + +func printHash(h hash.Hash, filename string) { + fmt.Printf("%x %v\n", h.Sum(nil), filename) +} + +// PrintSumForFile prints checksum computed by f for file with filename +func PrintSumForFile(filename string, f SumFunc) error { + file, err := os.Open(filename) + if err != nil { + return err + } + defer file.Close() + + h, err := f(file) + if err != nil { + return err + } + + printHash(h, filename) + return nil +} + +// PrintSumForStdin prints checksum computed by f for data from stdin +func PrintSumForStdin(f SumFunc) { + h, err := f(os.Stdin) + if err != nil { + PrintToStderr(err) + os.Exit(1) + } + printHash(h, "") + os.Exit(0) +} + +func checkLine(line string, re *regexp.Regexp, f SumFunc) (valid bool, err error) { + matches := re.MatchString(line) + if !matches { + err = ImproperlyFormattedErr + valid = false + return + } + submatches := re.FindStringSubmatch(line) + hash := submatches[1] + filename := submatches[2] + + file, err := os.Open(filename) + if err != nil { + valid = false + return + } + defer file.Close() + h, err := f(file) + if err != nil { + valid = false + return + } + + result := "OK" + valid = true + if fmt.Sprintf("%x", h.Sum(nil)) != hash { + result = "FAILED" + valid = false + } + fmt.Printf("%s: %s\n", filename, result) + + return +} + +// CheckSumsInReader checks checksums in r using f +func CheckSumsInReader(r io.Reader, re *regexp.Regexp, f SumFunc) (*CheckingResults, error) { + scanner := bufio.NewScanner(r) + + results := CheckingResults{} + for scanner.Scan() { + valid, err := checkLine(scanner.Text(), re, f) + if err == ImproperlyFormattedErr { + results.ImproperlyFormattedCount++ + } else if err != nil { + PrintToStderr(err) + results.FilesNotRead++ + } else if !valid { + results.InvalidChecksumCount++ + } + } + return &results, scanner.Err() +} + +// CheckSumsInFile checks checksums in file with filename using f +func CheckSumsInFile(filename string, re *regexp.Regexp, f SumFunc) (*CheckingResults, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + return CheckSumsInReader(file, re, f) +} + +// PrintCheckingResults prints the number of mismatched checksums and improperly formatted lines to os.Stderr +func PrintCheckingResults(results CheckingResults) { + if results.InvalidChecksumCount > 0 { + PrintToStderr( + fmt.Sprintf("%v computed checksums did NOT match", results.InvalidChecksumCount), + ) + } + if results.ImproperlyFormattedCount > 0 { + PrintToStderr( + fmt.Sprintf("%v lines are improperly formatted", results.ImproperlyFormattedCount), + ) + } + if results.FilesNotRead > 0 { + PrintToStderr( + fmt.Sprintf("%v listed files could not be read", results.FilesNotRead), + ) + } +} + +// SumMain is a function you're supposed to run in main of a hash sum utility +func SumMain(re *regexp.Regexp, f SumFunc) { + var args struct { + Check bool `arg:"-c"` + Files []string `arg:"positional"` + } + + arg.MustParse(&args) + + exitCode := 0 + if len(args.Files) == 0 { + if !args.Check { + PrintSumForStdin(f) + } else { + results, err := CheckSumsInReader(os.Stdin, re, f) + if err != nil { + PrintToStderr(err) + exitCode = 1 + } + PrintCheckingResults(*results) + } + } + + totalResults := CheckingResults{} + for _, filename := range args.Files { + if !args.Check { + err := PrintSumForFile(filename, f) + if err != nil { + PrintToStderr(err) + exitCode = 1 + } + } else { + results, err := CheckSumsInFile(filename, re, f) + if err != nil { + PrintToStderr(err) + exitCode = 1 + } + if results != nil { + totalResults.ImproperlyFormattedCount += results.ImproperlyFormattedCount + totalResults.InvalidChecksumCount += results.InvalidChecksumCount + } + } + } + PrintCheckingResults(totalResults) + os.Exit(exitCode) +} diff --git a/hashsum_test.go b/hashsum_test.go new file mode 100644 index 0000000..ac5e311 --- /dev/null +++ b/hashsum_test.go @@ -0,0 +1,69 @@ +package coreutils + +import ( + "bytes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "path" + "strings" + "testing" +) + +type CheckLineTestSuite struct { + suite.Suite +} + +func (suite *CheckLineTestSuite) TestImproperlyFormattedLine() { + t := suite.T() + valid, err := checkLine("name chef", MD5Regex, MD5Sum) + assert.Equal(t, false, valid) + assert.Error(t, err) + assert.Equal(t, ImproperlyFormattedErr, err) +} + +func (suite *CheckLineTestSuite) TestMismatchedChecksum() { + t := suite.T() + line := "4a5fb9ebd6c8ea7efb53d071053ef778 " + path.Join("test_data", "some_file.txt") + valid, err := checkLine(line, MD5Regex, MD5Sum) + assert.Equal(t, false, valid) + assert.NoError(t, err) +} + +func (suite *CheckLineTestSuite) TestValid() { + t := suite.T() + line := "6a5fb9ebd6c8ea7efb53d071053ef778 " + path.Join("test_data", "some_file.txt") + valid, err := checkLine(line, MD5Regex, MD5Sum) + assert.Equal(t, true, valid) + assert.NoError(t, err) +} + +func TestCheckLineTestSuite(t *testing.T) { + suite.Run(t, new(CheckLineTestSuite)) +} + +type CheckSumsInReaderTestSuite struct { + suite.Suite +} + +func (suite *CheckSumsInReaderTestSuite) Test() { + t := suite.T() + buf := bytes.NewBufferString(strings.Join( + []string{ + "name chef", + "4a5fb9ebd6c8ea7efb53d071053ef778 " + path.Join("test_data", "some_file.txt"), + "6a5fb9ebd6c8ea7efb53d071053ef778 " + path.Join("test_data", "some_file.txt"), + "4a5fb9ebd6c8ea7efb53d071053ef778 nonexistant_file", + }, + "\n", + )) + results, err := CheckSumsInReader(buf, MD5Regex, MD5Sum) + assert.NotNil(t, results) + assert.Equal(t, uint(1), results.ImproperlyFormattedCount) + assert.Equal(t, uint(1), results.FilesNotRead) + assert.Equal(t, uint(1), results.InvalidChecksumCount) + assert.NoError(t, err) +} + +func TestCheckSumsInReaderTestSuite(t *testing.T) { + suite.Run(t, new(CheckSumsInReaderTestSuite)) +} diff --git a/md5sum/md5sum.go b/md5sum/md5sum.go index 6d5d1aa..ede0cdd 100644 --- a/md5sum/md5sum.go +++ b/md5sum/md5sum.go @@ -1,141 +1,9 @@ package main import ( - "bufio" - "crypto/md5" - "errors" - "fmt" - "hash" - "io" - "os" - "strings" - - "github.com/alexflint/go-arg" common "source.heropunch.io/tomo/go-coreutils" ) -var ImproperlyFormattedErr = errors.New("improperly formatted line") - -func printSumForFile(filename string, sumFunc func(io.Reader) (hash.Hash, error)) error { - file, err := os.Open(filename) - if err != nil { - return err - } - defer file.Close() - - h, err := sumFunc(file) - if err != nil { - return err - } - - printHash(h, filename) - return nil -} - -func printSumForStdin(sumFunc func(io.Reader) (hash.Hash, error)) { - h, err := sumFunc(os.Stdin) - if err != nil { - common.PrintToStderr(err) - os.Exit(1) - } - printHash(h, "") - os.Exit(0) -} - -func md5sum(r io.Reader) (hash.Hash, error) { - h := md5.New() - if _, err := io.Copy(h, r); err != nil { - return nil, err - } - return h, nil -} - -func printHash(h hash.Hash, filename string) { - fmt.Printf("%x %v\n", h.Sum(nil), filename) -} - -func checkSumForFile(filename string) error { - file, err := os.Open(filename) - if err != nil { - return err - } - defer file.Close() - - return checkSums(file) -} - -func checkSums(r io.Reader) error { - scanner := bufio.NewScanner(r) - - for scanner.Scan() { - err := checkMD5Sum(scanner.Text()) - if err != nil { - return err - } - } - return scanner.Err() -} - -func checkMD5Sum(line string) error { - s := strings.SplitN(line, " ", 2) - hash := s[0] - filename := s[1] - - if filename == "" { - return ImproperlyFormattedErr - } - - file, err := os.Open(filename) - if err != nil { - return err - } - defer file.Close() - - h, err := md5sum(file) - if err != nil { - return err - } - result := "OK" - if fmt.Sprintf("%x", h.Sum(nil)) != hash { - result = "FAILED" - } - fmt.Printf("%s: %s\n", filename, result) - - return nil -} - func main() { - var args struct { - Check bool `arg:"-c"` - Files []string `arg:"positional"` - } - - arg.MustParse(&args) - - exitCode := 0 - if len(args.Files) == 0 { - if !args.Check { - printSumForStdin(md5sum) - } else { - fmt.Println("Checking from stdin") - } - } - - for _, filename := range args.Files { - if !args.Check { - err := printSumForFile(filename, md5sum) - if err != nil { - common.PrintToStderr(err) - exitCode = 1 - } - } else { - err := checkSumForFile(filename) - if err != nil { - common.PrintToStderr(err) - exitCode = 1 - } - } - } - - os.Exit(exitCode) + common.SumMain(common.MD5Regex, common.MD5Sum) } diff --git a/test_data/some_file.txt b/test_data/some_file.txt new file mode 100644 index 0000000..30f21dd --- /dev/null +++ b/test_data/some_file.txt @@ -0,0 +1 @@ +blah blah