aboutsummaryrefslogtreecommitdiff
path: root/network/protocol/v0v1/server/receivepack/capabilities.go
diff options
context:
space:
mode:
Diffstat (limited to 'network/protocol/v0v1/server/receivepack/capabilities.go')
-rw-r--r--network/protocol/v0v1/server/receivepack/capabilities.go192
1 files changed, 192 insertions, 0 deletions
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
+}