diff options
| author | 2026-03-25 16:22:03 +0000 | |
|---|---|---|
| committer | 2026-03-25 16:22:03 +0000 | |
| commit | 311edcd50f3a84f4b860bde3cb887451d74eaa11 (patch) | |
| tree | be7aa5e9a51e636358f33b1c90637b5024b70dc3 /network/protocol/v0v1/server | |
| parent | README: Split off contrib, benchmarks, remove history for now I guess, etc. (diff) | |
| signature | No signature | |
network/protocol: Rename from protocol v0.1.110
Diffstat (limited to 'network/protocol/v0v1/server')
19 files changed, 1724 insertions, 0 deletions
diff --git a/network/protocol/v0v1/server/advertise.go b/network/protocol/v0v1/server/advertise.go new file mode 100644 index 00000000..be1b1f02 --- /dev/null +++ b/network/protocol/v0v1/server/advertise.go @@ -0,0 +1,55 @@ +package server + +import ( + "fmt" + "strings" + + objectid "codeberg.org/lindenii/furgit/object/id" +) + +// AdvertiseRefs writes one server ref advertisement. +func (session *Session) AdvertiseRefs(ad Advertisement, capabilityTokens []string) error { + if session.opts.Version == Version1 { + err := session.enc.WriteData([]byte("version 1\n")) + if err != nil { + return err + } + } + + capList := strings.Join(capabilityTokens, " ") + + refs := sortAdvertisedRefs(ad.Refs) + if len(refs) == 0 { + line := fmt.Sprintf("%s capabilities^{}\x00%s\n", objectid.Zero(session.opts.Algorithm), capList) + + err := session.enc.WriteData([]byte(line)) + if err != nil { + return err + } + + return session.WriteFlush() + } + + for i, entry := range refs { + line := fmt.Sprintf("%s %s", entry.ID, entry.Name) + if i == 0 { + line += "\x00" + capList + } + + err := session.enc.WriteData([]byte(line + "\n")) + if err != nil { + return err + } + + if entry.Peeled != nil { + peeled := fmt.Sprintf("%s %s^{}\n", *entry.Peeled, entry.Name) + + err = session.enc.WriteData([]byte(peeled)) + if err != nil { + return err + } + } + } + + return session.WriteFlush() +} diff --git a/network/protocol/v0v1/server/advertise_test.go b/network/protocol/v0v1/server/advertise_test.go new file mode 100644 index 00000000..3aac7056 --- /dev/null +++ b/network/protocol/v0v1/server/advertise_test.go @@ -0,0 +1,101 @@ +package server_test + +import ( + "strings" + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + server "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" + objectid "codeberg.org/lindenii/furgit/object/id" +) + +func TestAdvertiseRefsWritesVersionOneHeadCapsAndPeeledTag(t *testing.T) { + t.Parallel() + + //nolint:thelper + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { + t.Parallel() + + headID := mustHexID(t, algo, "1") + tagID := mustHexID(t, algo, "2") + peeledID := mustHexID(t, algo, "3") + mainID := mustHexID(t, algo, "4") + + var out bufferWriteFlusher + + session := server.NewSession( + strings.NewReader(""), + &out, + server.Options{ + Version: server.Version1, + Algorithm: algo, + }, + ) + + err := session.AdvertiseRefs(server.Advertisement{ + Refs: []server.AdvertisedRef{ + {Name: "refs/tags/v1", ID: tagID, Peeled: &peeledID}, + {Name: "HEAD", ID: headID}, + {Name: "refs/heads/main", ID: mainID}, + }, + }, []string{ + "report-status", + "delete-refs", + "object-format=" + algo.String(), + "agent=furgit-test/1", + }) + if err != nil { + t.Fatalf("AdvertiseRefs: %v", err) + } + + got := out.String() + wantParts := []string{ + "000eversion 1\n", + headID.String() + " HEAD\x00report-status delete-refs object-format=" + algo.String() + " agent=furgit-test/1\n", + mainID.String() + " refs/heads/main\n", + tagID.String() + " refs/tags/v1\n", + peeledID.String() + " refs/tags/v1^{}\n", + "0000", + } + + for _, part := range wantParts { + if !strings.Contains(got, part) { + t.Fatalf("advertisement missing %q in %q", part, got) + } + } + }) +} + +func TestAdvertiseRefsWritesNoRefsCapabilitiesLine(t *testing.T) { + t.Parallel() + + //nolint:thelper + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { + t.Parallel() + + var out bufferWriteFlusher + + session := server.NewSession( + strings.NewReader(""), + &out, + server.Options{ + Algorithm: algo, + }, + ) + + err := session.AdvertiseRefs(server.Advertisement{}, []string{ + "report-status", + "object-format=" + algo.String(), + }) + if err != nil { + t.Fatalf("AdvertiseRefs: %v", err) + } + + got := out.String() + + want := objectid.Zero(algo).String() + " capabilities^{}\x00report-status object-format=" + algo.String() + "\n" + if !strings.Contains(got, want) { + t.Fatalf("unexpected no-refs advertisement %q", got) + } + }) +} diff --git a/network/protocol/v0v1/server/advertised_ref.go b/network/protocol/v0v1/server/advertised_ref.go new file mode 100644 index 00000000..cf6ddcc8 --- /dev/null +++ b/network/protocol/v0v1/server/advertised_ref.go @@ -0,0 +1,22 @@ +package server + +import objectid "codeberg.org/lindenii/furgit/object/id" + +// AdvertisedRef is one ref entry in one v0/v1 server advertisement. +type AdvertisedRef struct { + // Name is the advertised reference name. It may be HEAD or one full + // reference name. + Name string + // ID is the object ID currently advertised for Name. + ID objectid.ObjectID + // Peeled is the peeled annotated-tag target when available. + // + // If set, advertisement writes one immediate "<name>^{}" line after the + // main entry, matching Git's advertisement rules. + Peeled *objectid.ObjectID +} + +// Advertisement is one server-side ref advertisement. +type Advertisement struct { + Refs []AdvertisedRef +} diff --git a/network/protocol/v0v1/server/doc.go b/network/protocol/v0v1/server/doc.go new file mode 100644 index 00000000..ea0b3f18 --- /dev/null +++ b/network/protocol/v0v1/server/doc.go @@ -0,0 +1,2 @@ +// Package server implements shared server-side Git protocol v0/v1 framing. +package server diff --git a/network/protocol/v0v1/server/errors.go b/network/protocol/v0v1/server/errors.go new file mode 100644 index 00000000..6a456234 --- /dev/null +++ b/network/protocol/v0v1/server/errors.go @@ -0,0 +1,18 @@ +package server + +// ProtocolError reports one malformed or unsupported protocol input. +type ProtocolError struct { + Reason string +} + +// Error returns the formatted error string. +func (err *ProtocolError) Error() string { + return "protocol/v0v1/server: protocol error: " + err.Reason +} + +// ErrUnexpectedPacket reports one unexpected pkt-line control packet. +var ErrUnexpectedPacket = &ProtocolError{Reason: "unexpected control packet"} + +// ErrSideBandNotEnabled reports one attempt to write sideband frames without a +// negotiated side-band-64k session. +var ErrSideBandNotEnabled = &ProtocolError{Reason: "side-band-64k not enabled"} diff --git a/network/protocol/v0v1/server/frame.go b/network/protocol/v0v1/server/frame.go new file mode 100644 index 00000000..ad2a0801 --- /dev/null +++ b/network/protocol/v0v1/server/frame.go @@ -0,0 +1,20 @@ +package server + +import "codeberg.org/lindenii/furgit/network/protocol/pktline" + +// FrameType identifies one low-level v0/v1 server pkt-line frame type. +type FrameType = pktline.PacketType + +const ( + // FrameData is one data pkt-line. + FrameData = pktline.PacketData + // FrameFlush is one flush-pkt. + FrameFlush = pktline.PacketFlush + // FrameDelim is one delim-pkt. + FrameDelim = pktline.PacketDelim + // FrameResponseEnd is one response-end-pkt. + FrameResponseEnd = pktline.PacketResponseEnd +) + +// Frame is one decoded low-level pkt-line frame. +type Frame = pktline.Frame diff --git a/network/protocol/v0v1/server/helpers.go b/network/protocol/v0v1/server/helpers.go new file mode 100644 index 00000000..9a62f714 --- /dev/null +++ b/network/protocol/v0v1/server/helpers.go @@ -0,0 +1,29 @@ +package server + +import ( + "slices" +) + +func sortAdvertisedRefs(refs []AdvertisedRef) []AdvertisedRef { + out := append([]AdvertisedRef(nil), refs...) + slices.SortFunc(out, func(left, right AdvertisedRef) int { + if left.Name == "HEAD" && right.Name != "HEAD" { + return -1 + } + + if left.Name != "HEAD" && right.Name == "HEAD" { + return 1 + } + + switch { + case left.Name < right.Name: + return -1 + case left.Name > right.Name: + return 1 + default: + return 0 + } + }) + + return out +} diff --git a/network/protocol/v0v1/server/helpers_test.go b/network/protocol/v0v1/server/helpers_test.go new file mode 100644 index 00000000..261bbdc5 --- /dev/null +++ b/network/protocol/v0v1/server/helpers_test.go @@ -0,0 +1,28 @@ +package server_test + +import ( + "bytes" + "strings" + "testing" + + objectid "codeberg.org/lindenii/furgit/object/id" +) + +type bufferWriteFlusher struct { + bytes.Buffer +} + +func (bufferWriteFlusher) Flush() error { + return nil +} + +func mustHexID(tb testing.TB, algo objectid.Algorithm, digit string) objectid.ObjectID { + tb.Helper() + + id, err := objectid.ParseHex(algo, strings.Repeat(digit, algo.HexLen())) + if err != nil { + tb.Fatalf("objectid.ParseHex(%q): %v", strings.Repeat(digit, algo.HexLen()), err) + } + + return id +} diff --git a/network/protocol/v0v1/server/receivepack/capabilities.go b/network/protocol/v0v1/server/receivepack/capabilities.go new file mode 100644 index 00000000..e0ff51a3 --- /dev/null +++ b/network/protocol/v0v1/server/receivepack/capabilities.go @@ -0,0 +1,192 @@ +package receivepack + +import ( + "fmt" + "slices" + "strings" + + objectid "codeberg.org/lindenii/furgit/object/id" +) + +// Capabilities describes one receive-pack capability set. +type Capabilities struct { + ReportStatus bool + ReportStatusV2 bool + DeleteRefs bool + SideBand64K bool + Quiet bool + Atomic bool + OfsDelta bool + PushOptions bool + PushCertNonce string + ObjectFormat objectid.Algorithm + SessionID string + Agent string +} + +// Normalize returns one normalized copy of caps. +func (caps Capabilities) Normalize(defaultAlgorithm objectid.Algorithm) Capabilities { + if caps.ObjectFormat == objectid.AlgorithmUnknown { + caps.ObjectFormat = defaultAlgorithm + } + + return caps +} + +// Tokens returns capabilities in Git advertisement order. +func (caps Capabilities) Tokens(defaultAlgorithm objectid.Algorithm) []string { + caps = caps.Normalize(defaultAlgorithm) + + tokens := make([]string, 0, 11) + if caps.ReportStatus { + tokens = append(tokens, "report-status") + } + + if caps.ReportStatusV2 { + tokens = append(tokens, "report-status-v2") + } + + if caps.DeleteRefs { + tokens = append(tokens, "delete-refs") + } + + if caps.SideBand64K { + tokens = append(tokens, "side-band-64k") + } + + if caps.Quiet { + tokens = append(tokens, "quiet") + } + + if caps.Atomic { + tokens = append(tokens, "atomic") + } + + if caps.OfsDelta { + tokens = append(tokens, "ofs-delta") + } + + if caps.PushCertNonce != "" { + tokens = append(tokens, "push-cert="+caps.PushCertNonce) + } + + if caps.PushOptions { + tokens = append(tokens, "push-options") + } + + if caps.SessionID != "" { + tokens = append(tokens, "session-id="+caps.SessionID) + } + + if caps.ObjectFormat != objectid.AlgorithmUnknown { + tokens = append(tokens, "object-format="+caps.ObjectFormat.String()) + } + + if caps.Agent != "" { + tokens = append(tokens, "agent="+caps.Agent) + } + + return tokens +} + +func (caps Capabilities) supportsToken(token string, defaultAlgorithm objectid.Algorithm) bool { + name, value, _ := strings.Cut(token, "=") + + switch name { + case "report-status": + return caps.ReportStatus && value == "" + case "report-status-v2": + return caps.ReportStatusV2 && value == "" + case "delete-refs": + return caps.DeleteRefs && value == "" + case "side-band-64k": + return caps.SideBand64K && value == "" + case "quiet": + return caps.Quiet && value == "" + case "atomic": + return caps.Atomic && value == "" + case "ofs-delta": + return caps.OfsDelta && value == "" + case "push-options": + return caps.PushOptions && value == "" + case "push-cert": + return caps.PushCertNonce != "" && value != "" + case "object-format": + if value == "" { + return false + } + + algo, ok := objectid.ParseAlgorithm(value) + + return ok && algo == caps.Normalize(defaultAlgorithm).ObjectFormat + case "session-id": + return caps.SessionID != "" && value != "" + case "agent": + return caps.Agent != "" && value != "" + default: + return false + } +} + +func parseCapabilityList(s string) ([]string, error) { + s = strings.TrimSuffix(s, "\n") + if s == "" { + return nil, nil + } + + tokens := strings.Fields(s) + if slices.Contains(tokens, "") { + return nil, &ProtocolError{Reason: "empty capability token"} + } + + return tokens, nil +} + +func parseRequestedCapabilities( + tokens []string, + supported Capabilities, + defaultAlgorithm objectid.Algorithm, +) (Capabilities, error) { + var requested Capabilities + + requested.ObjectFormat = defaultAlgorithm + + for _, token := range tokens { + if !supported.supportsToken(token, defaultAlgorithm) { + return Capabilities{}, &ProtocolError{ + Reason: fmt.Sprintf("unsupported capability %q", token), + } + } + + name, value, _ := strings.Cut(token, "=") + switch name { + case "report-status": + requested.ReportStatus = true + case "report-status-v2": + requested.ReportStatusV2 = true + case "delete-refs": + requested.DeleteRefs = true + case "side-band-64k": + requested.SideBand64K = true + case "quiet": + requested.Quiet = true + case "atomic": + requested.Atomic = true + case "ofs-delta": + requested.OfsDelta = true + case "push-options": + requested.PushOptions = true + case "push-cert": + requested.PushCertNonce = value + case "object-format": + algo, _ := objectid.ParseAlgorithm(value) + requested.ObjectFormat = algo + case "session-id": + requested.SessionID = value + case "agent": + requested.Agent = value + } + } + + return requested, nil +} diff --git a/network/protocol/v0v1/server/receivepack/doc.go b/network/protocol/v0v1/server/receivepack/doc.go new file mode 100644 index 00000000..65793831 --- /dev/null +++ b/network/protocol/v0v1/server/receivepack/doc.go @@ -0,0 +1,2 @@ +// Package receivepack implements the receive-pack-specific server side of Git protocol v0/v1. +package receivepack diff --git a/network/protocol/v0v1/server/receivepack/errors.go b/network/protocol/v0v1/server/receivepack/errors.go new file mode 100644 index 00000000..d89f8959 --- /dev/null +++ b/network/protocol/v0v1/server/receivepack/errors.go @@ -0,0 +1,11 @@ +package receivepack + +// ProtocolError reports one malformed or unsupported receive-pack protocol input. +type ProtocolError struct { + Reason string +} + +// Error returns the formatted error string. +func (err *ProtocolError) Error() string { + return "protocol/v0v1/server/receivepack: protocol error: " + err.Reason +} diff --git a/network/protocol/v0v1/server/receivepack/helpers_test.go b/network/protocol/v0v1/server/receivepack/helpers_test.go new file mode 100644 index 00000000..5db8e6a6 --- /dev/null +++ b/network/protocol/v0v1/server/receivepack/helpers_test.go @@ -0,0 +1,28 @@ +package receivepack_test + +import ( + "bytes" + "strings" + "testing" + + objectid "codeberg.org/lindenii/furgit/object/id" +) + +type bufferWriteFlusher struct { + bytes.Buffer +} + +func (bufferWriteFlusher) Flush() error { + return nil +} + +func mustHexID(tb testing.TB, algo objectid.Algorithm, digit string) objectid.ObjectID { + tb.Helper() + + id, err := objectid.ParseHex(algo, strings.Repeat(digit, algo.HexLen())) + if err != nil { + tb.Fatalf("objectid.ParseHex(%q): %v", strings.Repeat(digit, algo.HexLen()), err) + } + + return id +} diff --git a/network/protocol/v0v1/server/receivepack/parse_test.go b/network/protocol/v0v1/server/receivepack/parse_test.go new file mode 100644 index 00000000..7c7cbe3b --- /dev/null +++ b/network/protocol/v0v1/server/receivepack/parse_test.go @@ -0,0 +1,255 @@ +package receivepack_test + +import ( + "errors" + "strings" + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/network/protocol/pktline" + common "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" + receivepack "codeberg.org/lindenii/furgit/network/protocol/v0v1/server/receivepack" + objectid "codeberg.org/lindenii/furgit/object/id" +) + +func TestReadRequestParsesCommandsAndPushOptions(t *testing.T) { + t.Parallel() + + //nolint:thelper + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { + t.Parallel() + + oldZero := objectid.Zero(algo).String() + oneID := mustHexID(t, algo, "1") + + var wire bufferWriteFlusher + + enc := pktline.NewEncoder(&wire) + + err := enc.WriteData([]byte( + oldZero + " " + oneID.String() + " refs/heads/main\x00report-status push-options object-format=" + algo.String() + "\n", + )) + if err != nil { + t.Fatalf("WriteData(first): %v", err) + } + + err = enc.WriteData([]byte( + oneID.String() + " " + oldZero + " refs/heads/old\n", + )) + if err != nil { + t.Fatalf("WriteData(second): %v", err) + } + + err = enc.WriteFlush() + if err != nil { + t.Fatalf("WriteFlush(commands): %v", err) + } + + err = enc.WriteData([]byte("ci.skip\n")) + if err != nil { + t.Fatalf("WriteData(push-option): %v", err) + } + + err = enc.WriteFlush() + if err != nil { + t.Fatalf("WriteFlush(push-options): %v", err) + } + + base := common.NewSession(strings.NewReader(wire.String()), &bufferWriteFlusher{}, common.Options{ + Algorithm: algo, + }) + session := receivepack.NewSession(base, receivepack.Capabilities{ + ReportStatus: true, + PushOptions: true, + ObjectFormat: algo, + }) + + req, err := session.ReadRequest() + if err != nil { + t.Fatalf("ReadRequest: %v", err) + } + + if len(req.Commands) != 2 { + t.Fatalf("len(req.Commands) = %d, want 2", len(req.Commands)) + } + + if !req.Capabilities.ReportStatus || !req.Capabilities.PushOptions { + t.Fatalf("capabilities = %#v", req.Capabilities) + } + + if len(req.PushOptions) != 1 || req.PushOptions[0] != "ci.skip" { + t.Fatalf("push options = %#v", req.PushOptions) + } + + if !req.PackExpected { + t.Fatalf("PackExpected = false, want true") + } + + if req.DeleteOnly { + t.Fatalf("DeleteOnly = true, want false") + } + }) +} + +func TestReadRequestDeleteOnlyDoesNotExpectPack(t *testing.T) { + t.Parallel() + + //nolint:thelper + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { + t.Parallel() + + oneID := mustHexID(t, algo, "1") + + var wire bufferWriteFlusher + + enc := pktline.NewEncoder(&wire) + + err := enc.WriteData([]byte( + oneID.String() + " " + objectid.Zero(algo).String() + " refs/heads/old\x00delete-refs object-format=" + algo.String() + "\n", + )) + if err != nil { + t.Fatalf("WriteData: %v", err) + } + + err = enc.WriteFlush() + if err != nil { + t.Fatalf("WriteFlush: %v", err) + } + + base := common.NewSession(strings.NewReader(wire.String()), &bufferWriteFlusher{}, common.Options{ + Algorithm: algo, + }) + session := receivepack.NewSession(base, receivepack.Capabilities{ + DeleteRefs: true, + ObjectFormat: algo, + }) + + req, err := session.ReadRequest() + if err != nil { + t.Fatalf("ReadRequest: %v", err) + } + + if req.PackExpected { + t.Fatalf("PackExpected = true, want false") + } + + if !req.DeleteOnly { + t.Fatalf("DeleteOnly = false, want true") + } + }) +} + +func TestReadRequestRejectsUnsupportedCapability(t *testing.T) { + t.Parallel() + + //nolint:thelper + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { + t.Parallel() + + oneID := mustHexID(t, algo, "1") + + var wire bufferWriteFlusher + + enc := pktline.NewEncoder(&wire) + + err := enc.WriteData([]byte( + objectid.Zero(algo).String() + " " + oneID.String() + " refs/heads/main\x00atomic object-format=" + algo.String() + "\n", + )) + if err != nil { + t.Fatalf("WriteData: %v", err) + } + + err = enc.WriteFlush() + if err != nil { + t.Fatalf("WriteFlush: %v", err) + } + + base := common.NewSession(strings.NewReader(wire.String()), &bufferWriteFlusher{}, common.Options{ + Algorithm: algo, + }) + session := receivepack.NewSession(base, receivepack.Capabilities{ObjectFormat: algo}) + + _, err = session.ReadRequest() + if err == nil { + t.Fatalf("ReadRequest error = nil, want error") + } + + protocolErr, ok := errors.AsType[*receivepack.ProtocolError](err) + if !ok { + t.Fatalf("errors.AsType[*receivepack.ProtocolError](%T) = false", err) + } + + if !strings.Contains(protocolErr.Reason, "unsupported capability") { + t.Fatalf("ProtocolError.Reason = %q", protocolErr.Reason) + } + }) +} + +func TestReadRequestParsesPushCertificate(t *testing.T) { + t.Parallel() + + //nolint:thelper + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { + t.Parallel() + + oneID := mustHexID(t, algo, "1") + + var wire bufferWriteFlusher + + enc := pktline.NewEncoder(&wire) + + err := enc.WriteData([]byte("push-cert\x00push-cert=nonce object-format=" + algo.String() + "\n")) + if err != nil { + t.Fatalf("WriteData(push-cert): %v", err) + } + + lines := []string{ + "certificate version 0.1\n", + "pusher Example <example@example.com>\n", + "nonce nonce\n", + "push-option ci.skip\n", + "\n", + objectid.Zero(algo).String() + " " + oneID.String() + " refs/heads/main\n", + "-----BEGIN PGP SIGNATURE-----\n", + "abcdef\n", + "push-cert-end\n", + } + + for _, line := range lines { + err = enc.WriteData([]byte(line)) + if err != nil { + t.Fatalf("WriteData(%q): %v", line, err) + } + } + + err = enc.WriteFlush() + if err != nil { + t.Fatalf("WriteFlush: %v", err) + } + + base := common.NewSession(strings.NewReader(wire.String()), &bufferWriteFlusher{}, common.Options{ + Algorithm: algo, + }) + session := receivepack.NewSession(base, receivepack.Capabilities{ + PushCertNonce: "server-nonce", + ObjectFormat: algo, + }) + + req, err := session.ReadRequest() + if err != nil { + t.Fatalf("ReadRequest: %v", err) + } + + if req.PushCert == nil { + t.Fatalf("PushCert = nil, want parsed certificate") + } + + if len(req.Commands) != 1 { + t.Fatalf("len(req.Commands) = %d, want 1", len(req.Commands)) + } + + if len(req.PushCert.EmbeddedOption) != 1 || req.PushCert.EmbeddedOption[0] != "ci.skip" { + t.Fatalf("embedded options = %#v", req.PushCert.EmbeddedOption) + } + }) +} diff --git a/network/protocol/v0v1/server/receivepack/report_status.go b/network/protocol/v0v1/server/receivepack/report_status.go new file mode 100644 index 00000000..fbe4fb4f --- /dev/null +++ b/network/protocol/v0v1/server/receivepack/report_status.go @@ -0,0 +1,185 @@ +package receivepack + +import ( + "fmt" + + "codeberg.org/lindenii/furgit/network/protocol/pktline" +) + +// WriteReportStatus writes one classic report-status response. +func (session *Session) WriteReportStatus(result ReportStatusResult) error { + unpackResult := "ok" + if result.UnpackError != "" { + unpackResult = result.UnpackError + } + + if !session.negotiated.SideBand64K { + err := session.base.WriteData(fmt.Appendf(nil, "unpack %s\n", unpackResult)) + if err != nil { + return err + } + + for _, command := range result.Commands { + line := fmt.Sprintf("ok %s\n", command.Name) + if command.Error != "" { + line = fmt.Sprintf("ng %s %s\n", command.Name, command.Error) + } + + err = session.base.WriteData([]byte(line)) + if err != nil { + return err + } + } + + return session.base.WriteFlush() + } + + buf, err := pktline.AppendData(nil, fmt.Appendf(nil, "unpack %s\n", unpackResult)) + if err != nil { + return err + } + + for _, command := range result.Commands { + line := fmt.Sprintf("ok %s\n", command.Name) + if command.Error != "" { + line = fmt.Sprintf("ng %s %s\n", command.Name, command.Error) + } + + buf, err = pktline.AppendData(buf, []byte(line)) + if err != nil { + return err + } + } + + buf = pktline.AppendFlushPkt(buf) + + w := session.base.PrimaryDataWriter() + + _, err = w.Write(buf) + if err != nil { + return err + } + + return session.base.WriteFlush() +} + +// WriteReportStatusV2 writes one report-status-v2 response. +func (session *Session) WriteReportStatusV2(result ReportStatusResult) error { + unpackResult := "ok" + if result.UnpackError != "" { + unpackResult = result.UnpackError + } + + if !session.negotiated.SideBand64K { //nolint:nestif + err := session.base.WriteData(fmt.Appendf(nil, "unpack %s\n", unpackResult)) + if err != nil { + return err + } + + for _, command := range result.Commands { + if command.Error != "" { + err = session.base.WriteData(fmt.Appendf(nil, "ng %s %s\n", command.Name, command.Error)) + if err != nil { + return err + } + + continue + } + + err = session.base.WriteData(fmt.Appendf(nil, "ok %s\n", command.Name)) + if err != nil { + return err + } + + if command.RefName != "" { + err = session.base.WriteData(fmt.Appendf(nil, "option refname %s\n", command.RefName)) + if err != nil { + return err + } + } + + if command.OldID != nil { + err = session.base.WriteData(fmt.Appendf(nil, "option old-oid %s\n", *command.OldID)) + if err != nil { + return err + } + } + + if command.NewID != nil { + err = session.base.WriteData(fmt.Appendf(nil, "option new-oid %s\n", *command.NewID)) + if err != nil { + return err + } + } + + if command.ForcedUpdate { + err = session.base.WriteData([]byte("option forced-update\n")) + if err != nil { + return err + } + } + } + + return session.base.WriteFlush() + } + + buf, err := pktline.AppendData(nil, fmt.Appendf(nil, "unpack %s\n", unpackResult)) + if err != nil { + return err + } + + for _, command := range result.Commands { + if command.Error != "" { + buf, err = pktline.AppendData(buf, fmt.Appendf(nil, "ng %s %s\n", command.Name, command.Error)) + if err != nil { + return err + } + + continue + } + + buf, err = pktline.AppendData(buf, fmt.Appendf(nil, "ok %s\n", command.Name)) + if err != nil { + return err + } + + if command.RefName != "" { + buf, err = pktline.AppendData(buf, fmt.Appendf(nil, "option refname %s\n", command.RefName)) + if err != nil { + return err + } + } + + if command.OldID != nil { + buf, err = pktline.AppendData(buf, fmt.Appendf(nil, "option old-oid %s\n", *command.OldID)) + if err != nil { + return err + } + } + + if command.NewID != nil { + buf, err = pktline.AppendData(buf, fmt.Appendf(nil, "option new-oid %s\n", *command.NewID)) + if err != nil { + return err + } + } + + if command.ForcedUpdate { + buf, err = pktline.AppendData(buf, []byte("option forced-update\n")) + if err != nil { + return err + } + } + } + + buf = pktline.AppendFlushPkt(buf) + + w := session.base.PrimaryDataWriter() + + _, err = w.Write(buf) + if err != nil { + return err + } + + return session.base.WriteFlush() +} diff --git a/network/protocol/v0v1/server/receivepack/report_status_test.go b/network/protocol/v0v1/server/receivepack/report_status_test.go new file mode 100644 index 00000000..b9b116f6 --- /dev/null +++ b/network/protocol/v0v1/server/receivepack/report_status_test.go @@ -0,0 +1,293 @@ +package receivepack_test + +import ( + "errors" + "io" + "strings" + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/network/protocol/pktline" + "codeberg.org/lindenii/furgit/network/protocol/sideband64k" + common "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" + receivepack "codeberg.org/lindenii/furgit/network/protocol/v0v1/server/receivepack" + objectid "codeberg.org/lindenii/furgit/object/id" +) + +func TestWriteReportStatusWritesClassicStatus(t *testing.T) { + t.Parallel() + + var out bufferWriteFlusher + + base := common.NewSession(strings.NewReader(""), &out, common.Options{}) + session := receivepack.NewSession(base, receivepack.Capabilities{}) + + err := session.WriteReportStatus(receivepack.ReportStatusResult{ + Commands: []receivepack.CommandResult{ + {Name: "refs/heads/main"}, + {Name: "refs/heads/dev", Error: "non-fast-forward"}, + }, + }) + if err != nil { + t.Fatalf("WriteReportStatus: %v", err) + } + + got := out.String() + wantParts := []string{ + "unpack ok\n", + "ok refs/heads/main\n", + "ng refs/heads/dev non-fast-forward\n", + "0000", + } + + for _, part := range wantParts { + if !strings.Contains(got, part) { + t.Fatalf("report-status missing %q in %q", part, got) + } + } +} + +func TestWriteReportStatusUsesSideBand64KWhenNegotiated(t *testing.T) { + t.Parallel() + + //nolint:thelper + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { + t.Parallel() + + var requestWire bufferWriteFlusher + + requestEnc := pktline.NewEncoder(&requestWire) + + err := requestEnc.WriteData([]byte( + objectid.Zero(algo).String() + " " + mustHexID(t, algo, "1").String() + " refs/heads/main\x00report-status side-band-64k object-format=" + algo.String() + "\n", + )) + if err != nil { + t.Fatalf("WriteData(request): %v", err) + } + + err = requestEnc.WriteFlush() + if err != nil { + t.Fatalf("WriteFlush(request): %v", err) + } + + var out bufferWriteFlusher + + base := common.NewSession(strings.NewReader(requestWire.String()), &out, common.Options{ + Algorithm: algo, + }) + session := receivepack.NewSession(base, receivepack.Capabilities{ + ReportStatus: true, + SideBand64K: true, + ObjectFormat: algo, + }) + + _, err = session.ReadRequest() + if err != nil { + t.Fatalf("ReadRequest: %v", err) + } + + err = session.WriteReportStatus(receivepack.ReportStatusResult{ + Commands: []receivepack.CommandResult{ + {Name: "refs/heads/main"}, + }, + }) + if err != nil { + t.Fatalf("WriteReportStatus: %v", err) + } + + dec := sideband64k.NewDecoder(strings.NewReader(out.String()), sideband64k.ReadOptions{}) + + frame, err := dec.ReadFrame() + if err != nil { + t.Fatalf("ReadFrame(unpack): %v", err) + } + + if frame.Type != sideband64k.FrameData { + t.Fatalf("first frame = %#v", frame) + } + + statusDec := pktline.NewDecoder(strings.NewReader(string(frame.Payload)), pktline.ReadOptions{}) + + statusFrame, err := statusDec.ReadFrame() + if err != nil { + t.Fatalf("ReadFrame(unpack status): %v", err) + } + + if statusFrame.Type != pktline.PacketData || string(statusFrame.Payload) != "unpack ok\n" { + t.Fatalf("first status frame = %#v", statusFrame) + } + + statusFrame, err = statusDec.ReadFrame() + if err != nil { + t.Fatalf("ReadFrame(ok status): %v", err) + } + + if statusFrame.Type != pktline.PacketData || string(statusFrame.Payload) != "ok refs/heads/main\n" { + t.Fatalf("second status frame = %#v", statusFrame) + } + + statusFrame, err = statusDec.ReadFrame() + if err != nil { + t.Fatalf("ReadFrame(status flush): %v", err) + } + + if statusFrame.Type != pktline.PacketFlush { + t.Fatalf("status flush frame.Type = %v, want FrameFlush", statusFrame.Type) + } + + frame, err = dec.ReadFrame() + if err != nil { + t.Fatalf("ReadFrame(outer flush): %v", err) + } + + if frame.Type != sideband64k.FrameFlush { + t.Fatalf("outer flush frame.Type = %v, want FrameFlush", frame.Type) + } + }) +} + +func TestWriteReportStatusV2WritesOptionLines(t *testing.T) { + t.Parallel() + + //nolint:thelper + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { + t.Parallel() + + oldID := mustHexID(t, algo, "1") + newID := mustHexID(t, algo, "2") + + var out bufferWriteFlusher + + base := common.NewSession(strings.NewReader(""), &out, common.Options{}) + session := receivepack.NewSession(base, receivepack.Capabilities{}) + + err := session.WriteReportStatusV2(receivepack.ReportStatusResult{ + Commands: []receivepack.CommandResult{ + { + Name: "refs/pseudo/proc", + RefName: "refs/heads/main", + OldID: &oldID, + NewID: &newID, + ForcedUpdate: true, + }, + {Name: "refs/heads/dev", Error: "rejected"}, + }, + }) + if err != nil { + t.Fatalf("WriteReportStatusV2: %v", err) + } + + got := out.String() + wantParts := []string{ + "unpack ok\n", + "ok refs/pseudo/proc\n", + "option refname refs/heads/main\n", + "option old-oid " + oldID.String() + "\n", + "option new-oid " + newID.String() + "\n", + "option forced-update\n", + "ng refs/heads/dev rejected\n", + "0000", + } + + for _, part := range wantParts { + if !strings.Contains(got, part) { + t.Fatalf("report-status-v2 missing %q in %q", part, got) + } + } + }) +} + +func TestWriteProgressRequiresSideBand64K(t *testing.T) { + t.Parallel() + + base := common.NewSession(strings.NewReader(""), &bufferWriteFlusher{}, common.Options{}) + session := receivepack.NewSession(base, receivepack.Capabilities{}) + + err := session.WriteProgress([]byte("progress\n")) + if !errors.Is(err, common.ErrSideBandNotEnabled) { + t.Fatalf("WriteProgress error = %v, want %v", err, common.ErrSideBandNotEnabled) + } +} + +func TestProgressWriterDiscardsWithoutSideBand64K(t *testing.T) { + t.Parallel() + + var out bufferWriteFlusher + + base := common.NewSession(strings.NewReader(""), &out, common.Options{}) + session := receivepack.NewSession(base, receivepack.Capabilities{}) + + n, err := io.WriteString(session.ProgressWriter(), "progress line\n") + if err != nil { + t.Fatalf("ProgressWriter.Write: %v", err) + } + + if n != len("progress line\n") { + t.Fatalf("ProgressWriter.Write n = %d, want %d", n, len("progress line\n")) + } + + if out.String() != "" { + t.Fatalf("unexpected wire output without side-band-64k: %q", out.String()) + } +} + +func TestProgressWriterUsesSideBand64KWhenNegotiated(t *testing.T) { + t.Parallel() + + //nolint:thelper + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { + t.Parallel() + + var requestWire bufferWriteFlusher + + requestEnc := pktline.NewEncoder(&requestWire) + + err := requestEnc.WriteData([]byte( + objectid.Zero(algo).String() + " " + mustHexID(t, algo, "1").String() + " refs/heads/main\x00report-status side-band-64k object-format=" + algo.String() + "\n", + )) + if err != nil { + t.Fatalf("WriteData(request): %v", err) + } + + err = requestEnc.WriteFlush() + if err != nil { + t.Fatalf("WriteFlush(request): %v", err) + } + + var out bufferWriteFlusher + + base := common.NewSession(strings.NewReader(requestWire.String()), &out, common.Options{ + Algorithm: algo, + }) + session := receivepack.NewSession(base, receivepack.Capabilities{ + ReportStatus: true, + SideBand64K: true, + ObjectFormat: algo, + }) + + _, err = session.ReadRequest() + if err != nil { + t.Fatalf("ReadRequest: %v", err) + } + + _, err = io.WriteString(session.ProgressWriter(), "remote: stage 1\r") + if err != nil { + t.Fatalf("ProgressWriter.Write: %v", err) + } + + dec := sideband64k.NewDecoder(strings.NewReader(out.String()), sideband64k.ReadOptions{}) + + frame, err := dec.ReadFrame() + if err != nil { + t.Fatalf("ReadFrame(progress): %v", err) + } + + if frame.Type != sideband64k.FrameProgress { + t.Fatalf("frame.Type = %v, want FrameProgress", frame.Type) + } + + if string(frame.Payload) != "remote: stage 1\r" { + t.Fatalf("frame.Payload = %q, want %q", frame.Payload, "remote: stage 1\r") + } + }) +} diff --git a/network/protocol/v0v1/server/receivepack/session.go b/network/protocol/v0v1/server/receivepack/session.go new file mode 100644 index 00000000..55019714 --- /dev/null +++ b/network/protocol/v0v1/server/receivepack/session.go @@ -0,0 +1,295 @@ +package receivepack + +import ( + "fmt" + "io" + "strings" + + common "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" + objectid "codeberg.org/lindenii/furgit/object/id" +) + +// Session is one stateful server-side receive-pack protocol session. +type Session struct { + base *common.Session + supported Capabilities + negotiated Capabilities +} + +// NewSession creates one receive-pack session over one common server session. +func NewSession(base *common.Session, supported Capabilities) *Session { + return &Session{ + base: base, + supported: supported, + } +} + +// AdvertiseRefs writes one receive-pack ref advertisement. +func (session *Session) AdvertiseRefs(ad common.Advertisement) error { + return session.base.AdvertiseRefs(ad, session.supported.Tokens(session.base.Algorithm())) +} + +// ReadRequest reads one receive-pack request through optional push-options. +func (session *Session) ReadRequest() (*Request, error) { + req := &Request{} + + var sawCommands bool + + for { + frame, err := session.base.ReadFrame() + if err != nil { + return nil, err + } + + switch frame.Type { + case common.FrameFlush: + goto afterCommands + case common.FrameData: + case common.FrameDelim, common.FrameResponseEnd: + return nil, &ProtocolError{Reason: fmt.Sprintf("unexpected packet type %v", frame.Type)} + } + + payload := string(frame.Payload) + if strings.HasPrefix(payload, "shallow ") { + line := trimOneLF(payload) + + shallowID, err := parseObjectID(session.base.Algorithm(), line[len("shallow "):]) + if err != nil { + return nil, err + } + + req.Shallow = append(req.Shallow, shallowID) + + continue + } + + if strings.HasPrefix(payload, "push-cert\x00") { + if sawCommands { + return nil, &ProtocolError{Reason: "got both push certificate and unsigned commands"} + } + + capabilityTokens, err := parseCapabilityList(payload[len("push-cert\x00"):]) + if err != nil { + return nil, err + } + + requested, err := parseRequestedCapabilities( + capabilityTokens, + session.supported, + session.base.Algorithm(), + ) + if err != nil { + return nil, err + } + + req.Capabilities = requested + + cert, err := session.readPushCertificate() + if err != nil { + return nil, err + } + + req.PushCert = cert + req.Commands = append(req.Commands, cert.Commands...) + sawCommands = true + + continue + } + + line := trimOneLF(payload) + if !sawCommands && strings.Contains(line, "\x00") { + commandPart, capPart, _ := strings.Cut(line, "\x00") + + capabilityTokens, err := parseCapabilityList(capPart) + if err != nil { + return nil, err + } + + requested, err := parseRequestedCapabilities( + capabilityTokens, + session.supported, + session.base.Algorithm(), + ) + if err != nil { + return nil, err + } + + req.Capabilities = requested + line = commandPart + } + + cmd, err := parseCommand(session.base.Algorithm(), line) + if err != nil { + return nil, err + } + + req.Commands = append(req.Commands, cmd) + sawCommands = true + } + +afterCommands: + if req.Capabilities.PushOptions { + for { + frame, err := session.base.ReadFrame() + if err != nil { + return nil, err + } + + switch frame.Type { + case common.FrameFlush: + goto afterPushOptions + case common.FrameData: + req.PushOptions = append(req.PushOptions, trimOneLF(string(frame.Payload))) + case common.FrameDelim, common.FrameResponseEnd: + return nil, &ProtocolError{Reason: fmt.Sprintf("unexpected packet type %v", frame.Type)} + } + } + } + +afterPushOptions: + req.DeleteOnly = deleteOnly(req.Commands) + + req.PackExpected = len(req.Commands) > 0 && !req.DeleteOnly + + session.negotiated = req.Capabilities + + if req.Capabilities.SideBand64K { + session.base.EnableSideBand64K() + } + + return req, nil +} + +// WriteProgress writes one progress packet. +func (session *Session) WriteProgress(p []byte) error { + return session.base.WriteProgress(p) +} + +// ProgressWriter returns one chunking writer for sideband progress output. +// +// When side-band-64k was not negotiated, writes are discarded. +func (session *Session) ProgressWriter() io.Writer { + return session.base.ProgressWriter() +} + +// WriteError writes one fatal error packet. +func (session *Session) WriteError(p []byte) error { + return session.base.WriteError(p) +} + +// ErrorWriter returns one chunking writer for sideband error output. +// +// When side-band-64k was not negotiated, writes are discarded. +func (session *Session) ErrorWriter() io.Writer { + return session.base.ErrorWriter() +} + +func trimOneLF(s string) string { + return strings.TrimSuffix(s, "\n") +} + +func parseObjectID(algo objectid.Algorithm, s string) (objectid.ObjectID, error) { + id, err := objectid.ParseHex(algo, s) + if err != nil { + return objectid.ObjectID{}, &ProtocolError{ + Reason: fmt.Sprintf("invalid object id %q", s), + } + } + + return id, nil +} + +func commandIsDelete(cmd Command) bool { + return cmd.NewID == objectid.Zero(cmd.NewID.Algorithm()) +} + +func deleteOnly(commands []Command) bool { + if len(commands) == 0 { + return false + } + + for _, cmd := range commands { + if !commandIsDelete(cmd) { + return false + } + } + + return true +} + +func parseCommand(algo objectid.Algorithm, line string) (Command, error) { + fields := strings.Fields(line) + if len(fields) != 3 { + return Command{}, &ProtocolError{Reason: fmt.Sprintf("malformed command %q", line)} + } + + oldID, err := parseObjectID(algo, fields[0]) + if err != nil { + return Command{}, err + } + + newID, err := parseObjectID(algo, fields[1]) + if err != nil { + return Command{}, err + } + + return Command{OldID: oldID, NewID: newID, Name: fields[2]}, nil +} + +func (session *Session) readPushCertificate() (*PushCertificate, error) { + cert := &PushCertificate{} + inCommands := false + inSignature := false + + for { + frame, err := session.base.ReadFrame() + if err != nil { + return nil, err + } + + switch frame.Type { + case common.FrameFlush: + return nil, &ProtocolError{Reason: "unexpected flush inside push certificate"} + case common.FrameData: + case common.FrameDelim, common.FrameResponseEnd: + return nil, &ProtocolError{Reason: fmt.Sprintf("unexpected packet type %v", frame.Type)} + } + + line := string(frame.Payload) + if line == "push-cert-end\n" { + return cert, nil + } + + if !inCommands { + if line == "\n" { + inCommands = true + + continue + } + + trimmed := trimOneLF(line) + cert.HeaderLines = append(cert.HeaderLines, trimmed) + + if strings.HasPrefix(trimmed, "push-option ") { + cert.EmbeddedOption = append(cert.EmbeddedOption, trimmed[len("push-option "):]) + } + + continue + } + + if !inSignature { + trimmed := trimOneLF(line) + + cmd, err := parseCommand(session.base.Algorithm(), trimmed) + if err == nil { + cert.Commands = append(cert.Commands, cmd) + + continue + } + + inSignature = true + } + + cert.SignatureLines = append(cert.SignatureLines, trimOneLF(line)) + } +} diff --git a/network/protocol/v0v1/server/receivepack/types.go b/network/protocol/v0v1/server/receivepack/types.go new file mode 100644 index 00000000..b281a86b --- /dev/null +++ b/network/protocol/v0v1/server/receivepack/types.go @@ -0,0 +1,45 @@ +package receivepack + +import objectid "codeberg.org/lindenii/furgit/object/id" + +// Command is one requested reference update. +type Command struct { + OldID objectid.ObjectID + NewID objectid.ObjectID + Name string +} + +// PushCertificate is one parsed push certificate block. +type PushCertificate struct { + HeaderLines []string + EmbeddedOption []string + Commands []Command + SignatureLines []string +} + +// Request is one parsed receive-pack request. +type Request struct { + Capabilities Capabilities + Shallow []objectid.ObjectID + Commands []Command + PushCert *PushCertificate + PushOptions []string + PackExpected bool + DeleteOnly bool +} + +// CommandResult is one per-command report-status result. +type CommandResult struct { + Name string + Error string + RefName string + OldID *objectid.ObjectID + NewID *objectid.ObjectID + ForcedUpdate bool +} + +// ReportStatusResult is one report-status payload. +type ReportStatusResult struct { + UnpackError string + Commands []CommandResult +} diff --git a/network/protocol/v0v1/server/session.go b/network/protocol/v0v1/server/session.go new file mode 100644 index 00000000..ab79a7d7 --- /dev/null +++ b/network/protocol/v0v1/server/session.go @@ -0,0 +1,131 @@ +package server + +import ( + "io" + + "codeberg.org/lindenii/furgit/network/protocol/pktline" + "codeberg.org/lindenii/furgit/network/protocol/sideband64k" + objectid "codeberg.org/lindenii/furgit/object/id" +) + +// Options configures one server-side v0/v1 session. +type Options struct { + // Version selects protocol v0 or v1 framing. + Version Version + // Algorithm is the repository object ID algorithm for this session. + Algorithm objectid.Algorithm +} + +// Session is one stateful server-side v0/v1 server protocol session. +type Session struct { + dec *pktline.Decoder + enc *pktline.Encoder + sideband *sideband64k.Encoder + opts Options + useSideBand bool +} + +// NewSession creates one v0/v1 server session over r and w. +func NewSession(r io.Reader, w pktline.WriteFlusher, opts Options) *Session { + return &Session{ + dec: pktline.NewDecoder(r, pktline.ReadOptions{}), + enc: pktline.NewEncoder(w), + sideband: sideband64k.NewEncoder(w), + opts: opts, + } +} + +// Algorithm returns the session object ID algorithm. +func (session *Session) Algorithm() objectid.Algorithm { + return session.opts.Algorithm +} + +// ReadFrame reads one low-level pkt-line frame from the session input. +func (session *Session) ReadFrame() (Frame, error) { + return session.dec.ReadFrame() +} + +// EnableSideBand64K enables side-band-64k output framing for subsequent data, +// progress, error, and flush writes. +func (session *Session) EnableSideBand64K() { + session.useSideBand = true +} + +// WriteData writes one primary output packet. +func (session *Session) WriteData(p []byte) error { + if session.useSideBand { + return session.sideband.WriteData(p) + } + + return session.enc.WriteData(p) +} + +// WriteProgress writes one progress packet. +func (session *Session) WriteProgress(p []byte) error { + if !session.useSideBand { + return ErrSideBandNotEnabled + } + + return session.sideband.WriteProgress(p) +} + +// WriteError writes one fatal error packet. +func (session *Session) WriteError(p []byte) error { + if !session.useSideBand { + return ErrSideBandNotEnabled + } + + return session.sideband.WriteError(p) +} + +// WriteFlush writes one trailing flush packet. +func (session *Session) WriteFlush() error { + if session.useSideBand { + return session.sideband.WriteFlush() + } + + return session.enc.WriteFlush() +} + +// FlushIO flushes buffered transport output without emitting pkt-line frames. +func (session *Session) FlushIO() error { + if session.useSideBand { + return session.sideband.FlushIO() + } + + return session.enc.FlushIO() +} + +// ProgressWriter returns one chunking writer for sideband progress output. +// +// When side-band-64k was not negotiated, writes are discarded. +func (session *Session) ProgressWriter() io.Writer { + if !session.useSideBand { + return io.Discard + } + + return sideband64k.NewChunkWriter(session.sideband, sideband64k.BandProgress) +} + +// ErrorWriter returns one chunking writer for sideband error output. +// +// When side-band-64k was not negotiated, writes are discarded. +func (session *Session) ErrorWriter() io.Writer { + if !session.useSideBand { + return io.Discard + } + + return sideband64k.NewChunkWriter(session.sideband, sideband64k.BandError) +} + +// PrimaryDataWriter returns one chunking writer for primary output bytes. +// +// When side-band-64k is enabled, writes are chunked into band-1 sideband +// frames. Otherwise writes are chunked into direct pkt-line data frames. +func (session *Session) PrimaryDataWriter() io.Writer { + if session.useSideBand { + return sideband64k.NewChunkWriter(session.sideband, sideband64k.BandData) + } + + return pktline.NewChunkWriter(session.enc) +} diff --git a/network/protocol/v0v1/server/version.go b/network/protocol/v0v1/server/version.go new file mode 100644 index 00000000..23ae9466 --- /dev/null +++ b/network/protocol/v0v1/server/version.go @@ -0,0 +1,12 @@ +package server + +// Version identifies the protocol version used on one v0/v1 server session. +type Version uint8 + +const ( + // Version0 is the original protocol framing with no leading version line. + Version0 Version = iota + // Version1 is protocol v1, which is v0 plus one leading "version 1\n" + // pkt-line before ref advertisement. + Version1 +) |
