Validate v2 fetch acknowledgment flow · Entire

Validate v2 fetch acknowledgment flow

0c5ff28→main· pjbgf·2mo ago·2 files·+245 added/-2 removed

Changes

2

249 unmodified lines

250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
10 unmodified lines

283
284
285
279
286
287
288
289
290
291
292
293
294
295
296
32 unmodified lines

329
330
331
318
332
333
334
335
336
337
338
339
4 unmodified lines

344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383

249 unmodified lines

func storeV2FetchPack(store storer.Storer, r io.Reader, verbose bool) error {
    reader := NewPacketReader(r)
    expectPackfile := false
    for {
        kind, payload, err := reader.ReadPacket()
        if err != nil {
            if errors.Is(err, io.EOF) {
                if expectPackfile {
                    return errors.New("expected packfile to be sent after 'ready'")
                }
                return nil
            }
            return fmt.Errorf("decode protocol v2 fetch response: %w", err)
        }
        switch kind {
        case PacketFlush:
            if expectPackfile {
                return errors.New("expected packfile to be sent after 'ready'")
            }
            return nil
        case PacketDelim, PacketResponseEnd:
            continue
10 unmodified lines

return fmt.Errorf("update object storage: %w", err)
            }
            return nil
        case "acknowledgments\n", "shallow-info\n":
        case "acknowledgments\n":
            ready, err := skipV2Acknowledgments(reader)
            if err != nil {
                return err
            }
            expectPackfile = ready
        case "shallow-info\n":
            expectPackfile = true
            if err := SkipSection(reader); err != nil {
                return err
            }
32 unmodified lines

Reader: demux,
                Closer: body,
            }, nil
        case "acknowledgments\n", "shallow-info\n":
        case "acknowledgments\n":
            if _, err := skipV2Acknowledgments(reader); err != nil {
                return nil, err
            }
        case "shallow-info\n":
            if err := SkipSection(reader); err != nil {
                return nil, err
            }
4 unmodified lines

}
}

func skipV2Acknowledgments(reader *PacketReader) (bool, error) {
    ready := false
    for {
        kind, payload, err := reader.ReadPacket()
        if err != nil {
            return false, err
        }
        switch kind {
        case PacketFlush:
            if ready {
                return false, errors.New("expected packfile to be sent after 'ready'")
            }
            return false, errors.New("protocol v2 fetch response ended without packfile after acknowledgments")
        case PacketDelim:
            if !ready {
                return false, errors.New("expected no other sections to be sent after no 'ready'")
            }
            return true, nil
        case PacketData:
            line := string(payload)
            switch {
            case line == "NAK\n", strings.HasPrefix(line, "ACK "):
                continue
            case line == "ready\n":
                ready = true
            default:
                return false, fmt.Errorf("unexpected acknowledgment line %q", strings.TrimSpace(line))
            }
        default:
            return false, fmt.Errorf("unexpected packet type %v in acknowledgments section", kind)
        }
    }
}

// --- V1 fetch implementation ---

// buildV1UploadPackBody encodes a v1 upload-pack request body for stateless-rpc HTTP.

Minternal/gitproto/fetch.go+54/-2

3 unmodified lines

4
5
6
7
8
9
10
11
12
13
823 unmodified lines

837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
136 unmodified lines

1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169

3 unmodified lines

"bytes"
    "context"
    "errors"
    "fmt"
    "io"
    "net/http"
    "strings"
    "testing"
    "time"

823 unmodified lines

}

func TestStoreV2FetchPackRejectsAcknowledgmentsWithoutReady(t *testing.T) {
    var wire bytes.Buffer
    if _, err := pktline.WriteString(&wire, "acknowledgments\n"); err != nil {
        t.Fatalf("write acknowledgments header: %v", err)
    }
    if _, err := pktline.WriteString(&wire, "NAK\n"); err != nil {
        t.Fatalf("write NAK: %v", err)
    }
    if err := pktline.WriteFlush(&wire); err != nil {
        t.Fatalf("write flush: %v", err)
    }

err := storeV2FetchPack(memory.NewStorage(), &wire, false)
    if err == nil {
        t.Fatal("expected missing packfile error")
    }
    if !strings.Contains(err.Error(), "ended without packfile after acknowledgments") {
        t.Fatalf("error = %v, want missing packfile error", err)
    }
}

func TestOpenV2PackStreamRejectsAcknowledgmentsWithoutReady(t *testing.T) {
    var wire bytes.Buffer
    if _, err := pktline.WriteString(&wire, "acknowledgments\n"); err != nil {
        t.Fatalf("write acknowledgments header: %v", err)
    }
    if _, err := pktline.WriteString(&wire, "NAK\n"); err != nil {
        t.Fatalf("write NAK: %v", err)
    }
    if err := pktline.WriteFlush(&wire); err != nil {
        t.Fatalf("write flush: %v", err)
    }

_, err := openV2PackStream(io.NopCloser(&wire), false)
    if err == nil {
        t.Fatal("expected missing packfile error")
    }
    if !strings.Contains(err.Error(), "ended without packfile after acknowledgments") {
        t.Fatalf("error = %v, want missing packfile error", err)
    }
}

func TestStoreV2FetchPackRejectsReadyWithoutPackfile(t *testing.T) {
    var wire bytes.Buffer
    if _, err := pktline.WriteString(&wire, "acknowledgments\n"); err != nil {
        t.Fatalf("write acknowledgments header: %v", err)
    }
    if _, err := pktline.WriteString(&wire, "ready\n"); err != nil {
        t.Fatalf("write ready: %v", err)
    }
    if err := pktline.WriteDelim(&wire); err != nil {
        t.Fatalf("write delim: %v", err)
    }
    if err := pktline.WriteFlush(&wire); err != nil {
        t.Fatalf("write flush: %v", err)
    }

err := storeV2FetchPack(memory.NewStorage(), &wire, false)
    if err == nil {
        t.Fatal("expected missing packfile error")
    }
    if !strings.Contains(err.Error(), "expected packfile to be sent after 'ready'") {
        t.Fatalf("error = %v, want expected packfile error", err)
    }
}

func TestFetchPackV2ManyHavesSendsDoneAndReadsPack(t *testing.T) {
    const negotiationBoundary = 16
    const refCount = negotiationBoundary + 1

ep, err := transport.ParseURL("https://example.com/repo.git")
    if err != nil {
        t.Fatalf("parse endpoint: %v", err)
    }

seenRequest := false
    conn := NewConn(ep, "source", nil, roundTripperFunc(func(req *http.Request) (*http.Response, error) {
        seenRequest = true
        body, err := io.ReadAll(req.Body)
        if err != nil {
            t.Fatalf("read request body: %v", err)
        }
        lines := readV2FetchRequestLines(t, body)
        if got := countLinesWithPrefix(lines, "want "); got != refCount {
            t.Fatalf("want lines = %d, want %d; lines=%q", got, refCount, lines)
        }
        if got := countLinesWithPrefix(lines, "have "); got != refCount {
            t.Fatalf("have lines = %d, want %d; lines=%q", got, refCount, lines)
        }
        if !containsLine(lines, "done") {
            t.Fatalf("fetch request did not send done; lines=%q", lines)
        }

var wire bytes.Buffer
        if _, err := pktline.WriteString(&wire, "packfile\n"); err != nil {
            t.Fatalf("write packfile header: %v", err)
        }
        if _, err := pktline.Write(&wire, append([]byte{1}, []byte("PACK")...)); err != nil {
            t.Fatalf("write sideband packet: %v", err)
        }
        if err := pktline.WriteFlush(&wire); err != nil {
            t.Fatalf("write flush: %v", err)
        }
        return &http.Response{
            StatusCode: http.StatusOK,
            Request:    req,
            Body:       io.NopCloser(bytes.NewReader(wire.Bytes())),
        }, nil
    }))

caps := &V2Capabilities{
        Caps: map[string]string{
            "fetch": "",
        },
    }
    desired := make(map[plumbing.ReferenceName]DesiredRef, refCount)
    targetRefs := make(map[plumbing.ReferenceName]plumbing.Hash, refCount);
    for i := 0; i < refCount; i++ {
        sourceRef := plumbing.ReferenceName(fmt.Sprintf("refs/heads/source-%02d", i))
        targetRef := plumbing.ReferenceName(fmt.Sprintf("refs/heads/target-%02d", i))
        desired[targetRef] = DesiredRef{
            SourceRef:  sourceRef,
            TargetRef:  targetRef,
            SourceHash: plumbing.NewHash(fmt.Sprintf("%040x", i+1)),
        }
        targetRefs[plumbing.ReferenceName(fmt.Sprintf("refs/haves/%02d", i))] = plumbing.NewHash(fmt.Sprintf("%040x", i+1000));
    }

rc, err := fetchPackV2(context.Background(), conn, caps, desired, targetRefs, false)
    if err != nil {
        t.Fatalf("fetchPackV2: %v", err)
    }
    got, err := io.ReadAll(rc)
    if err != nil {
        t.Fatalf("read pack stream: %v", err)
    }
    if string(got) != "PACK" {
        t.Fatalf("pack stream = %q, want PACK", got)
    }
    if closeErr := rc.Close(); closeErr != nil {
        t.Fatalf("close pack stream: %v", closeErr)
    }
    if !seenRequest {
        t.Fatal("expected fetch request")
    }
}

func TestFetchPackV2ReturnedReaderClosesBodyOnInterruption(t *testing.T) {
    ep, err := transport.ParseURL("https://example.com/repo.git")
    if err != nil {
        t.Fatalf("expected NoErrAlreadyUpToDate, got %v", err)
    }
}

func readV2FetchRequestLines(t *testing.T, body []byte) []string {
    t.Helper()

reader := NewPacketReader(bytes.NewReader(body))
    var lines []string
    for {
        kind, payload, err := reader.ReadPacket()
        if err != nil {
            t.Fatalf("read v2 fetch request: %v", err)
        }
        switch kind {
        case PacketFlush:
            return lines
        case PacketData:
            lines = append(lines, strings.TrimSuffix(string(payload), "\n"))
        case PacketDelim, PacketResponseEnd:
            continue
        default:
            t.Fatalf("unexpected packet type %v in v2 fetch request", kind)
        }
    }
}

func countLinesWithPrefix(lines []string, prefix string) int {
    var count int
    for _, line := range lines {
        if strings.HasPrefix(line, prefix) {
            count++
        }
    }
    return count
}

func containsLine(lines []string, want string) bool {
    for _, line := range lines {
        if line == want {
            return true
        }
    }
    return false
}