├── go.sum ├── .gitignore ├── go.mod ├── pkg ├── filler │ ├── filler.go │ └── base │ │ ├── filler_test.go │ │ └── filler.go ├── matcher │ ├── matcher.go │ ├── regexp │ │ ├── matcher_test.go │ │ └── matcher.go │ └── prefix │ │ └── matcher.go ├── worker │ ├── result.go │ └── commitmsg │ │ ├── result.go │ │ ├── worker_test.go │ │ └── worker.go └── config │ └── config.go ├── tests ├── Dockerfile ├── perf-test.sh └── test.sh ├── Dockerfile ├── .goreleaser.yaml ├── .golangci.yaml ├── .github ├── dependabot.yml └── workflows │ └── go.yaml └── cmd └── githashcrash └── main.go /go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | bin/ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Mattias-/githashcrash 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /pkg/filler/filler.go: -------------------------------------------------------------------------------- 1 | package filler 2 | 3 | type Filler interface { 4 | Fill(uint64) 5 | OutputBuffer() *[]byte 6 | } 7 | -------------------------------------------------------------------------------- /pkg/matcher/matcher.go: -------------------------------------------------------------------------------- 1 | package matcher 2 | 3 | type Matcher interface { 4 | Match([]byte) bool 5 | String() string 6 | } 7 | -------------------------------------------------------------------------------- /tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | RUN apk --no-cache add \ 4 | bash \ 5 | coreutils \ 6 | bc \ 7 | git 8 | 9 | COPY . /tests 10 | 11 | ENTRYPOINT ["bash"] 12 | CMD ["/tests/test.sh"] 13 | -------------------------------------------------------------------------------- /pkg/worker/result.go: -------------------------------------------------------------------------------- 1 | package worker 2 | 3 | type Result interface { 4 | ShellRecreateCmd() string 5 | Sha1() string 6 | Object() []byte 7 | } 8 | 9 | type Worker interface { 10 | Count() uint64 11 | Work(chan Result) 12 | } 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.20.1 2 | WORKDIR /src 3 | COPY . . 4 | RUN CGO_ENABLED=0 go build -o "githashcrash" "cmd/githashcrash/main.go" 5 | 6 | FROM alpine:latest 7 | RUN apk add git 8 | COPY --from=0 "/src/githashcrash" "/githashcrash" 9 | CMD ["/githashcrash"] 10 | -------------------------------------------------------------------------------- /tests/perf-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | cp -r /repo /tmp/repo 5 | cd /tmp/repo 6 | recreate_cmd=$(githashcrash '^000000.*' | tail -1) 7 | bash -c "$recreate_cmd" 8 | [[ $(git rev-parse HEAD) =~ ^000000.* ]] || exit 1 9 | git show -s 10 | git cat-file -p HEAD 11 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | builds: 2 | - id: githashcrash 3 | main: ./cmd/githashcrash 4 | env: 5 | - CGO_ENABLED=0 6 | 7 | brews: 8 | - name: githashcrash 9 | tap: 10 | owner: zegl 11 | name: homebrew-tap 12 | homepage: "https://github.com/zegl/githashcrash/" 13 | folder: Formula 14 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | go: '1.19' 3 | 4 | linters: 5 | enable: 6 | - gofmt 7 | - gosec 8 | - misspell 9 | 10 | linters-settings: 11 | gosec: 12 | excludes: 13 | - G401 14 | - G404 15 | - G505 16 | 17 | issues: 18 | max-issues-per-linter: 0 19 | max-same-issues: 0 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | 13 | - package-ecosystem: "docker" 14 | directory: "/" 15 | schedule: 16 | interval: "daily" 17 | -------------------------------------------------------------------------------- /pkg/matcher/regexp/matcher_test.go: -------------------------------------------------------------------------------- 1 | package regexpmatcher 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | ) 7 | 8 | func TestMatch(t *testing.T) { 9 | m := New("^30.*") 10 | s := "" 11 | if m.Match([]byte(s)) { 12 | t.Errorf("Expected hex(%s)=%s not to match %s", s, hex.EncodeToString([]byte(s)), m.String()) 13 | } 14 | 15 | s = "0000" 16 | if !m.Match([]byte(s)) { 17 | t.Errorf("Expected hex(%s)=%s to match %s", s, hex.EncodeToString([]byte(s)), m.String()) 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pkg/matcher/prefix/matcher.go: -------------------------------------------------------------------------------- 1 | package prefixmatcher 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "fmt" 7 | 8 | "github.com/Mattias-/githashcrash/pkg/matcher" 9 | ) 10 | 11 | type prefixmatcher struct { 12 | prefix []byte 13 | } 14 | 15 | func New(start string) matcher.Matcher { 16 | dst := make([]byte, hex.DecodedLen(len([]byte(start)))) 17 | _, err := hex.Decode(dst, []byte(start)) 18 | if err != nil { 19 | panic(err) 20 | } 21 | 22 | return &prefixmatcher{ 23 | prefix: dst, 24 | } 25 | } 26 | 27 | func (m prefixmatcher) String() string { 28 | return fmt.Sprintf("prefix(%s)", m.prefix) 29 | } 30 | 31 | func (m *prefixmatcher) Match(hsum []byte) bool { 32 | return bytes.HasPrefix(hsum, m.prefix) 33 | } 34 | -------------------------------------------------------------------------------- /pkg/matcher/regexp/matcher.go: -------------------------------------------------------------------------------- 1 | package regexpmatcher 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "regexp" 7 | 8 | "github.com/Mattias-/githashcrash/pkg/matcher" 9 | ) 10 | 11 | type regexpmatcher struct { 12 | regexp *regexp.Regexp 13 | encodedBuffer *[]byte 14 | exp string 15 | } 16 | 17 | func New(exp string) matcher.Matcher { 18 | var targetHash = regexp.MustCompile(exp) 19 | // Hex encoded SHA1 is 40 bytes 20 | encodedBuffer := make([]byte, 40) 21 | return ®expmatcher{ 22 | targetHash, 23 | &encodedBuffer, 24 | exp, 25 | } 26 | } 27 | 28 | func (m *regexpmatcher) String() string { 29 | return fmt.Sprintf("regexp(%s)", m.exp) 30 | } 31 | 32 | func (m *regexpmatcher) Match(hsum []byte) bool { 33 | hex.Encode(*m.encodedBuffer, hsum) 34 | return m.regexp.Match(*m.encodedBuffer) 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/go.yaml: -------------------------------------------------------------------------------- 1 | name: go 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | tags: 8 | - 'v*' 9 | pull_request: 10 | 11 | jobs: 12 | go: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v3 20 | with: 21 | go-version: 1.19 22 | 23 | - name: Build 24 | run: go build -v ./... 25 | 26 | - name: Test 27 | run: go test -v ./... 28 | 29 | - name: Lint 30 | uses: golangci/golangci-lint-action@v3 31 | with: 32 | version: latest 33 | 34 | # - name: Release 35 | # uses: goreleaser/goreleaser-action@v3 36 | # if: success() && startsWith(github.ref, 'refs/tags/v') 37 | # with: 38 | # version: latest 39 | # args: release --rm-dist 40 | # env: 41 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | -------------------------------------------------------------------------------- /pkg/worker/commitmsg/result.go: -------------------------------------------------------------------------------- 1 | package commitmsg 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "encoding/base64" 7 | "fmt" 8 | 9 | "github.com/Mattias-/githashcrash/pkg/worker" 10 | ) 11 | 12 | type result struct { 13 | sha1 string 14 | object []byte 15 | } 16 | 17 | // Compile time check that interface is implemented 18 | var _ worker.Result = result{} 19 | 20 | func (r result) Sha1() string { 21 | return r.sha1 22 | } 23 | 24 | func (r result) Object() []byte { 25 | return r.object 26 | } 27 | 28 | func (r result) ShellRecreateCmd() string { 29 | var b bytes.Buffer 30 | w := zlib.NewWriter(&b) 31 | _, err := w.Write(r.object) 32 | w.Close() 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | b64Content := base64.StdEncoding.EncodeToString(b.Bytes()) 38 | return fmt.Sprintf("mkdir -p .git/objects/%s; echo '%s' | base64 -d > .git/objects/%s/%s; git reset %s\n", r.sha1[:2], b64Content, r.sha1[:2], r.sha1[2:], r.sha1) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/filler/base/filler_test.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import "testing" 4 | 5 | func TestBase_bufferlen(t *testing.T) { 6 | f1 := New([]byte("abc")) 7 | buf1 := *(f1.OutputBuffer()) 8 | 9 | f1.Fill(0) 10 | if len(buf1) != 12 { 11 | t.Errorf("Expected len %d, got %d", 4, len(buf1)) 12 | } 13 | 14 | f1.Fill(1_000_000_000_000) 15 | if len(buf1) != 12 { 16 | t.Errorf("Expected len %d, got %d", 4, len(buf1)) 17 | } 18 | } 19 | 20 | func TestBase_seeds(t *testing.T) { 21 | f1 := New([]byte{}) 22 | buf1 := *(f1.OutputBuffer()) 23 | f1.Fill(0) 24 | 25 | f2 := New([]byte("")) 26 | buf2 := *(f2.OutputBuffer()) 27 | f2.Fill(0) 28 | if buf1[0] != buf2[0] { 29 | t.Errorf("Expected buffer values to be equal %d, %d", buf1[0], buf2[0]) 30 | } 31 | if buf1[1] != buf2[1] { 32 | t.Errorf("Expected buffer values to be equal %d, %d", buf1[1], buf2[1]) 33 | } 34 | if buf1[2] != buf2[2] { 35 | t.Errorf("Expected buffer values to be equal %d, %d", buf1[2], buf2[2]) 36 | } 37 | } 38 | func TestBase_seeds2(t *testing.T) { 39 | f1 := New([]byte("aaaaaaaaaa")) 40 | buf2 := *(f1.OutputBuffer()) 41 | f1.Fill(0) 42 | _ = buf2 43 | } 44 | -------------------------------------------------------------------------------- /pkg/filler/base/filler.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/binary" 6 | ) 7 | 8 | // Base construct a short filling bytes by switching base 9 | type base struct { 10 | seed []byte 11 | seedLen int 12 | inputBuffer []byte 13 | outputBuffer []byte 14 | } 15 | 16 | var b64encoder = base64.RawStdEncoding 17 | 18 | func New(seed []byte) *base { 19 | seedLen := 3 20 | if len(seed) > seedLen { 21 | seed = seed[:2] 22 | } 23 | 24 | rawCollisionLen := 9 25 | // collisionLen = 12 26 | collisionLen := b64encoder.EncodedLen(rawCollisionLen) 27 | 28 | inputBuffer := make([]byte, rawCollisionLen) 29 | outputBuffer := make([]byte, collisionLen) 30 | 31 | // inputBuffer always start with seed 32 | copy(inputBuffer, seed) 33 | return &base{ 34 | seed, 35 | seedLen, 36 | inputBuffer, 37 | outputBuffer, 38 | } 39 | } 40 | 41 | func (b base) OutputBuffer() *[]byte { 42 | return &b.outputBuffer 43 | } 44 | 45 | // Fill output buffer with new value 46 | func (b base) Fill(value uint64) { 47 | binary.PutUvarint(b.inputBuffer[b.seedLen:], value) 48 | // base64 encoding reduce byte size 49 | // from i to o 50 | b64encoder.Encode(b.outputBuffer, b.inputBuffer) 51 | } 52 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "math/rand" 9 | "os" 10 | "os/exec" 11 | "runtime" 12 | "time" 13 | ) 14 | 15 | type Config struct { 16 | Cpuprofile string 17 | Seed []byte 18 | Placeholder []byte 19 | Object []byte 20 | MatcherInput string 21 | Threads int 22 | MetricsPort string 23 | } 24 | 25 | func GetConfig() *Config { 26 | c := &Config{} 27 | flag.StringVar(&c.Cpuprofile, "cpuprofile", "", "write cpu profile to `file`") 28 | 29 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 30 | seed := flag.Int("seed", r.Intn(99999), "Seed bytes, default is actually random.") 31 | placeholder := flag.String("placeholder", "REPLACEME", "placeholder to mutate") 32 | flag.IntVar(&c.Threads, "threads", runtime.NumCPU(), "threads") 33 | flag.StringVar(&c.MetricsPort, "metrics-port", "", "Expose metrics on port.") 34 | 35 | flag.Parse() 36 | c.Seed = []byte(fmt.Sprintf("%x", seed)) 37 | c.Placeholder = []byte(*placeholder) 38 | 39 | args := flag.Args() 40 | c.MatcherInput = args[0] 41 | if len(args) == 2 { 42 | var err error 43 | if args[1] == "-" { 44 | c.Object, err = io.ReadAll(os.Stdin) 45 | } else { 46 | c.Object, err = os.ReadFile(args[1]) 47 | } 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | } else { 52 | obj, err := exec.Command("git", "cat-file", "-p", "HEAD").Output() 53 | if err != nil { 54 | log.Fatal("Could not run git command:", err) 55 | } 56 | c.Object = obj 57 | } 58 | return c 59 | } 60 | -------------------------------------------------------------------------------- /pkg/worker/commitmsg/worker_test.go: -------------------------------------------------------------------------------- 1 | package commitmsg 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | filler "github.com/Mattias-/githashcrash/pkg/filler/base" 9 | matcher "github.com/Mattias-/githashcrash/pkg/matcher/prefix" 10 | "github.com/Mattias-/githashcrash/pkg/worker" 11 | ) 12 | 13 | func TestWorker(t *testing.T) { 14 | hashRe := "0000" 15 | matcher := matcher.New(hashRe) 16 | obj := []byte(`tree 5fe7b5921cf9f617615100dae6cc20747e6140e6 17 | parent 0a681dcc33851637c3f5fbce17de93547c7d180b 18 | author Mattias Appelgren 1548587085 +0100 19 | committer Mattias Appelgren 1548587085 +0100 20 | 21 | Better counting 22 | 23 | Hello world: REPLACEME abc 24 | The end 25 | `) 26 | placeholder := []byte("REPLACEME") 27 | expected := "00001bfa085cd462d9d3029ccf3dc95fd6d0674a" 28 | filler := filler.New([]byte("000")) 29 | w := NewWorker(matcher, filler, obj, placeholder) 30 | results := make(chan worker.Result) 31 | go w.Work(results) 32 | var r = <-results 33 | 34 | if !strings.HasPrefix(r.Sha1(), "0000") { 35 | t.Fail() 36 | } 37 | if r.Sha1() != expected { 38 | t.Errorf("Sha1 %s is not %s", r.Sha1(), "0000") 39 | } 40 | if bytes.Equal(obj, r.Object()) { 41 | t.Fail() 42 | } 43 | if bytes.Contains(r.Object(), placeholder) { 44 | t.Fail() 45 | } 46 | 47 | } 48 | 49 | func BenchmarkWorker(b *testing.B) { 50 | hashRe := "0000" 51 | matcher := matcher.New(hashRe) 52 | obj := []byte(`tree 5fe7b5921cf9f617615100dae6cc20747e6140e6 53 | parent 0a681dcc33851637c3f5fbce17de93547c7d180b 54 | author Mattias Appelgren 1548587085 +0100 55 | committer Mattias Appelgren 1548587085 +0100 56 | 57 | Better counting 58 | 59 | Hello world: REPLACEME abc 60 | The end 61 | `) 62 | placeholder := []byte("REPLACEME") 63 | for i := 0; i < b.N; i++ { 64 | filler := filler.New([]byte("000")) 65 | w := NewWorker(matcher, filler, obj, placeholder) 66 | results := make(chan worker.Result) 67 | go w.Work(results) 68 | var r = <-results 69 | 70 | if !strings.HasPrefix(r.Sha1(), "0000") { 71 | b.Fail() 72 | } 73 | if bytes.Equal(obj, r.Object()) { 74 | b.Fail() 75 | } 76 | if bytes.Contains(r.Object(), placeholder) { 77 | b.Fail() 78 | } 79 | 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | githashcrash=$1 5 | repo=$2 6 | testsdir=$(mktemp -d) 7 | pattern='^00.*' 8 | 9 | cleanup() { 10 | rm -rf "$testsdir" 11 | } 12 | 13 | trap cleanup EXIT 14 | 15 | # Correctness 16 | cp -r "$repo" "$testsdir/repo0" && cd "$_" 17 | recreate_cmd=$($githashcrash "$pattern" | tail -1) 18 | bash -c "$recreate_cmd" &>/dev/null 19 | hash_0=$(git rev-parse HEAD) 20 | [[ $hash_0 =~ $pattern ]] || exit 1 21 | 22 | # Correctness with seed 23 | cp -r "$repo" "$testsdir/repo1" && cd "$_" 24 | recreate_cmd=$(GITHASHCRASH_SEED=11 GITHASHCRASH_THREADS=1 $githashcrash "$pattern" | tail -1) 25 | bash -c "$recreate_cmd" &>/dev/null 26 | hash_1=$(git rev-parse HEAD) 27 | [[ $hash_1 =~ $pattern ]] || exit 1 28 | 29 | # Consistency 30 | cp -r "$repo" "$testsdir/repo2" && cd "$_" 31 | recreate_cmd2=$(GITHASHCRASH_SEED=11 GITHASHCRASH_THREADS=1 $githashcrash "$pattern" | tail -1) 32 | bash -c "$recreate_cmd2" &>/dev/null 33 | hash_2=$(git rev-parse HEAD) 34 | diff <(echo "$hash_1") <(echo "$hash_2") 35 | 36 | # Recreate in another copy of repo 37 | cp -r "$repo" "$testsdir/repo3" && cd "$_" 38 | bash -c "$recreate_cmd" &>/dev/null 39 | hash_3=$(git rev-parse HEAD) 40 | diff <(echo "$hash_1") <(echo "$hash_3") 41 | 42 | # Different seed yield different hash 43 | cp -r "$repo" "$testsdir/repo4" && cd "$_" 44 | recreate_cmd4=$(GITHASHCRASH_SEED=22 GITHASHCRASH_THREADS=1 $githashcrash "$pattern" | tail -1) 45 | bash -c "$recreate_cmd4" &>/dev/null 46 | hash_4=$(git rev-parse HEAD) 47 | [[ $hash_4 =~ $pattern ]] || exit 1 48 | if diff <(echo "$hash_1") <(echo "$hash_4"); then 49 | echo "Expected different hashes" 50 | exit 1 51 | fi 52 | 53 | # Pass object as argument 54 | cp -r "$repo" "$testsdir/repo5.1" && cd "$_" 55 | obj=$(git cat-file -p HEAD) 56 | recreate_cmd=$(GITHASHCRASH_SEED=33 $githashcrash "$pattern" "$obj" | tail -1) 57 | cp -r "$repo" "$testsdir/repo5.2" && cd "$_" 58 | bash -c "$recreate_cmd" &>/dev/null 59 | hash_5=$(git rev-parse HEAD) 60 | [[ $hash_5 =~ $pattern ]] || exit 1 61 | 62 | # Replacement 63 | repl_1="REPLACEME" 64 | cp -r "$repo" "$testsdir/repo6" && cd "$_" 65 | git commit --allow-empty -m "Test commit" -m "Message message" -m "hello world $repl_1 hello world" -m "More message" 66 | git show | grep "$repl_1" 67 | recreate_cmd=$($githashcrash "$pattern" | tail -1) 68 | bash -c "$recreate_cmd" &>/dev/null 69 | hash_6=$(git rev-parse HEAD) 70 | [[ $hash_6 =~ $pattern ]] || exit 1 71 | ! git show | grep "$repl_1" 72 | 73 | 74 | echo "Tests passed!" 75 | -------------------------------------------------------------------------------- /pkg/worker/commitmsg/worker.go: -------------------------------------------------------------------------------- 1 | package commitmsg 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha1" 6 | "encoding" 7 | "encoding/hex" 8 | "fmt" 9 | "log" 10 | 11 | "github.com/Mattias-/githashcrash/pkg/worker" 12 | ) 13 | 14 | type Matcher interface { 15 | Match([]byte) bool 16 | } 17 | 18 | type Filler interface { 19 | Fill(uint64) 20 | OutputBuffer() *[]byte 21 | } 22 | 23 | type Worker struct { 24 | count uint64 25 | matcher Matcher 26 | filler Filler 27 | object []byte 28 | placeholder []byte 29 | } 30 | 31 | func (w *Worker) Count() uint64 { 32 | return w.count 33 | } 34 | 35 | func NewWorker(matcher Matcher, filler Filler, obj, placeholder []byte) *Worker { 36 | return &Worker{ 37 | count: 0, 38 | matcher: matcher, 39 | filler: filler, 40 | object: obj, 41 | placeholder: placeholder, 42 | } 43 | } 44 | 45 | func (w *Worker) Work(rc chan worker.Result) { 46 | outputBuffer := w.filler.OutputBuffer() 47 | 48 | // Split on placeholder 49 | z := bytes.SplitN(w.object, w.placeholder, 2) 50 | before := z[0] 51 | var after []byte 52 | if len(z) == 2 { 53 | after = z[1] 54 | } else { 55 | // If no placeholder is found place it last. 56 | after = []byte("\n") 57 | } 58 | 59 | // The new object: Before placeholder, placeholder, After placeholder 60 | newObjLen := len(before) + len(*outputBuffer) + len(after) 61 | 62 | b := bytes.NewBufferString(fmt.Sprintf("commit %d\x00", newObjLen)) 63 | b.Write(before) 64 | newObjectStart := b.Bytes() 65 | newObjectEnd := after 66 | 67 | // Freeze the hash of the object bytes before the placeholder 68 | first := sha1.New() 69 | first.Write(newObjectStart) 70 | marshaler, ok := first.(encoding.BinaryMarshaler) 71 | if !ok { 72 | log.Fatal("first does not implement encoding.BinaryMarshaler") 73 | } 74 | state, err := marshaler.MarshalBinary() 75 | if err != nil { 76 | log.Fatal("unable to marshal hash:", err) 77 | } 78 | 79 | for { 80 | w.count++ 81 | w.filler.Fill(w.count) 82 | 83 | second := sha1.New() 84 | unmarshaler := second.(encoding.BinaryUnmarshaler) 85 | // nolint:errcheck 86 | unmarshaler.UnmarshalBinary(state) 87 | second.Write(*outputBuffer) 88 | second.Write(newObjectEnd) 89 | hsum := second.Sum(nil) 90 | 91 | if w.matcher.Match(hsum) { 92 | b := bytes.NewBuffer(newObjectStart) 93 | b.Write(*outputBuffer) 94 | b.Write(newObjectEnd) 95 | 96 | rc <- result{ 97 | sha1: hex.EncodeToString(hsum), 98 | object: b.Bytes(), 99 | } 100 | return 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /cmd/githashcrash/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "runtime/pprof" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/Mattias-/githashcrash/pkg/config" 14 | filler "github.com/Mattias-/githashcrash/pkg/filler/base" 15 | matcher "github.com/Mattias-/githashcrash/pkg/matcher/regexp" 16 | "github.com/Mattias-/githashcrash/pkg/worker" 17 | "github.com/Mattias-/githashcrash/pkg/worker/commitmsg" 18 | ) 19 | 20 | var workers []worker.Worker 21 | 22 | func printStats(start time.Time) { 23 | sum := hashCount() 24 | elapsed := time.Since(start).Round(time.Second) 25 | log.Println("Time:", elapsed.String()) 26 | log.Println("Tested:", sum) 27 | log.Printf("%.2f MH/s\n", sum/1_000_000/elapsed.Seconds()) 28 | } 29 | 30 | func hashCount() float64 { 31 | var sum float64 32 | for _, w := range workers { 33 | sum += float64(w.Count()) 34 | } 35 | return sum 36 | } 37 | 38 | func main() { 39 | c := config.GetConfig() 40 | if c.Cpuprofile != "" { 41 | f, err := os.Create(c.Cpuprofile) 42 | if err != nil { 43 | log.Fatal("could not create CPU profile: ", err) 44 | } 45 | if err := pprof.StartCPUProfile(f); err != nil { 46 | log.Fatal("could not start CPU profile: ", err) 47 | } 48 | defer pprof.StopCPUProfile() 49 | } 50 | 51 | if c.MetricsPort != "" { 52 | go func() { 53 | http.HandleFunc("/metrics", func(w http.ResponseWriter, req *http.Request) { 54 | fmt.Fprintln(w, `# HELP hashcount_total How many Hashes has been tested. 55 | # TYPE hashcount_total counter 56 | hashcount_total`, hashCount()) 57 | }) 58 | log.Fatal(http.ListenAndServe(c.MetricsPort, nil)) // #nosec G114 59 | }() 60 | } 61 | 62 | // listening OS shutdown singal 63 | signalChan := make(chan os.Signal, 1) 64 | signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) 65 | go func() { 66 | <-signalChan 67 | log.Println("Got shutdown signal.") 68 | pprof.StopCPUProfile() 69 | os.Exit(1) 70 | }() 71 | 72 | log.Println("Workers:", c.Threads) 73 | 74 | results := make(chan worker.Result) 75 | for i := 0; i < c.Threads; i++ { 76 | filler := filler.New(append(c.Seed[:2], byte(i))) 77 | matcher := matcher.New(c.MatcherInput) 78 | w := commitmsg.NewWorker(matcher, filler, c.Object, c.Placeholder) 79 | workers = append(workers, w) 80 | go w.Work(results) 81 | } 82 | 83 | // Log stats during execution 84 | start := time.Now() 85 | ticker := time.NewTicker(time.Second * 2) 86 | go func() { 87 | for range ticker.C { 88 | printStats(start) 89 | } 90 | }() 91 | 92 | result := <-results 93 | 94 | ticker.Stop() 95 | printStats(start) 96 | 97 | log.Println("Found:", result.Sha1()) 98 | fmt.Println(result.ShellRecreateCmd()) 99 | } 100 | --------------------------------------------------------------------------------