diff --git a/.github/workflows/go-util-build.yml b/.github/workflows/go-util-build.yml index 8b5b86b..3c36b28 100644 --- a/.github/workflows/go-util-build.yml +++ b/.github/workflows/go-util-build.yml @@ -12,7 +12,7 @@ jobs: - name: Setup Go environment uses: actions/setup-go@v4.0.1 with: - go-version: "1.20" + go-version: "1.23" - name: Checkout uses: actions/checkout@v3.5.3 diff --git a/.gitignore b/.gitignore index 50a888b..23c4fbe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.out +coverage.* __debug_bin **/artifacts diff --git a/README.md b/README.md index 38041a8..a968d2b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ Notes & External Docs --------------------- +### Libraries ### + +* [CLI Parser Kong](https://github.com/alecthomas/kong#readme) + ### Cross Compiling ### * [Go Architecture Matrix](https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..64e85c4 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,19 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| --------- | ------------------ | +| tbd | :white_check_mark: | +| < tbd | :x: | +| latest | :x: | + +## Reporting a Vulnerability + +To report any vulnerability or bugs open an issue at: + +https://github.com/soerenkoehler/go-chdiff/issues + +Resource considerations: Real life will always out-prioritise non-commercial +open source hobby projects. So don't expect 24/7 support with 30min response +time. \ No newline at end of file diff --git a/chdiff/chdiff.go b/chdiff/chdiff.go index bd24a5a..0877f17 100644 --- a/chdiff/chdiff.go +++ b/chdiff/chdiff.go @@ -2,61 +2,155 @@ package chdiff import ( _ "embed" - "log" + "encoding/json" + "io" "os" - "path" + "path/filepath" "github.com/alecthomas/kong" - "github.com/soerenkoehler/chdiff-go/digest" - "github.com/soerenkoehler/chdiff-go/util" + "github.com/soerenkoehler/go-chdiff/common" + "github.com/soerenkoehler/go-chdiff/diff" + "github.com/soerenkoehler/go-chdiff/digest" + "github.com/soerenkoehler/go-chdiff/util" ) -//go:embed description.txt -var _Description string +const ( + DefaultDigestName string = ".chdiff.txt" + UserConfigFileName string = ".chdiff-config.json" +) -var cli struct { - Create cmdDigest `cmd:"" name:"create" aliases:"c" help:"Create digest file for PATH."` - Verify cmdDigest `cmd:"" name:"verify" aliases:"v" default:"1" help:"Verify digest file for PATH."` -} +var ( + //go:embed description.txt + _description string + + //go:embed default-config.json + _defaultConfigJson []byte +) type cmdDigest struct { - Path string `arg:"" name:"PATH" type:"path" default:"." help:"Path for which to calculate the digest"` - Algorithm string `name:"alg" help:"The checksum algorithm to use [SHA256,SHA512]." enum:"SHA256,SHA512" default:"SHA256"` + RootPath string `arg:"" name:"PATH" type:"path" default:"." help:"Path for which to calculate the digest"` + DigestFile string `name:"file" short:"f" help:"Optional: Path to different location of the digest file."` } -func DoMain( +type CmdCreate struct { + cmdDigest + Algorithm string `name:"algorithm" short:"a" help:"The checksum algorithm to use [SHA256,SHA512]." enum:"SHA256,SHA512" default:"SHA256"` +} + +type CmdVerify struct{ cmdDigest } + +type ChdiffDependencies interface { + DigestRead(string, string) (digest.Digest, error) + DigestWrite(digest.Digest, string) error + DigestCalculate(string, digest.HashType) digest.Digest + DigestCompare(digest.Digest, digest.Digest) diff.Diff + DiffPrint(io.Writer, diff.Diff) + Stdout() io.Writer + Stderr() io.Writer + KongExit() func(int) +} + +func Chdiff( version string, args []string, - digestService digest.Service, - stdioService util.StdIOService) { + deps ChdiffDependencies) { os.Args = args - log.SetOutput(stdioService.Stdout()) + util.InitLogger(deps.Stderr()) + + loadConfig() + + var cli struct { + Create CmdCreate `cmd:"" name:"create" aliases:"c" help:"Create digest file for PATH."` + Verify CmdVerify `cmd:"" name:"verify" aliases:"v" help:"Verify digest file for PATH."` + } ctx := kong.Parse( &cli, kong.Vars{"VERSION": version}, - kong.Description(_Description), + kong.Description(_description), kong.UsageOnError(), - kong.Writers( - stdioService.Stdout(), - stdioService.Stderr())) + kong.Writers(deps.Stdout(), deps.Stderr()), + kong.Exit(deps.KongExit()), + kong.BindTo(deps, (*ChdiffDependencies)(nil))) - switch ctx.Command() { + if ctx != nil { + ctx.FatalIfErrorf(ctx.Run(deps)) + } +} - case "create", "create ": - digestService.Create( - cli.Create.Path, - path.Join(cli.Create.Path, "out.txt"), - cli.Create.Algorithm) +func (cmd *CmdCreate) Run(deps ChdiffDependencies) error { + return deps.DigestWrite( + deps.DigestCalculate( + cmd.RootPath, + hashTypeFromAlgorithm(cmd.Algorithm)), + defaultDigestFile(cmd.cmdDigest)) +} - case "verify", "verify ": - digestService.Verify( - cli.Verify.Path, - path.Join(cli.Verify.Path, "out.txt"), - cli.Verify.Algorithm) +func (cmd *CmdVerify) Run(deps ChdiffDependencies) error { + var chain util.ChainContext + var oldDigest digest.Digest + chain.Chain(func() { + oldDigest, chain.Err = deps.DigestRead( + cmd.RootPath, + defaultDigestFile(cmd.cmdDigest)) + }).Chain(func() { + deps.DiffPrint( + deps.Stdout(), + deps.DigestCompare( + oldDigest, + deps.DigestCalculate( + cmd.RootPath, + oldDigest.Algorithm))) + }).ChainError("verify") + + return chain.Err +} + +func loadConfig() { + chain := &util.ChainContext{} + chain.Chain(func() { + chain.Err = json.Unmarshal(readConfigFile(), &common.Config) + }).Chain(func() { + util.SetLogLevelByName(common.Config.LogLevel) + util.Debug("%+v", common.Config) + }).ChainFatal("reading config") +} + +func readConfigFile() []byte { + userhome, err := os.UserHomeDir() + if err != nil { + util.Warn("can't determine user home") + return _defaultConfigJson + } + + configFile := filepath.Join(userhome, UserConfigFileName) + data, err := os.ReadFile(configFile) + if err != nil { + os.WriteFile(configFile, _defaultConfigJson, 0744) + return _defaultConfigJson + } + + return data +} + +func hashTypeFromAlgorithm(algorithm string) digest.HashType { + switch algorithm { + case "SHA512": + return digest.SHA512 + case "SHA256": + fallthrough default: - log.Fatalf("unknown command: %s", ctx.Command()) + return digest.SHA256 + } +} + +func defaultDigestFile(cmd cmdDigest) string { + digestFile := cmd.DigestFile + if len(cmd.DigestFile) == 0 { + digestFile = filepath.Join(cmd.RootPath, DefaultDigestName) } + absPath, _ := filepath.Abs(digestFile) + return absPath } diff --git a/chdiff/chdiff_test.go b/chdiff/chdiff_test.go index 59882bc..9329a4b 100644 --- a/chdiff/chdiff_test.go +++ b/chdiff/chdiff_test.go @@ -1,72 +1,262 @@ -package chdiff +package chdiff_test import ( - "path" + "fmt" + "io" "path/filepath" + "strings" "testing" - "github.com/soerenkoehler/chdiff-go/util" - "github.com/soerenkoehler/go-testutils/mockutil" + "github.com/soerenkoehler/go-chdiff/chdiff" + "github.com/soerenkoehler/go-chdiff/diff" + "github.com/soerenkoehler/go-chdiff/digest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +var ( + mockDigestLoaded = digest.Digest{Algorithm: digest.SHA256} + mockDigestCalculated = digest.Digest{} + mockDiffResult = diff.Diff{} ) -type digestServiceMock struct { - mockutil.Registry +type MockDependencies struct { + mock.Mock } -func (mock digestServiceMock) Create(dataPath, digestPath, algorithm string) { - mockutil.Register( - &mock.Registry, - mockutil.Call{"create", dataPath, digestPath, algorithm}) +func (m *MockDependencies) DigestRead(dp, df string) (digest.Digest, error) { + result := m.Called(dp, df) + return result.Get(0).(digest.Digest), result.Error(1) } -func (mock *digestServiceMock) Verify(dataPath, digestPath, algorithm string) { - mockutil.Register( - &mock.Registry, - mockutil.Call{"verify", dataPath, digestPath, algorithm}) +func (m *MockDependencies) DigestWrite(d digest.Digest, df string) error { + return m.Called(d, df).Error(0) } -func expectDigestServiceCall( - t *testing.T, - args []string, - call, dataPath, digestPath, algorithm string) { +func (m *MockDependencies) DigestCalculate(rp string, ht digest.HashType) digest.Digest { + return m.Called(rp, ht).Get(0).(digest.Digest) +} - absDataPath, _ := filepath.Abs(dataPath) - absDigestPath := path.Join(absDataPath, digestPath) +func (m *MockDependencies) DigestCompare(old, new digest.Digest) diff.Diff { + return m.Called(old, new).Get(0).(diff.Diff) +} - digestService := &digestServiceMock{ - Registry: mockutil.Registry{T: t}, - } +func (m *MockDependencies) DiffPrint(out io.Writer, d diff.Diff) { + m.Called(out, d) +} - DoMain("TEST", args, digestService, util.DefaultStdIOService{}) +func (m *MockDependencies) Stdout() io.Writer { + return m.Called().Get(0).(io.Writer) +} - mockutil.Verify( - &digestService.Registry, - mockutil.Call{call, absDataPath, absDigestPath, algorithm}) +func (m *MockDependencies) Stderr() io.Writer { + return m.Called().Get(0).(io.Writer) } -func TestCmdVerifyIsDefault(t *testing.T) { - expectDigestServiceCall(t, +func (m *MockDependencies) KongExit() func(int) { + return m.Called().Get(0).(func(int)) +} + +type TSChdiff struct { + suite.Suite + Stdout *strings.Builder + Stderr *strings.Builder + Dependencies *MockDependencies +} + +func TestSuiteRunner(t *testing.T) { + suite.Run(t, &TSChdiff{}) +} + +func (s *TSChdiff) SetupTest() { + s.Stdout = &strings.Builder{} + s.Stderr = &strings.Builder{} + s.Dependencies = &MockDependencies{} + s.Dependencies. + On("Stdout").Return(s.Stdout).Once(). + On("Stderr").Return(s.Stderr).Twice(). + On("KongExit").Return( + func(e int) { + s.Dependencies.MethodCalled("exit", e) + }) +} + +func (s *TSChdiff) TestLoadConfig() { + s.T().Setenv("HOME", "../testdata/chdiff/userhome") + s.Dependencies.Mock.On("exit", mock.Anything).Return() + + chdiff.Chdiff("TEST", []string{""}, s.Dependencies) + + s.Dependencies.AssertExpectations(s.T()) + assert.Contains( + s.T(), s.Stderr.String(), + "[D] {Exclude:{Absolute:[] Relative:[] Anywhere:[]} LogLevel:debug}") +} + +func (s *TSChdiff) TestLoadConfigCreateMissingFile() { + tempUserhome := s.T().TempDir() + s.T().Setenv("HOME", tempUserhome) + s.Dependencies.Mock.On("exit", mock.Anything).Return() + + chdiff.Chdiff("TEST", []string{""}, s.Dependencies) + + s.Dependencies.AssertExpectations(s.T()) + assert.FileExists(s.T(), filepath.Join(tempUserhome, chdiff.UserConfigFileName)) + assert.Contains( + s.T(), s.Stderr.String(), + // Attention: Kong's error message contains double space between commands + "error: expected one of \"create\", \"verify\"\n") +} + +func (s *TSChdiff) TestLoadConfigBadUserhome() { + s.T().Setenv("HOME", "") + s.Dependencies.Mock.On("exit", mock.Anything).Return() + + chdiff.Chdiff("TEST", []string{""}, s.Dependencies) + + s.Dependencies.AssertExpectations(s.T()) + assert.Contains( + s.T(), s.Stderr.String(), + "[W] can't determine user home") +} + +func (s *TSChdiff) TestLoadConfigBadJson() { + s.T().Setenv("HOME", "../testdata/chdiff/userhome-with-bad-config") + s.Dependencies.Mock.On("exit", mock.Anything).Return() + + assert.PanicsWithError(s.T(), + `/!\ reading config: invalid character 'i' looking for beginning of value`, func() { + chdiff.Chdiff("TEST", []string{""}, s.Dependencies) + }) +} + +func (s *TSChdiff) TestNoCommand() { + testErrorMessage(s, []string{""}, - "verify", - ".", - "out.txt", - "SHA256") + // Attention: Kong's error message contains double space between commands + "error: expected one of \"create\", \"verify\"\n") } -func TestCmdVerifyWithoutPath(t *testing.T) { - expectDigestServiceCall(t, +func (s *TSChdiff) TestUnknownCommand() { + testErrorMessage(s, + []string{"", "bad-command"}, + "error: unexpected argument bad-command\n") +} + +func (s *TSChdiff) TestVerifyWithoutPath() { + testDigestVerify(s, []string{"", "v"}, - "verify", ".", - "out.txt", - "SHA256") + chdiff.DefaultDigestName, + digest.SHA256) } -func TestCmdVerifyWithPath(t *testing.T) { - expectDigestServiceCall(t, +func (s *TSChdiff) TestVerifyWithPath() { + testDigestVerify(s, []string{"", "v", "x"}, - "verify", "x", - "out.txt", - "SHA256") + chdiff.DefaultDigestName, + digest.SHA256) +} + +func (s *TSChdiff) TestDigestVerifyMissingDigestFile() { + + absDataPath, _ := filepath.Abs("x") + absDigestFile := filepath.Join(absDataPath, chdiff.DefaultDigestName) + + s.Dependencies. + On("exit", mock.Anything).Return(). + On("DigestRead", absDataPath, absDigestFile).Return( + digest.Digest{}, + fmt.Errorf("no such file")) + + chdiff.Chdiff("TEST", []string{"", "v", "x"}, s.Dependencies) + + s.Dependencies.AssertExpectations(s.T()) + assert.Contains(s.T(), s.Stderr.String(), "[E] verify: no such file") +} + +func (s *TSChdiff) TestDigestCreateSHA256DefaultName() { + absDataPath, _ := filepath.Abs("x") + absDigestFile := filepath.Join(absDataPath, chdiff.DefaultDigestName) + + s.Dependencies. + On("DigestCalculate", absDataPath, digest.SHA256).Return(mockDigestCalculated). + On("DigestWrite", mockDigestCalculated, absDigestFile).Return(nil) + + chdiff.Chdiff("TEST", []string{"", "c", "-a", "SHA256", "x"}, s.Dependencies) + + s.Dependencies.AssertExpectations(s.T()) +} + +func (s *TSChdiff) TestDigestCreateSHA256ExplicitName() { + absDataPath, _ := filepath.Abs("x") + absDigestPath, _ := filepath.Abs("y") + absDigestFile := filepath.Join(absDigestPath, "explicit") + + s.Dependencies. + On("DigestCalculate", absDataPath, digest.SHA256).Return(mockDigestCalculated). + On("DigestWrite", mockDigestCalculated, absDigestFile).Return(nil) + + chdiff.Chdiff("TEST", []string{"", "c", "-a", "SHA256", "x", "-f", "y/explicit"}, s.Dependencies) + + s.Dependencies.AssertExpectations(s.T()) +} + +func (s *TSChdiff) TestDigestCreateSHA512() { + absDataPath, _ := filepath.Abs("x") + absDigestFile := filepath.Join(absDataPath, chdiff.DefaultDigestName) + + s.Dependencies. + On("DigestCalculate", absDataPath, digest.SHA512).Return(mockDigestCalculated). + On("DigestWrite", mockDigestCalculated, absDigestFile).Return(nil) + + chdiff.Chdiff("TEST", []string{"", "c", "-a", "SHA512", "x"}, s.Dependencies) + + s.Dependencies.AssertExpectations(s.T()) +} + +func (s *TSChdiff) TestDigestCreateBadAlgorithm() { + s.Dependencies.Mock.On("exit", mock.Anything).Return() + + chdiff.Chdiff("TEST", []string{"", "c", "-a", "WRONG", "x"}, s.Dependencies) + + s.Dependencies.AssertExpectations(s.T()) + assert.Contains(s.T(), s.Stderr.String(), "--algorithm must be one of \"SHA256\",\"SHA512\" but got \"WRONG\"") +} + +func testErrorMessage( + s *TSChdiff, + args []string, + expected string) { + + s.Dependencies.Mock.On("exit", mock.Anything).Return() + + chdiff.Chdiff("TEST", args, s.Dependencies) + + s.Dependencies.Mock.AssertExpectations(s.T()) + assert.Contains(s.T(), s.Stderr.String(), expected) +} + +func testDigestVerify( + s *TSChdiff, + args []string, + dataPath, digestPath string, + algorithm digest.HashType) { + + absDataPath, _ := filepath.Abs(dataPath) + absDigestFile := filepath.Join(absDataPath, chdiff.DefaultDigestName) + + s.Dependencies. + On("Stdout").Return(s.Stdout).Once(). + On("DigestRead", absDataPath, absDigestFile).Return(mockDigestLoaded, nil). + On("DigestCalculate", absDataPath, digest.SHA256).Return(mockDigestCalculated). + On("DigestCompare", mockDigestLoaded, mockDigestCalculated).Return(mockDiffResult). + On("DiffPrint", s.Stdout, mockDiffResult).Return() + + chdiff.Chdiff("TEST", args, s.Dependencies) + + s.Dependencies.AssertExpectations(s.T()) } diff --git a/chdiff/default-config.json b/chdiff/default-config.json new file mode 100644 index 0000000..aa37429 --- /dev/null +++ b/chdiff/default-config.json @@ -0,0 +1,11 @@ +{ + "exclude": { + "absolute": [ + "C:\\System Volume Information" + ], + "relative": [ + ".chdiff.txt" + ], + "anywhere": [] + } +} diff --git a/common/model.go b/common/model.go new file mode 100644 index 0000000..293c74b --- /dev/null +++ b/common/model.go @@ -0,0 +1,23 @@ +package common + +import ( + "time" + + "github.com/lestrrat-go/strftime" +) + +type Location struct { + Path string + Time time.Time +} + +var Config struct { + Exclude struct { + Absolute []string + Relative []string + Anywhere []string + } + LogLevel string +} + +var LocationTimeFormat, _ = strftime.New("%Y-%m-%d %H-%M-%S") diff --git a/diff/comparator.go b/diff/comparator.go new file mode 100644 index 0000000..ed40e0a --- /dev/null +++ b/diff/comparator.go @@ -0,0 +1,92 @@ +package diff + +import ( + "fmt" + "io" + "sort" + + "github.com/soerenkoehler/go-chdiff/common" + "github.com/soerenkoehler/go-chdiff/digest" +) + +type Comparator func(digest.Digest, digest.Digest) Diff + +type DiffPrinter func(io.Writer, Diff) + +var statusIcon map[DiffStatus]string = map[DiffStatus]string{ + Identical: " ", + Modified: "*", + Added: "+", + Removed: "-"} + +func Compare(old, new digest.Digest) Diff { + diffEntries := map[string]DiffEntry{} + + // step 1: identical, modified and removed files + for path, oldHash := range *old.Entries { + status := Removed + if newHash, newExists := (*new.Entries)[path]; newExists { + if oldHash == newHash { + status = Identical + } else { + status = Modified + } + } + diffEntries[path] = DiffEntry{ + File: path, + Status: status} + } + + // step 2: added files + for path := range *new.Entries { + if _, oldExists := (*old.Entries)[path]; !oldExists { + diffEntries[path] = DiffEntry{ + File: path, + Status: Added} + } + } + + return Diff{ + LocationA: old.Location, + LocationB: new.Location, + Entries: diffEntries} +} + +func Print(out io.Writer, diff Diff) { + fmt.Fprintf(out, + "Old: (%s) %v\nNew: (%s) %v\n", + common.LocationTimeFormat.FormatString(diff.LocationA.Time), + diff.LocationA.Path, + common.LocationTimeFormat.FormatString(diff.LocationB.Time), + diff.LocationB.Path) + + count := make(map[DiffStatus]int32, 4) + + for _, v := range diff.sortedEntries() { + count[v.Status]++ + if v.Status != Identical { + fmt.Fprintf(out, "%s %v\n", statusIcon[v.Status], v.File) + } + } + + fmt.Fprintf(out, + "Identical: %v | Modified: %v | Added: %v | Removed: %v\n", + count[Identical], count[Modified], count[Added], count[Removed]) +} + +func (diff Diff) sortedEntries() []DiffEntry { + keys := make([]string, 0, len(diff.Entries)) + values := make([]DiffEntry, 0, len(diff.Entries)) + + for k := range diff.Entries { + keys = append(keys, k) + } + + sort.Strings(keys) + + for _, k := range keys { + values = append(values, diff.Entries[k]) + } + + return values +} diff --git a/diff/comparator_test.go b/diff/comparator_test.go new file mode 100644 index 0000000..27d3f5f --- /dev/null +++ b/diff/comparator_test.go @@ -0,0 +1,142 @@ +package diff_test + +import ( + "fmt" + "os" + "strings" + "testing" + "time" + + "github.com/soerenkoehler/go-chdiff/common" + "github.com/soerenkoehler/go-chdiff/diff" + "github.com/soerenkoehler/go-chdiff/digest" + "github.com/stretchr/testify/suite" +) + +const ( + digestPath1 = "/path/to/digestfile" + digestPath2 = "/path/to/dir" + digestFile1 = "../testdata/diff/comparator/digest-old.txt" + digestFile2 = "../testdata/diff/comparator/digest-new.txt" + fileHash1 = "hash1" + fileHash2 = "hash2" +) + +var ( + digestTime1 time.Time = time.Date(2020, 3, 4, 16, 17, 18, 0, time.Local) + digestTime2 time.Time = time.Date(2022, 1, 2, 13, 14, 15, 0, time.Local) +) + +type TSComparator struct { + suite.Suite + Stdout *strings.Builder +} + +func TestSuiteRunner(t *testing.T) { + suite.Run(t, &TSComparator{}) +} + +func (s *TSComparator) SetupTest() { + s.Stdout = &strings.Builder{} +} + +func (s *TSComparator) TestOutputEmptyDiff() { + diff.Print(s.Stdout, makeDiff(s, 0, 0, 0, 0)) + + expect(s, []string{}, 0, 0, 0, 0) +} + +func (s *TSComparator) TestOutputNoChanges() { + diff.Print(s.Stdout, makeDiff(s, 2, 0, 0, 0)) + + expect(s, []string{}, 2, 0, 0, 0) +} + +func (s *TSComparator) TestOutputWithChanges() { + diff.Print(s.Stdout, makeDiff(s, 0, 3, 5, 7)) + + expect(s, []string{ + "* relPath0", + "* relPath1", + "- relPath10", + "- relPath11", + "- relPath12", + "- relPath13", + "- relPath14", + "* relPath2", + "+ relPath3", + "+ relPath4", + "+ relPath5", + "+ relPath6", + "+ relPath7", + "- relPath8", + "- relPath9", + }, 0, 3, 5, 7) +} + +func (s *TSComparator) TestCompare() { + diff.Print(s.Stdout, diff.Compare( + makeDigest(s, digestPath1, digestFile1, digestTime1), + makeDigest(s, digestPath2, digestFile2, digestTime2))) + + expect(s, + []string{ + "- f0", + "* f2", + "+ f3", + }, 1, 1, 1, 1) +} + +func makeDiff(s *TSComparator, identical, modified, added, removed int32) diff.Diff { + result := diff.Diff{ + LocationA: common.Location{ + Path: digestPath1, + Time: digestTime1}, + LocationB: common.Location{ + Path: digestPath2, + Time: digestTime2}, + Entries: map[string]diff.DiffEntry{}} + entry := 0 + add := func(count int32, status diff.DiffStatus) { + for ; count > 0; count-- { + relPath := fmt.Sprintf("relPath%d", entry) + result.Entries[relPath] = diff.DiffEntry{ + File: relPath, + Status: status, + } + entry++ + } + } + add(identical, diff.Identical) + add(modified, diff.Modified) + add(added, diff.Added) + add(removed, diff.Removed) + return result +} + +func expect(s *TSComparator, entries []string, identical, modified, added, removed int32) { + // for non-empty entries list require a final newline + entriesText := strings.Join(append(entries, ""), "\n") + + expected := fmt.Sprintf( + "Old: (%s) %v\nNew: (%s) %v\n%vIdentical: %v | Modified: %v | Added: %v | Removed: %v\n", + common.LocationTimeFormat.FormatString(digestTime1), digestPath1, + common.LocationTimeFormat.FormatString(digestTime2), digestPath2, + entriesText, + identical, modified, added, removed) + + actual := s.Stdout.String() + + if actual != expected { + s.T().Fatalf("expected:\n%v\nactual:\n%v", expected, actual) + } +} + +func makeDigest(s *TSComparator, digestPath, digestFile string, modTime time.Time) digest.Digest { + os.Chtimes(digestFile, modTime, modTime) + result, err := digest.Load(digestPath, digestFile) + if err != nil { + s.T().Fatal(err) + } + return result +} diff --git a/diff/model.go b/diff/model.go new file mode 100644 index 0000000..5d77a94 --- /dev/null +++ b/diff/model.go @@ -0,0 +1,23 @@ +package diff + +import "github.com/soerenkoehler/go-chdiff/common" + +type DiffStatus int32 + +const ( + Identical DiffStatus = iota + Modified + Added + Removed +) + +type DiffEntry struct { + File string + Status DiffStatus +} + +type Diff struct { + LocationA common.Location + LocationB common.Location + Entries map[string]DiffEntry +} diff --git a/digest/calculator.go b/digest/calculator.go new file mode 100644 index 0000000..54a6740 --- /dev/null +++ b/digest/calculator.go @@ -0,0 +1,168 @@ +package digest + +import ( + "crypto/sha256" + "crypto/sha512" + "encoding/hex" + "hash" + "io" + "io/fs" + "os" + "path/filepath" + "sync" + "time" + + "github.com/soerenkoehler/go-chdiff/common" + "github.com/soerenkoehler/go-chdiff/util" +) + +type Calculator func( + rootPath string, + algorithm HashType) Digest + +type digestEntry struct { + file string + hash string +} + +type digestContext struct { + rootPath string + algorithm HashType + waitGroup *sync.WaitGroup + digest chan digestEntry +} + +func Calculate( + rootPath string, + algorithm HashType) Digest { + + context := digestContext{ + rootPath: rootPath, + algorithm: algorithm, + waitGroup: &sync.WaitGroup{}, + digest: make(chan digestEntry), + } + + go func() { + defer close(context.digest) + + var absPath string + + chain := &util.ChainContext{} + chain.Chain(func() { + absPath, chain.Err = filepath.Abs(context.rootPath) + }).Chain(func() { + context.processPath(absPath) + context.waitGroup.Wait() + }).ChainFatal("calculate") + }() + + result := NewDigest(rootPath, time.Now()) + for entry := range context.digest { + (*result.Entries)[entry.file] = entry.hash + } + + return result +} + +func (context digestContext) processPath(path string) { + context.waitGroup.Add(1) + go func() { + defer context.waitGroup.Done() + + if context.pathExcluded(path) { + return + } + + switch info := util.Stat(path); { + case info.IsSymlink: + util.Warn("skipping symlink: %v -> %v", path, info.Target) + case info.IsDir: + context.processDir(path) + default: + context.processFile(path) + } + }() +} + +func (context digestContext) processDir(dir string) { + var entries []fs.DirEntry + + chain := &util.ChainContext{} + chain.Chain(func() { + entries, chain.Err = os.ReadDir(dir) + }).Chain(func() { + for _, entry := range entries { + context.processPath(filepath.Join(dir, entry.Name())) + } + }).ChainError("process dir") +} + +func (context digestContext) processFile(file string) { + var relativePath string + var input *os.File + + chain := &util.ChainContext{} + chain.Chain(func() { + relativePath, chain.Err = filepath.Rel(context.rootPath, file) + }).Chain(func() { + input, chain.Err = os.Open(file) + }).Chain(func() { + defer input.Close() + + hash := getNewHash(context.algorithm) + io.Copy(hash, input) + + context.digest <- digestEntry{ + file: relativePath, + hash: hex.EncodeToString(hash.Sum(nil)), + } + }).ChainError("process file") +} + +func (context digestContext) pathExcluded(path string) bool { + var relativePath string + var result bool + + chain := &util.ChainContext{} + chain.Chain(func() { + relativePath, chain.Err = filepath.Rel(context.rootPath, path) + }).Chain(func() { + result = matchAnyPattern(path, common.Config.Exclude.Absolute) || + matchAnyPattern(relativePath, common.Config.Exclude.Relative) || + matchAnyPattern(filepath.Base(relativePath), common.Config.Exclude.Anywhere) + }).ChainError("filter path") + + return result +} + +func matchAnyPattern(path string, patterns []string) bool { + for _, pattern := range patterns { + if matchPattern(pattern, path) { + return true + } + } + return false +} + +func matchPattern(path, pattern string) bool { + var chain util.ChainContext + var result bool + + chain.Chain(func() { + result, chain.Err = filepath.Match(pattern, path) + }).ChainError("match path") + + return result +} + +func getNewHash(algorithm HashType) hash.Hash { + switch algorithm { + case SHA512: + return sha512.New() + case SHA256: + fallthrough + default: + return sha256.New() + } +} diff --git a/digest/calculator_test.go b/digest/calculator_test.go new file mode 100644 index 0000000..1438fda --- /dev/null +++ b/digest/calculator_test.go @@ -0,0 +1,176 @@ +package digest_test + +import ( + "io" + "math/rand" + "os" + "path/filepath" + "testing" + + "github.com/soerenkoehler/go-chdiff/common" + "github.com/soerenkoehler/go-chdiff/digest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type testCase struct { + path string + size int64 + seed int64 + hash string + inDigest bool +} + +type TSCalculator struct { + suite.Suite + root string +} + +func TestSuiteRunner(t *testing.T) { + suite.Run(t, &TSCalculator{}) +} + +func (s *TSCalculator) SetupTest() { + s.root = s.T().TempDir() +} + +func (s *TSCalculator) TestDigest256() { + s.verifyDigest([]testCase{{ + path: "zero", + size: 0, + seed: 1, + hash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + inDigest: true, + }, { + path: "data_1", + size: 256, + seed: 1, + hash: "352adfeb0dc6e28699635c5911cf33e2e0a86aedf85a5a99bba97749000ae1c7", + inDigest: true, + }, { + path: "sub/data_1", + size: 256, + seed: 2, + hash: "1629705c76a590f2e16b8c42fa0aca9c405401fcfc794399e71f0954f1e0d50e", + inDigest: true, + }}, digest.SHA256) +} + +func (s *TSCalculator) TestDigest512() { + s.verifyDigest([]testCase{{ + path: "zero", + size: 0, + seed: 1, + hash: "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", + inDigest: true, + }, { + path: "data_1", + size: 256, + seed: 1, + hash: "aa43d14bc209ae859af792d9d0ba6ab27ab7d3802281c6a528485d44ac18f1c5019287e93ec1d3f15e843df0f05278b06471e61597b05cee6d3a347434729b88", + inDigest: true, + }, { + path: "sub/data_1", + size: 256, + seed: 2, + hash: "22c8fbc6f57675b9614933fbcbb0f93987385a201004ae4495c6ba6805dbb85d46fcb02222a60d2f151f7346d249027b5fa0684c0ded2e7d0895ece38fce2c6b", + inDigest: true, + }}, digest.SHA512) +} + +func (s *TSCalculator) TestExclude() { + common.Config.Exclude.Absolute = []string{filepath.Join(s.root, "excludeAbs")} + common.Config.Exclude.Relative = []string{"excludeRel"} + common.Config.Exclude.Anywhere = []string{"excludeAny"} + + s.verifyDigest([]testCase{{ + path: "data_1", + size: 256, + seed: 1, + hash: "352adfeb0dc6e28699635c5911cf33e2e0a86aedf85a5a99bba97749000ae1c7", + inDigest: true, + }, { + path: "excludeAbs", + size: 0, + seed: 1, + hash: "", + inDigest: false, + }, { + path: "excludeRel", + size: 0, + seed: 1, + hash: "", + inDigest: false, + }, { + path: "excludeAny", + size: 0, + seed: 1, + hash: "", + inDigest: false, + }, { + path: "sub/data_1", + size: 256, + seed: 2, + hash: "1629705c76a590f2e16b8c42fa0aca9c405401fcfc794399e71f0954f1e0d50e", + inDigest: true, + }, { + path: "sub/excludeRel", + size: 256, + seed: 3, + hash: "2e98052dd231a0217464daf09e4a203611b3490845864bf7ea93254c93ca7372", + inDigest: true, + }, { + path: "sub/excludeAny", + size: 0, + seed: 1, + hash: "", + inDigest: false, + }}, digest.SHA256) +} + +func (s *TSCalculator) verifyDigest( + testdata []testCase, + algorithm digest.HashType) { + + createData(s, testdata) + + digest := digest.Calculate(s.root, algorithm) + + count := 0 + for _, dataPoint := range testdata { + entryHash, entryInDigest := (*digest.Entries)[dataPoint.path] + assert.Equal(s.T(), dataPoint.inDigest, entryInDigest, dataPoint.path) + if entryInDigest { + assert.Equal(s.T(), dataPoint.hash, entryHash, dataPoint.path) + count++ + } + } + require.Equal(s.T(), count, len(*digest.Entries)) +} + +func createData( + s *TSCalculator, + testdata []testCase) { + + for _, dataPoint := range testdata { + file := filepath.Join(s.root, dataPoint.path) + createRandomFile(file, dataPoint.size, dataPoint.seed) + } +} + +func createRandomFile(file string, size, seed int64) { + err := os.MkdirAll(filepath.Dir(file), 0700) + if err != nil { + panic(err) + } + + out, err := os.Create(file) + if err != nil { + panic(err) + } + + defer out.Close() + in := rand.New(rand.NewSource(seed)) + io.CopyN(out, in, size) +} diff --git a/digest/digest.go b/digest/digest.go index 3a68952..5e5cd9b 100644 --- a/digest/digest.go +++ b/digest/digest.go @@ -1,173 +1,32 @@ package digest import ( - "crypto/sha256" - "crypto/sha512" - "encoding/hex" - "fmt" - "hash" - "io" - "log" - "os" - "path" - "path/filepath" - "sort" - "sync" - "time" - - "github.com/soerenkoehler/chdiff-go/util" + "github.com/soerenkoehler/go-chdiff/util" ) -type DigestEntry struct { - file string - hash string - size int64 - modTime time.Time -} - -type Digest map[string]DigestEntry - -type DigestContext struct { - rootpath string - algorithm string - waitgroup *sync.WaitGroup - digest chan DigestEntry -} - -// Service is the mockable API for the digest service. -type Service interface { - Create(dataPath, digestPath, algorithm string) - Verify(dataPath, digestPath, algorithm string) -} - -// DefaultService ist the production implementation of the digest service. -type DefaultService struct{} - -func (DefaultService) Create(dataPath, digestPath, algorithm string) { - digest := calculateDigest(dataPath, algorithm) - fmt.Printf("Saving %s\n", digestPath) - for _, k := range digest.sortedKeys() { - fmt.Print(digest[k].entryToString()) - } -} - -func (DefaultService) Verify(dataPath, digestPath, algorithm string) { - digest := calculateDigest(dataPath, algorithm) - fmt.Printf("Verify %s\n", digestPath) - for _, k := range digest.sortedKeys() { - fmt.Print(digest[k].entryToString()) - } -} - -func calculateDigest(rootpath, algorithm string) Digest { - context := DigestContext{ - rootpath: rootpath, - algorithm: algorithm, - waitgroup: &sync.WaitGroup{}, - digest: make(chan DigestEntry), +func (digest *Digest) AddFileHash(file, hash string) { + newHashType := getHashType(hash) + if newHashType == Unknown { + util.Fatal("unknown hash type: %v", hash) } - go func() { - context.processPath(context.rootpath) - context.waitgroup.Wait() - close(context.digest) - }() - - result := Digest{} - for entry := range context.digest { - result[entry.file] = entry - } - - return result -} - -func (context DigestContext) processPath(path string) { - context.waitgroup.Add(1) - go func() { - switch info := util.Stat(path); { - case info.IsSymlink: - log.Printf("[W] skipping symlink: %s => %s", path, info.Target) - case info.IsDir: - context.processDir(path) - default: - context.processFile(path) + if digest.Algorithm != newHashType { + if digest.Algorithm != Unknown { + util.Fatal("hash type mismatch old=%v new=%v", digest.Algorithm, newHashType) } - context.waitgroup.Done() - }() -} - -func (context DigestContext) processDir(dir string) { - entries, err := os.ReadDir(dir) - if err != nil { - log.Printf("[E]: %s\n", err) - } else { - for _, entry := range entries { - context.processPath(path.Join(dir, entry.Name())) - } - } -} - -func (context DigestContext) processFile(file string) { - info, err := os.Lstat(file) - if err != nil { - log.Printf("[E]: %s\n", err) - return - } - - relativePath, err := filepath.Rel(context.rootpath, file) - if err != nil { - log.Printf("[E]: %s\n", err) - return - } - - checksum, err := context.getNewHash() - if err != nil { - log.Printf("[E]: %s\n", err) - return + digest.Algorithm = newHashType } - input, err := os.Open(file) - if err != nil { - log.Printf("[E]: %s\n", err) - return - } - - defer input.Close() - io.Copy(checksum, input) - - context.digest <- DigestEntry{ - file: relativePath, - hash: hex.EncodeToString(checksum.Sum(nil)), - size: info.Size(), - modTime: info.ModTime(), - } + (*digest.Entries)[file] = hash } -func (context DigestContext) getNewHash() (hash.Hash, error) { - switch context.algorithm { - case "SHA256": - return sha256.New(), nil - case "SHA512": - return sha512.New(), nil +func getHashType(hash string) HashType { + switch len(hash) { + case 128: + return SHA512 + case 64: + return SHA256 + default: + return Unknown } - return nil, fmt.Errorf("invalid hash algorithm %v", context.algorithm) -} - -func (digest Digest) sortedKeys() []string { - keys := make([]string, 0, len(digest)) - for key := range digest { - keys = append(keys, key) - } - sort.Strings(keys) - return keys -} - -func (entry DigestEntry) entryToString() string { - return fmt.Sprintf( - "# %d %s %s\n%s *%s\n", - entry.size, - entry.modTime.Local().Format("20060102-150405"), - entry.file, - entry.hash, - entry.file) } diff --git a/digest/digest_test.go b/digest/digest_test.go index cb55733..e226c83 100644 --- a/digest/digest_test.go +++ b/digest/digest_test.go @@ -1,118 +1,43 @@ -package digest +package digest_test import ( - "path" + "crypto/rand" + "encoding/hex" "testing" + "time" - "github.com/soerenkoehler/go-testutils/datautil" - // "github.com/google/go-cmp/cmp" + "github.com/soerenkoehler/go-chdiff/digest" + "github.com/stretchr/testify/assert" ) -type testCase struct { - path string - size int64 - seed uint64 - hash string +func TestInitialHashUnknown(t *testing.T) { + d := digest.NewDigest("path", time.Now()) + assert.Equal(t, digest.Unknown, d.Algorithm) } -func TestWrongAlgorithm(t *testing.T) { - if len(createDigest(t, []testCase{{ - path: "invalid", - size: 0, - seed: 1, - hash: "invalid", - }}, "INVALID")) != 0 { - t.Fatal("invalid algorithm must not create digest entries") - } +func TestHashTypeSetOnFirstCall(t *testing.T) { + d := digest.NewDigest("path", time.Now()) + d.AddFileHash("file", createRandomHash(32)) + assert.Equal(t, digest.SHA256, d.Algorithm) } -func TestDigest256(t *testing.T) { - runDigestCalculationTest(t, []testCase{{ - path: "zero", - size: 0, - seed: 1, - hash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - }, { - path: "data_1", - size: 256, - seed: 1, - hash: "a6452fbd8c12f8df622c1ca4c567f966801fb56442aca03b4e1303e7a412a9d5", - }, { - path: "sub/data_1", - size: 256, - seed: 1, - hash: "a6452fbd8c12f8df622c1ca4c567f966801fb56442aca03b4e1303e7a412a9d5", - }}, "SHA256") +func TestInvalidHash(t *testing.T) { + assert.PanicsWithError(t, `/!\ unknown hash type: bad-hash`, func() { + d := digest.NewDigest("path", time.Now()) + d.AddFileHash("file", "bad-hash") + }) } -func TestDigest512(t *testing.T) { - runDigestCalculationTest(t, []testCase{{ - path: "zero", - size: 0, - seed: 1, - hash: "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", - }, { - path: "data_1", - size: 256, - seed: 1, - hash: "f3f00e46e5dc3819b8268afedb1221f25a4c29d3223979ede1df107155cc75bd427a5795b820fbd83fd4785899cb9de201b770a2c88a3bed90be37e82156e10b", - }, { - path: "sub/data_1", - size: 256, - seed: 1, - hash: "f3f00e46e5dc3819b8268afedb1221f25a4c29d3223979ede1df107155cc75bd427a5795b820fbd83fd4785899cb9de201b770a2c88a3bed90be37e82156e10b", - }}, "SHA512") +func TestHashTypeMismatch(t *testing.T) { + assert.PanicsWithError(t, `/!\ hash type mismatch old=1 new=2`, func() { + d := digest.NewDigest("path", time.Now()) + d.AddFileHash("file", createRandomHash(32)) + d.AddFileHash("file", createRandomHash(64)) + }) } -func runDigestCalculationTest( - t *testing.T, - data []testCase, - algorithm string) { - - verifyDigest(t, data, - createDigest(t, data, algorithm)) -} - -func createDigest( - t *testing.T, - data []testCase, - algorithm string) Digest { - - root := t.TempDir() - - for _, dataPoint := range data { - file := path.Join(root, dataPoint.path) - datautil.CreateRandomFile(file, dataPoint.size, dataPoint.seed) - } - - return calculateDigest(root, algorithm) -} - -func verifyDigest( - t *testing.T, - data []testCase, - digest Digest) { - - if len(digest) != len(data) { - t.Fatal("Digest size must match number of input data points") - } - - for _, dataPoint := range data { - expectedPath := dataPoint.path - actualPath := digest[expectedPath].file - if actualPath != expectedPath { - t.Errorf("DigestEntry.file (%v) must match Digest map key (%v)", - actualPath, - expectedPath) - } - - expectedHash := dataPoint.hash - actualHash := digest[expectedPath].hash - if actualHash != expectedHash { - t.Errorf("actual hash (%v) does not match expected hash (%v) (test file: %v)", - actualHash, - expectedHash, - expectedPath) - } - } +func createRandomHash(hashsize int) string { + hashbytes := make([]byte, hashsize) + rand.Read(hashbytes) + return hex.EncodeToString(hashbytes) } diff --git a/digest/file.go b/digest/file.go new file mode 100644 index 0000000..9fab5d0 --- /dev/null +++ b/digest/file.go @@ -0,0 +1,76 @@ +package digest + +import ( + "bufio" + "fmt" + "io/fs" + "os" + "strings" + + "github.com/soerenkoehler/go-chdiff/util" +) + +const SEPARATOR_TEXT = " " +const SEPARATOR_BINARY = " *" + +type Reader func(digestRootPath, digestFile string) (Digest, error) + +type Writer func(digest Digest, digestFile string) error + +func Load(digestPath, digestFile string) (Digest, error) { + var fileInfo fs.FileInfo + var input *os.File + var digest Digest + + chain := &util.ChainContext{} + + chain.Chain(func() { + fileInfo, chain.Err = os.Lstat(digestFile) + hackLstatErrorForWindow(&chain.Err) + }).Chain(func() { + input, chain.Err = os.Open(digestFile) + }).Chain(func() { + defer input.Close() + + digest = NewDigest(digestPath, fileInfo.ModTime().Local()) + + lines := bufio.NewScanner(input) + for lines.Scan() { + normalized := strings.Replace(lines.Text(), SEPARATOR_TEXT, SEPARATOR_BINARY, 1) + tokens := strings.SplitN(normalized, SEPARATOR_BINARY, 2) + if len(tokens) != 2 { + chain.Err = fmt.Errorf("invalid digest file") + return + } + digest.AddFileHash(tokens[1], tokens[0]) + } + }) + + return digest, chain.Err +} + +// hack for better error message under Windows +func hackLstatErrorForWindow(err *error) { + if *err != nil { + (*err).(*os.PathError).Op = "lstat" + } +} + +func Save(digest Digest, digestFile string) error { + var output *os.File + + chain := &util.ChainContext{} + + chain.Chain(func() { + output, chain.Err = os.Create(digestFile) + }).Chain(func() { + defer output.Close() + + for k, v := range *digest.Entries { + fmt.Fprintf(output, "%v%v%v\n", v, SEPARATOR_BINARY, k) + } + os.Chtimes(digestFile, digest.Location.Time, digest.Location.Time) + }) + + return chain.Err +} diff --git a/digest/file_test.go b/digest/file_test.go new file mode 100644 index 0000000..6d83476 --- /dev/null +++ b/digest/file_test.go @@ -0,0 +1,59 @@ +package digest_test + +import ( + "fmt" + "path/filepath" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/soerenkoehler/go-chdiff/digest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type TSFile struct { + suite.Suite +} + +func TestSuiteFileRunner(t *testing.T) { + suite.Run(t, &TSFile{}) +} + +func (s *TSFile) SetupTest() { +} + +func (s *TSFile) TestLoadNonexistantFile() { + path := "../testdata/digest/file" + file := filepath.Join(path, "nonexistant-digest-file.txt") + _, err := digest.Load(path, file) + assert.EqualError(s.T(), err, fmt.Sprintf("lstat %v: no such file or directory", file)) +} + +func (s *TSFile) TestLoadBadDigest1Column() { + path := "../testdata/digest/file" + file := filepath.Join(path, "bad-digest-1-column.txt") + _, err := digest.Load(path, file) + assert.EqualError(s.T(), err, "invalid digest file") +} + +func (s *TSFile) TestSaveLoad256() { + s.testSaveLoad(32) +} + +func (s *TSFile) TestSaveLoad512() { + s.testSaveLoad(64) +} + +func (s *TSFile) testSaveLoad(hashsize int) { + digestPath := s.T().TempDir() + digestTime := time.Now() + digestFile := filepath.Join(digestPath, "test-digest.txt") + expected := digest.NewDigest(digestPath, digestTime) + expected.AddFileHash("file1", createRandomHash(hashsize)) + expected.AddFileHash("file2", createRandomHash(hashsize)) + digest.Save(expected, digestFile) + actual, err := digest.Load(digestPath, digestFile) + assert.Nil(s.T(), err) + assert.True(s.T(), cmp.Equal(expected, actual), cmp.Diff(expected, actual)) +} diff --git a/digest/model.go b/digest/model.go new file mode 100644 index 0000000..f261406 --- /dev/null +++ b/digest/model.go @@ -0,0 +1,43 @@ +package digest + +import ( + "time" + + "github.com/soerenkoehler/go-chdiff/common" +) + +type FileHashes map[string]string + +type HashType int32 + +const ( + Unknown HashType = iota + SHA256 + SHA512 +) + +type Digest struct { + Location common.Location + Algorithm HashType + Entries *FileHashes +} + +func NewDigest( + digestPath string, + digestTime time.Time) Digest { + + return Digest{ + Location: common.Location{ + Path: digestPath, + Time: time.Date( + digestTime.Local().Year(), + digestTime.Local().Month(), + digestTime.Local().Day(), + digestTime.Local().Hour(), + digestTime.Local().Minute(), + digestTime.Local().Second(), + 0, + time.Local)}, + Algorithm: Unknown, + Entries: &FileHashes{}} +} diff --git a/digest/model_test.go b/digest/model_test.go new file mode 100644 index 0000000..1b4c9bd --- /dev/null +++ b/digest/model_test.go @@ -0,0 +1,15 @@ +package digest_test + +import ( + "testing" + "time" + + "github.com/soerenkoehler/go-chdiff/digest" + "github.com/stretchr/testify/assert" +) + +func TestNewDigestTruncatesTime(t *testing.T) { + digest := digest.NewDigest("", + time.Date(1999, 12, 31, 23, 59, 58, 999999, time.Local)) + assert.Equal(t, 0, digest.Location.Time.Nanosecond()) +} diff --git a/go-chdiff.code-workspace b/go-chdiff.code-workspace new file mode 100644 index 0000000..724e2c6 --- /dev/null +++ b/go-chdiff.code-workspace @@ -0,0 +1,12 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "files.exclude": { + "**/__debug*": true, + } + } +} diff --git a/go.mod b/go.mod index 2aa283b..c269d92 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,17 @@ -module github.com/soerenkoehler/chdiff-go +module github.com/soerenkoehler/go-chdiff -go 1.16 +go 1.23 require ( - github.com/alecthomas/kong v0.2.16 - github.com/soerenkoehler/go-testutils v0.0.1 + github.com/alecthomas/kong v0.9.0 + github.com/google/go-cmp v0.6.0 + github.com/lestrrat-go/strftime v1.1.0 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3bb9170..df702d3 100644 --- a/go.sum +++ b/go.sum @@ -1,144 +1,26 @@ -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/alecthomas/kong v0.2.16 h1:F232CiYSn54Tnl1sJGTeHmx4vJDNLVP2b9yCVMOQwHQ= -github.com/alecthomas/kong v0.2.16/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= -github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU= +github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA= +github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 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/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= -github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= -github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= -github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= -github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= +github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= +github.com/lestrrat-go/strftime v1.1.0 h1:gMESpZy44/4pXLO/m+sL0yBd1W6LjgjrrD4a68Gapyg= +github.com/lestrrat-go/strftime v1.1.0/go.mod h1:uzeIB52CeUJenCo1syghlugshMysrqUT51HlxphXVeI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.21.0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM= -github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/soerenkoehler/go-testutils v0.0.1 h1:UtjfiYjsVBbSkaSiCIZxzBr8ELOPZIFnL8EKG6P0gwk= -github.com/soerenkoehler/go-testutils v0.0.1/go.mod h1:t3RxsVNLxAxEtG+bZw79DyfPBmDnkwB+8XTwc+PUjRk= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= -golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= -golang.org/x/exp v0.0.0-20210514180818-737f94c0881e h1:VqVU3dsTLGDa5pW74b+xG1lvKltt4EZIUrFPeKOqV2s= -golang.org/x/exp v0.0.0-20210514180818-737f94c0881e/go.mod h1:MSdmUWF4ZWBPSUbgUX/gaau5kvnbkSs9pgtY6B9JXDE= -golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= -gonum.org/v1/gonum v0.8.1-0.20200930085651-eea0b5cb5cc9/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= -gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= -gonum.org/v1/gonum v0.9.1 h1:HCWmqqNoELL0RAQeKBXWtkp04mGk8koafcB4He6+uhc= -gonum.org/v1/gonum v0.9.1/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= -gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= -gonum.org/v1/netlib v0.0.0-20210302091547-ede94419cf37/go.mod h1:zQa7n16lh3Z6FbSTYgjG+KNhz1bA/b9t3plFEaGMp+A= -gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= -gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= -modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= -modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= -modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= -modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 8b608ef..4d5985b 100644 --- a/main.go +++ b/main.go @@ -2,19 +2,56 @@ package main import ( _ "embed" + "io" "os" - "github.com/soerenkoehler/chdiff-go/chdiff" - "github.com/soerenkoehler/chdiff-go/digest" - "github.com/soerenkoehler/chdiff-go/util" + "github.com/soerenkoehler/go-chdiff/chdiff" + "github.com/soerenkoehler/go-chdiff/diff" + "github.com/soerenkoehler/go-chdiff/digest" ) var _Version = "DEV" +type DefaultDependencies struct{} + +func (*DefaultDependencies) DigestRead(dp, df string) (digest.Digest, error) { + return digest.Load(dp, df) +} + +func (*DefaultDependencies) DigestWrite(d digest.Digest, df string) error { + return digest.Save(d, df) +} + +func (*DefaultDependencies) DigestCalculate(rp string, ht digest.HashType) digest.Digest { + return digest.Calculate(rp, ht) +} + +func (*DefaultDependencies) DigestCompare(old, new digest.Digest) diff.Diff { + return diff.Compare(old, new) +} + +func (*DefaultDependencies) DiffPrint(out io.Writer, d diff.Diff) { + diff.Print(out, d) +} + +func (*DefaultDependencies) Stdout() io.Writer { + return os.Stdout +} + +func (*DefaultDependencies) Stderr() io.Writer { + return os.Stderr +} + +func (*DefaultDependencies) KongExit() func(int) { + return os.Exit +} + func main() { - chdiff.DoMain( - _Version, - os.Args, - digest.DefaultService{}, - util.DefaultStdIOService{}) + defer func() { + if r := recover(); r != nil { + os.Exit(1) + } + }() + + chdiff.Chdiff(_Version, os.Args, &DefaultDependencies{}) } diff --git a/testdata/chdiff/userhome-with-bad-config/.chdiff-config.json b/testdata/chdiff/userhome-with-bad-config/.chdiff-config.json new file mode 100644 index 0000000..691c466 --- /dev/null +++ b/testdata/chdiff/userhome-with-bad-config/.chdiff-config.json @@ -0,0 +1 @@ +invalid-json \ No newline at end of file diff --git a/testdata/chdiff/userhome/.chdiff-config.json b/testdata/chdiff/userhome/.chdiff-config.json new file mode 100644 index 0000000..362aa42 --- /dev/null +++ b/testdata/chdiff/userhome/.chdiff-config.json @@ -0,0 +1,8 @@ +{ + "exclude": { + "absolute": [], + "relative": [], + "anywhere": [] + }, + "logLevel": "debug" +} \ No newline at end of file diff --git a/testdata/diff/comparator/digest-new.txt b/testdata/diff/comparator/digest-new.txt new file mode 100644 index 0000000..0cd3142 --- /dev/null +++ b/testdata/diff/comparator/digest-new.txt @@ -0,0 +1,3 @@ +0000000000000000000000000000000000000000000000000000000000000010 *f1 +000000000000000000000000000000000000000000000000000000000000002b *f2 +0000000000000000000000000000000000000000000000000000000000000030 *f3 diff --git a/testdata/diff/comparator/digest-old.txt b/testdata/diff/comparator/digest-old.txt new file mode 100644 index 0000000..eae611d --- /dev/null +++ b/testdata/diff/comparator/digest-old.txt @@ -0,0 +1,3 @@ +0000000000000000000000000000000000000000000000000000000000000000 *f0 +0000000000000000000000000000000000000000000000000000000000000010 *f1 +000000000000000000000000000000000000000000000000000000000000002a *f2 diff --git a/testdata/digest/file/bad-digest-1-column.txt b/testdata/digest/file/bad-digest-1-column.txt new file mode 100644 index 0000000..cd09bbf --- /dev/null +++ b/testdata/digest/file/bad-digest-1-column.txt @@ -0,0 +1 @@ +0000000000000000000000000000000000000000000000000000000000000000 diff --git a/util/chain.go b/util/chain.go new file mode 100644 index 0000000..4d53b16 --- /dev/null +++ b/util/chain.go @@ -0,0 +1,24 @@ +package util + +type ChainContext struct { + Err error +} + +func (ctx *ChainContext) Chain(f func()) *ChainContext { + if ctx.Err == nil { + f() + } + return ctx +} + +func (ctx *ChainContext) ChainError(label string) { + if ctx.Err != nil { + Error("%s: %s", label, ctx.Err.Error()) + } +} + +func (ctx *ChainContext) ChainFatal(label string) { + if ctx.Err != nil { + Fatal("%s: %s", label, ctx.Err.Error()) + } +} diff --git a/util/file.go b/util/file.go index 64a4d68..f303727 100644 --- a/util/file.go +++ b/util/file.go @@ -1,8 +1,8 @@ package util import ( - "log" "os" + "path/filepath" ) // PathInfo distilles information from FileInfo and Readlink @@ -14,17 +14,25 @@ type PathInfo struct { // Stat checks if a path is a directory, a symlink or otherwise a regular file. func Stat(path string) PathInfo { + defaultResult := PathInfo{ + IsDir: false, + IsSymlink: false, + Target: path} + info, err := os.Lstat(path) if err != nil { - log.Printf("[E]: %s\n", err) - return PathInfo{ - IsDir: false, - IsSymlink: false, - Target: path} + Error(err.Error()) + return defaultResult } - target, _ := os.Readlink(path) + + target, err := filepath.EvalSymlinks(path) + if err != nil { + Error(err.Error()) + return defaultResult + } + return PathInfo{ IsDir: info.IsDir(), - IsSymlink: 0 != (info.Mode() & os.ModeSymlink), + IsSymlink: (info.Mode() & os.ModeSymlink) != 0, Target: target} } diff --git a/util/log.go b/util/log.go new file mode 100644 index 0000000..24f926a --- /dev/null +++ b/util/log.go @@ -0,0 +1,85 @@ +package util + +import ( + "fmt" + "io" + "log" + "strings" +) + +type LogLevel int32 + +const ( + LOG_DEBUG LogLevel = iota + LOG_INFO + LOG_WARN + LOG_ERROR + LOG_FATAL + LOG_NONE +) + +var logPrefixe = map[LogLevel]string{ + LOG_DEBUG: `[D] `, + LOG_INFO: `[I] `, + LOG_WARN: `[W] `, + LOG_ERROR: `[E] `, + LOG_FATAL: `/!\ `, +} + +var levelNames = map[string]LogLevel{ + "debug": LOG_DEBUG, + "info": LOG_INFO, + "warn": LOG_WARN, + "error": LOG_ERROR, + "fatal": LOG_FATAL, + "none": LOG_NONE, +} + +var minLevel = LOG_INFO + +func InitLogger(writer io.Writer) { + log.SetOutput(writer) + log.SetFlags(log.Ltime | log.Lmsgprefix) +} + +func SetLogLevel(newLevel LogLevel) { + minLevel = newLevel +} + +func SetLogLevelByName(newLevelName string) { + if newLevel, ok := levelNames[strings.ToLower(newLevelName)]; ok { + SetLogLevel(newLevel) + } +} + +func Log(aktLevel LogLevel, format string, v ...any) string { + if minLevel > aktLevel { + return "" + } + + var msg strings.Builder + msg.WriteString(logPrefixe[aktLevel]) + msg.WriteString(fmt.Sprintf(format, v...)) + log.Println(msg.String()) + return msg.String() +} + +func Debug(format string, v ...any) { + Log(LOG_DEBUG, format, v...) +} + +func Info(format string, v ...any) { + Log(LOG_INFO, format, v...) +} + +func Warn(format string, v ...any) { + Log(LOG_WARN, format, v...) +} + +func Error(format string, v ...any) { + Log(LOG_ERROR, format, v...) +} + +func Fatal(format string, v ...any) { + panic(fmt.Errorf("%s", Log(LOG_FATAL, format, v...))) +} diff --git a/util/stdio.go b/util/stdio.go deleted file mode 100644 index 72a86c9..0000000 --- a/util/stdio.go +++ /dev/null @@ -1,31 +0,0 @@ -package util - -import ( - "io" - "os" -) - -// StdIOService provides mockable console IO. -type StdIOService interface { - Stdin() io.Reader - Stdout() io.Writer - Stderr() io.Writer -} - -// DefaultStdIOService provides the default IO from the os package. -type DefaultStdIOService struct{} - -// Stdin provides the default stdin. -func (DefaultStdIOService) Stdin() io.Reader { - return os.Stdin -} - -// Stdout provides the default stdout. -func (DefaultStdIOService) Stdout() io.Writer { - return os.Stdout -} - -// Stderr provides the default stderr. -func (DefaultStdIOService) Stderr() io.Writer { - return os.Stderr -}