vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
| 1 | package lfs |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "fmt" |
| 6 | "strings" |
| 7 | "testing" |
| 8 | ) |
| 9 | |
| 10 | // frame writes one text packet the way a client does, so the seeds below are the |
| 11 | // shapes a real pusher sends rather than plausible-looking bytes. |
| 12 | func frame(payload string) string { |
| 13 | return fmt.Sprintf("%04x%s", len(payload)+4, payload) |
| 14 | } |
| 15 | |
| 16 | // FuzzPktReader drives the reader that turns a stream from a pusher into packet |
| 17 | // lengths and payloads. A length that is believed when it should not be is how a |
| 18 | // stream parser is turned into an allocation or a desynchronised connection. |
| 19 | // |
| 20 | // Properties: |
| 21 | // |
| 22 | // - It cannot return more bytes than it was given, so no packet invents data. |
| 23 | // - A control packet carries no payload. |
| 24 | // - What it reads survives being written again. The two sides of this protocol |
| 25 | // are this same file, and a round trip that disagrees means a client and the |
| 26 | // server would disagree about where a packet ends. |
| 27 | func FuzzPktReader(f *testing.F) { |
| 28 | for _, seed := range []string{ |
| 29 | frame("version https://git-lfs/1.0\n") + "0000", |
| 30 | frame("batch\n") + "0001" + frame("upload\n") + "0000", |
| 31 | frame("put\n") + "0001" + frame("123\n") + "0000", |
| 32 | frame("a") + frame("b") + "0000", |
| 33 | "", |
| 34 | "0000", |
| 35 | "0001", |
| 36 | "0002", |
| 37 | "0003", |
| 38 | "0004", |
| 39 | "0005a", |
| 40 | "zzzz", |
| 41 | "FFFF", |
| 42 | "ffff", |
| 43 | "ffffshort", |
| 44 | "0005a0000", |
| 45 | frame("x\n") + "0001" + "0000", |
| 46 | } { |
| 47 | f.Add(seed) |
| 48 | } |
| 49 | f.Fuzz(func(t *testing.T, stream string) { |
| 50 | pr := newPktReader(strings.NewReader(stream)) |
| 51 | var lines []string |
| 52 | read := 0 |
| 53 | for { |
| 54 | data, kind, err := pr.next() |
| 55 | if err != nil { |
| 56 | // Either the input ended between packets or it was refused. |
| 57 | break |
| 58 | } |
| 59 | if kind != -1 { |
| 60 | if data != nil { |
| 61 | t.Fatalf("control packet %d carried %d bytes", kind, len(data)) |
| 62 | } |
| 63 | continue |
| 64 | } |
| 65 | read += len(data) |
| 66 | lines = append(lines, strings.TrimRight(string(data), "\n")) |
| 67 | } |
| 68 | if read > len(stream) { |
| 69 | t.Fatalf("read %d bytes of payload from a %d byte stream", read, len(stream)) |
| 70 | } |
| 71 | if len(lines) == 0 { |
| 72 | return |
| 73 | } |
| 74 | // Write the lines back out and read them again. pkt appends the newline the |
| 75 | // framing counts, which is why the comparison is on the trimmed line. |
| 76 | var buf bytes.Buffer |
| 77 | pw := newPktWriter(&buf) |
| 78 | for _, line := range lines { |
| 79 | if err := pw.pkt(line); err != nil { |
| 80 | // An oversized line cannot be written back; the reader refuses it too. |
| 81 | return |
| 82 | } |
| 83 | } |
| 84 | if err := pw.flush(); err != nil { |
| 85 | t.Fatalf("flush: %v", err) |
| 86 | } |
| 87 | again := newPktReader(bytes.NewReader(buf.Bytes())) |
| 88 | for i, want := range lines { |
| 89 | data, kind, err := again.next() |
| 90 | if err != nil || kind != -1 { |
| 91 | t.Fatalf("line %d (%q) did not survive re-encoding: kind %d, err %v", i, want, kind, err) |
| 92 | } |
| 93 | if got := strings.TrimRight(string(data), "\n"); got != want { |
| 94 | t.Fatalf("line %d re-encoded as %q, want %q", i, got, want) |
| 95 | } |
| 96 | } |
| 97 | }) |
| 98 | } |
| 99 | |
| 100 | // FuzzReadBatch drives the two loops above the packet reader: the arguments up to |
| 101 | // a delimiter, then the object lines up to a flush. These are the loops that a |
| 102 | // stream can hold open, so the property is that each one terminates on the input |
| 103 | // it was given and returns no more than it contains. |
| 104 | func FuzzReadBatch(f *testing.F) { |
| 105 | for _, seed := range []string{ |
| 106 | frame("batch\n") + "0001" + frame("upload\n") + frame("oid size\n") + "0000", |
| 107 | frame("batch\n") + "0000", |
| 108 | "0001" + "0000", |
| 109 | "0001", |
| 110 | frame("a\n") + "0001" + frame("b\n") + frame("c\n") + "0000", |
| 111 | "", |
| 112 | "0000", |
| 113 | "zzzz", |
| 114 | "0005a" + "0000", |
| 115 | } { |
| 116 | f.Add(seed) |
| 117 | } |
| 118 | f.Fuzz(func(t *testing.T, stream string) { |
| 119 | args, lines, err := newPktReader(strings.NewReader(stream)).readBatch() |
| 120 | if err != nil { |
| 121 | return |
| 122 | } |
| 123 | returned := 0 |
| 124 | for _, list := range [][]string{args, lines} { |
| 125 | for _, line := range list { |
| 126 | returned += len(line) |
| 127 | if strings.HasSuffix(line, "\n") { |
| 128 | t.Fatalf("readBatch returned %q with its newline still attached", line) |
| 129 | } |
| 130 | } |
| 131 | } |
| 132 | if returned > len(stream) { |
| 133 | t.Fatalf("readBatch returned %d bytes from a %d byte stream", returned, len(stream)) |
| 134 | } |
| 135 | }) |
| 136 | } |
| 137 |