Commit d99ece54b6
Verified · cmc ci/build: success ci/test: success ci/vuln: success
Layout: unified · split
internal/gitutil/archive_test.go added +36
| @@ -0,0 +1,36 @@ | ||
| 1 | package gitutil | |
| 2 | ||
| 3 | import ( | |
| 4 | "bytes" | |
| 5 | "errors" | |
| 6 | "testing" | |
| 7 | ) | |
| 8 | ||
| 9 | // An archive is bounded: past MaxArchiveBytes git is stopped and the | |
| 10 | // caller gets ErrArchiveTooLarge rather than a truncated stream (#124). | |
| 11 | func TestArchiveCap(t *testing.T) { | |
| 12 | dir := t.TempDir() | |
| 13 | git(t, dir, "init", "-q", "-b", "main") | |
| 14 | write(t, dir, "big.txt", string(bytes.Repeat([]byte("gitbay archive cap test line\n"), 4000))) | |
| 15 | git(t, dir, "add", ".") | |
| 16 | git(t, dir, "commit", "-q", "-m", "base") | |
| 17 | ||
| 18 | var out bytes.Buffer | |
| 19 | if err := Archive(dir, "main", "x", &out); err != nil { | |
| 20 | t.Fatalf("archive: %v", err) | |
| 21 | } | |
| 22 | if out.Len() < 2 || out.Bytes()[0] != 0x1f || out.Bytes()[1] != 0x8b { | |
| 23 | t.Fatalf("not gzip output: %d bytes", out.Len()) | |
| 24 | } | |
| 25 | ||
| 26 | defer func(v int64) { MaxArchiveBytes = v }(MaxArchiveBytes) | |
| 27 | MaxArchiveBytes = 64 | |
| 28 | out.Reset() | |
| 29 | err := Archive(dir, "main", "x", &out) | |
| 30 | if !errors.Is(err, ErrArchiveTooLarge) { | |
| 31 | t.Fatalf("over the cap: err=%v, %d bytes written", err, out.Len()) | |
| 32 | } | |
| 33 | if int64(out.Len()) > 64 { | |
| 34 | t.Fatalf("wrote %d bytes past a 64-byte cap", out.Len()) | |
| 35 | } | |
| 36 | } | |
internal/gitutil/read.go +46 −4
| @@ -2,11 +2,14 @@ package gitutil | ||
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | 4 | "bytes" |
| 5 | "context" | |
| 6 | "errors" | |
| 5 | 7 | "fmt" |
| 6 | 8 | "io" |
| 7 | 9 | "os/exec" |
| 8 | 10 | "strconv" |
| 9 | 11 | "strings" |
| 12 | "time" | |
| 10 | 13 | ) |
| 11 | 14 | |
| 12 | 15 | type TreeEntry struct { |
| @@ -101,11 +104,50 @@ func Refs(dir, kind string) ([]Ref, error) { | ||
| 101 | 104 | return refs, nil |
| 102 | 105 | } |
| 103 | 106 | |
| 104 | // Archive streams a tar.gz of ref to w. | |
| 107 | // Bounds on one archive: a request cannot hold git and a goroutine for | |
| 108 | // longer than this, or stream more than this, however large the | |
| 109 | // repository or however slowly the client reads (#124). | |
| 110 | const archiveTimeout = 2 * time.Minute | |
| 111 | ||
| 112 | var MaxArchiveBytes int64 = 512 << 20 | |
| 113 | ||
| 114 | var ErrArchiveTooLarge = errors.New("archive exceeds the size limit") | |
| 115 | ||
| 116 | // Archive streams a tar.gz of ref to w, within archiveTimeout and | |
| 117 | // MaxArchiveBytes. Past either, git is killed and the error says which. | |
| 105 | 118 | func Archive(dir, ref, prefix string, w io.Writer) error { |
| 106 | cmd := exec.Command("git", "-C", dir, "archive", "--format=tar.gz", "--prefix="+prefix+"/", ref) | |
| 107 | cmd.Stdout = w | |
| 108 | return cmd.Run() | |
| 119 | ctx, cancel := context.WithTimeout(context.Background(), archiveTimeout) | |
| 120 | defer cancel() | |
| 121 | cmd := exec.CommandContext(ctx, "git", "-C", dir, "archive", "--format=tar.gz", "--prefix="+prefix+"/", ref) | |
| 122 | lw := &cappedWriter{w: w, left: MaxArchiveBytes, stop: cancel} | |
| 123 | cmd.Stdout = lw | |
| 124 | err := cmd.Run() | |
| 125 | switch { | |
| 126 | case lw.exceeded: | |
| 127 | return ErrArchiveTooLarge | |
| 128 | case errors.Is(ctx.Err(), context.DeadlineExceeded): | |
| 129 | return fmt.Errorf("git archive: timed out after %s", archiveTimeout) | |
| 130 | } | |
| 131 | return err | |
| 132 | } | |
| 133 | ||
| 134 | // cappedWriter passes bytes through until the cap, then stops the | |
| 135 | // producer instead of writing a truncated tail. | |
| 136 | type cappedWriter struct { | |
| 137 | w io.Writer | |
| 138 | left int64 | |
| 139 | stop func() | |
| 140 | exceeded bool | |
| 141 | } | |
| 142 | ||
| 143 | func (c *cappedWriter) Write(p []byte) (int, error) { | |
| 144 | if int64(len(p)) > c.left { | |
| 145 | c.exceeded = true | |
| 146 | c.stop() | |
| 147 | return 0, ErrArchiveTooLarge | |
| 148 | } | |
| 149 | c.left -= int64(len(p)) | |
| 150 | return c.w.Write(p) | |
| 109 | 151 | } |
| 110 | 152 | |
| 111 | 153 | // ShowPatch returns the stat+patch text for one commit. |