// Type-Length-Value splitting and parsing for proxy protocol V2.
// See spec https://www.haproxy.org/download/3.4/doc/proxy-protocol.txt sections 2.2 to 2.2.8.

package proxyproto

import (
	"encoding/binary"
	"errors"
	"fmt"
	"math"
)

// TLV type constants defined by the PROXY protocol spec.
//
//nolint:revive // Names follow the spec.
const (
	// Section 2.2.
	PP2_TYPE_ALPN               PP2Type = 0x01
	PP2_TYPE_AUTHORITY          PP2Type = 0x02
	PP2_TYPE_CRC32C             PP2Type = 0x03
	PP2_TYPE_NOOP               PP2Type = 0x04
	PP2_TYPE_UNIQUE_ID          PP2Type = 0x05
	PP2_TYPE_SSL                PP2Type = 0x20
	PP2_SUBTYPE_SSL_VERSION     PP2Type = 0x21
	PP2_SUBTYPE_SSL_CN          PP2Type = 0x22
	PP2_SUBTYPE_SSL_CIPHER      PP2Type = 0x23
	PP2_SUBTYPE_SSL_SIG_ALG     PP2Type = 0x24
	PP2_SUBTYPE_SSL_KEY_ALG     PP2Type = 0x25
	PP2_SUBTYPE_SSL_GROUP       PP2Type = 0x26
	PP2_SUBTYPE_SSL_SIG_SCHEME  PP2Type = 0x27
	PP2_SUBTYPE_SSL_CLIENT_CERT PP2Type = 0x28
	PP2_TYPE_NETNS              PP2Type = 0x30

	// Section 2.2.8, reserved types.
	PP2_TYPE_MIN_CUSTOM     PP2Type = 0xE0
	PP2_TYPE_MAX_CUSTOM     PP2Type = 0xEF
	PP2_TYPE_MIN_EXPERIMENT PP2Type = 0xF0
	PP2_TYPE_MAX_EXPERIMENT PP2Type = 0xF7
	PP2_TYPE_MIN_FUTURE     PP2Type = 0xF8
	PP2_TYPE_MAX_FUTURE     PP2Type = 0xFF
)

var (
	// ErrTruncatedTLV indicates a TLV was truncated.
	ErrTruncatedTLV = errors.New("proxyproto: truncated TLV")
	// ErrMalformedTLV indicates a TLV has malformed data.
	ErrMalformedTLV = errors.New("proxyproto: malformed TLV Value")
	// ErrIncompatibleTLV indicates a TLV is of an unexpected type.
	ErrIncompatibleTLV = errors.New("proxyproto: incompatible TLV type")
)

// PP2Type is the proxy protocol v2 type.
type PP2Type byte

// TLV is a uninterpreted Type-Length-Value for V2 protocol, see section 2.2.
type TLV struct {
	Type  PP2Type
	Value []byte
}

// SplitTLVs splits the Type-Length-Value vector, returns the vector or an error.
func SplitTLVs(raw []byte) ([]TLV, error) {
	var tlvs []TLV
	for i := 0; i < len(raw); {
		tlv := TLV{
			Type: PP2Type(raw[i]),
		}
		if len(raw)-i <= 2 {
			return nil, ErrTruncatedTLV
		}
		tlvLen := int(binary.BigEndian.Uint16(raw[i+1 : i+3])) // Max length = 65K
		i += 3
		if i+tlvLen > len(raw) {
			return nil, ErrTruncatedTLV
		}
		// Ignore no-op padding
		if tlv.Type != PP2_TYPE_NOOP {
			tlv.Value = make([]byte, tlvLen)
			copy(tlv.Value, raw[i:i+tlvLen])
		}
		i += tlvLen
		tlvs = append(tlvs, tlv)
	}
	return tlvs, nil
}

// JoinTLVs joins multiple Type-Length-Value records.
func JoinTLVs(tlvs []TLV) ([]byte, error) {
	var raw []byte
	for _, tlv := range tlvs {
		if len(tlv.Value) > math.MaxUint16 {
			return nil, fmt.Errorf("proxyproto: cannot format TLV %v with length %d", tlv.Type, len(tlv.Value))
		}
		var length [2]byte
		//nolint:gosec // lengthValue is validated above.
		lengthValue := uint16(len(tlv.Value))
		binary.BigEndian.PutUint16(length[:], lengthValue)
		raw = append(raw, byte(tlv.Type))
		raw = append(raw, length[:]...)
		raw = append(raw, tlv.Value...)
	}
	return raw, nil
}

// Registered is true if the type is registered in the spec, see section 2.2.
func (p PP2Type) Registered() bool {
	switch p {
	case PP2_TYPE_ALPN,
		PP2_TYPE_AUTHORITY,
		PP2_TYPE_CRC32C,
		PP2_TYPE_NOOP,
		PP2_TYPE_UNIQUE_ID,
		PP2_TYPE_SSL,
		PP2_SUBTYPE_SSL_VERSION,
		PP2_SUBTYPE_SSL_CN,
		PP2_SUBTYPE_SSL_CIPHER,
		PP2_SUBTYPE_SSL_SIG_ALG,
		PP2_SUBTYPE_SSL_KEY_ALG,
		PP2_SUBTYPE_SSL_GROUP,
		PP2_SUBTYPE_SSL_SIG_SCHEME,
		PP2_SUBTYPE_SSL_CLIENT_CERT,
		PP2_TYPE_NETNS:
		return true
	}
	return false
}

// App is true if the type is reserved for application specific data, see section 2.2.8.
func (p PP2Type) App() bool {
	return p >= PP2_TYPE_MIN_CUSTOM && p <= PP2_TYPE_MAX_CUSTOM
}

// Experiment is true if the type is reserved for temporary experimental use by application
// developers, see section 2.2.8.
func (p PP2Type) Experiment() bool {
	return p >= PP2_TYPE_MIN_EXPERIMENT && p <= PP2_TYPE_MAX_EXPERIMENT
}

// Future is true is the type is reserved for future use, see section 2.2.8.
func (p PP2Type) Future() bool {
	return p >= PP2_TYPE_MIN_FUTURE
}

// Spec is true if the type is covered by the spec, see section 2.2 and 2.2.8.
func (p PP2Type) Spec() bool {
	return p.Registered() || p.App() || p.Experiment() || p.Future()
}
