shithub: furgit

ref: b46eba214daa9a6ede179ed543033b0f3485ec2e
dir: /protocol/v0v1/server/receivepack/capabilities.go/

View raw version
package receivepack

import (
	"fmt"
	"slices"
	"strings"

	"codeberg.org/lindenii/furgit/objectid"
)

// 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
}