Files
upstream-seaweedfs/weed/mq/kafka/protocol/flexible_versions.go
upstream-mirror 89f5be005d
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 03:23:42 +05:00

480 lines
12 KiB
Go

package protocol
import (
"encoding/binary"
"fmt"
)
// FlexibleVersions provides utilities for handling Kafka flexible versions protocol
// Flexible versions use compact arrays/strings and tagged fields for backward compatibility
// CompactArrayLength encodes a length for compact arrays
// Compact arrays encode length as length+1, where 0 means empty array
func CompactArrayLength(length uint32) []byte {
// Compact arrays use length+1 encoding (0 = null, 1 = empty, n+1 = array of length n)
// For an empty array (length=0), we return 1 (not 0, which would be null)
return EncodeUvarint(length + 1)
}
// DecodeCompactArrayLength decodes a compact array length
// Returns the actual length and number of bytes consumed
func DecodeCompactArrayLength(data []byte) (uint32, int, error) {
if len(data) == 0 {
return 0, 0, fmt.Errorf("no data for compact array length")
}
if data[0] == 0 {
return 0, 1, nil // Empty array
}
length, consumed, err := DecodeUvarint(data)
if err != nil {
return 0, 0, fmt.Errorf("decode compact array length: %w", err)
}
if length == 0 {
return 0, consumed, fmt.Errorf("invalid compact array length encoding")
}
return length - 1, consumed, nil
}
// CompactStringLength encodes a length for compact strings
// Compact strings encode length as length+1, where 0 means null string
func CompactStringLength(length int) []byte {
if length < 0 {
return []byte{0} // Null string
}
return EncodeUvarint(uint32(length + 1))
}
// DecodeCompactStringLength decodes a compact string length
// Returns the actual length (-1 for null), and number of bytes consumed
func DecodeCompactStringLength(data []byte) (int, int, error) {
if len(data) == 0 {
return 0, 0, fmt.Errorf("no data for compact string length")
}
if data[0] == 0 {
return -1, 1, nil // Null string
}
length, consumed, err := DecodeUvarint(data)
if err != nil {
return 0, 0, fmt.Errorf("decode compact string length: %w", err)
}
if length == 0 {
return 0, consumed, fmt.Errorf("invalid compact string length encoding")
}
return int(length - 1), consumed, nil
}
// EncodeUvarint encodes an unsigned integer using variable-length encoding
// This is used for compact arrays, strings, and tagged fields
func EncodeUvarint(value uint32) []byte {
var buf []byte
for value >= 0x80 {
buf = append(buf, byte(value)|0x80)
value >>= 7
}
buf = append(buf, byte(value))
return buf
}
// DecodeUvarint decodes a variable-length unsigned integer
// Returns the decoded value and number of bytes consumed
func DecodeUvarint(data []byte) (uint32, int, error) {
var value uint32
var shift uint
var consumed int
for i, b := range data {
consumed = i + 1
value |= uint32(b&0x7F) << shift
if (b & 0x80) == 0 {
return value, consumed, nil
}
shift += 7
if shift >= 32 {
return 0, consumed, fmt.Errorf("uvarint overflow")
}
}
return 0, consumed, fmt.Errorf("incomplete uvarint")
}
// TaggedField represents a tagged field in flexible versions
type TaggedField struct {
Tag uint32
Data []byte
}
// TaggedFields represents a collection of tagged fields
type TaggedFields struct {
Fields []TaggedField
}
// EncodeTaggedFields encodes tagged fields for flexible versions
func (tf *TaggedFields) Encode() []byte {
if len(tf.Fields) == 0 {
return []byte{0} // Empty tagged fields
}
var buf []byte
// Number of tagged fields
buf = append(buf, EncodeUvarint(uint32(len(tf.Fields)))...)
for _, field := range tf.Fields {
// Tag
buf = append(buf, EncodeUvarint(field.Tag)...)
// Size
buf = append(buf, EncodeUvarint(uint32(len(field.Data)))...)
// Data
buf = append(buf, field.Data...)
}
return buf
}
// DecodeTaggedFields decodes tagged fields from flexible versions
func DecodeTaggedFields(data []byte) (*TaggedFields, int, error) {
if len(data) == 0 {
return &TaggedFields{}, 0, fmt.Errorf("no data for tagged fields")
}
if data[0] == 0 {
return &TaggedFields{}, 1, nil // Empty tagged fields
}
offset := 0
// Number of tagged fields
numFields, consumed, err := DecodeUvarint(data[offset:])
if err != nil {
return nil, 0, fmt.Errorf("decode tagged fields count: %w", err)
}
offset += consumed
fields := make([]TaggedField, numFields)
for i := uint32(0); i < numFields; i++ {
// Tag
tag, consumed, err := DecodeUvarint(data[offset:])
if err != nil {
return nil, 0, fmt.Errorf("decode tagged field %d tag: %w", i, err)
}
offset += consumed
// Size
size, consumed, err := DecodeUvarint(data[offset:])
if err != nil {
return nil, 0, fmt.Errorf("decode tagged field %d size: %w", i, err)
}
offset += consumed
// Data
if offset+int(size) > len(data) {
// More detailed error information
return nil, 0, fmt.Errorf("tagged field %d data truncated: need %d bytes at offset %d, but only %d total bytes available", i, size, offset, len(data))
}
fields[i] = TaggedField{
Tag: tag,
Data: data[offset : offset+int(size)],
}
offset += int(size)
}
return &TaggedFields{Fields: fields}, offset, nil
}
// IsFlexibleVersion determines if an API version uses flexible versions
// This is API-specific and based on when each API adopted flexible versions
func IsFlexibleVersion(apiKey, apiVersion uint16) bool {
switch APIKey(apiKey) {
case APIKeyApiVersions:
return apiVersion >= 3
case APIKeyMetadata:
return apiVersion >= 9
case APIKeyFetch:
return apiVersion >= 12
case APIKeyProduce:
return apiVersion >= 9
case APIKeyJoinGroup:
return apiVersion >= 6
case APIKeySyncGroup:
return apiVersion >= 4
case APIKeyOffsetCommit:
return apiVersion >= 8
case APIKeyOffsetFetch:
return apiVersion >= 6
case APIKeyFindCoordinator:
return apiVersion >= 3
case APIKeyHeartbeat:
return apiVersion >= 4
case APIKeyLeaveGroup:
return apiVersion >= 4
case APIKeyCreateTopics:
return apiVersion >= 2
case APIKeyDeleteTopics:
return apiVersion >= 4
default:
return false
}
}
// FlexibleString encodes a string for flexible versions (compact format)
func FlexibleString(s string) []byte {
// Compact strings use length+1 encoding (0 = null, 1 = empty, n+1 = string of length n)
// For an empty string (s=""), we return length+1 = 1 (not 0, which would be null)
var buf []byte
buf = append(buf, CompactStringLength(len(s))...)
buf = append(buf, []byte(s)...)
return buf
}
// parseCompactString parses a compact string from flexible protocol
// Returns the string bytes and the number of bytes consumed
func parseCompactString(data []byte) ([]byte, int) {
if len(data) == 0 {
return nil, 0
}
// Parse compact string length (unsigned varint - no zigzag decoding!)
length, consumed := decodeUnsignedVarint(data)
if consumed == 0 {
return nil, 0
}
// Debug logging for compact string parsing
if length == 0 {
// Null string (length 0 means null)
return nil, consumed
}
// In compact strings, length is actual length + 1
// So length 1 means empty string, length > 1 means non-empty
if length == 0 {
return nil, consumed // Already handled above
}
actualLength := int(length - 1)
if actualLength < 0 {
return nil, 0
}
if actualLength == 0 {
// Empty string (length was 1)
return []byte{}, consumed
}
if consumed+actualLength > len(data) {
return nil, 0
}
result := data[consumed : consumed+actualLength]
return result, consumed + actualLength
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
// decodeUnsignedVarint decodes an unsigned varint (no zigzag decoding)
func decodeUnsignedVarint(data []byte) (uint64, int) {
if len(data) == 0 {
return 0, 0
}
var result uint64
var shift uint
var bytesRead int
for i, b := range data {
if i > 9 { // varints can be at most 10 bytes
return 0, 0 // invalid varint
}
bytesRead++
result |= uint64(b&0x7F) << shift
if (b & 0x80) == 0 {
// Most significant bit is 0, we're done
return result, bytesRead
}
shift += 7
}
return 0, 0 // incomplete varint
}
// FlexibleNullableString encodes a nullable string for flexible versions
func FlexibleNullableString(s *string) []byte {
if s == nil {
return []byte{0} // Null string
}
return FlexibleString(*s)
}
// DecodeFlexibleString decodes a flexible string
// Returns the string (empty for null) and bytes consumed
func DecodeFlexibleString(data []byte) (string, int, error) {
length, consumed, err := DecodeCompactStringLength(data)
if err != nil {
return "", 0, err
}
if length < 0 {
return "", consumed, nil // Null string -> empty string
}
if consumed+length > len(data) {
return "", 0, fmt.Errorf("string data truncated")
}
return string(data[consumed : consumed+length]), consumed + length, nil
}
// FlexibleVersionHeader handles the request header parsing for flexible versions
type FlexibleVersionHeader struct {
APIKey uint16
APIVersion uint16
CorrelationID uint32
ClientID *string
TaggedFields *TaggedFields
}
// parseRegularHeader parses a regular (non-flexible) Kafka request header
func parseRegularHeader(data []byte) (*FlexibleVersionHeader, []byte, error) {
if len(data) < 8 {
return nil, nil, fmt.Errorf("header too short")
}
header := &FlexibleVersionHeader{}
offset := 0
// API Key (2 bytes)
header.APIKey = binary.BigEndian.Uint16(data[offset : offset+2])
offset += 2
// API Version (2 bytes)
header.APIVersion = binary.BigEndian.Uint16(data[offset : offset+2])
offset += 2
// Correlation ID (4 bytes)
header.CorrelationID = binary.BigEndian.Uint32(data[offset : offset+4])
offset += 4
// Regular versions use standard strings
if len(data) < offset+2 {
return nil, nil, fmt.Errorf("missing client_id length")
}
clientIDLen := int16(binary.BigEndian.Uint16(data[offset : offset+2]))
offset += 2
if clientIDLen >= 0 {
if len(data) < offset+int(clientIDLen) {
return nil, nil, fmt.Errorf("client_id truncated")
}
clientID := string(data[offset : offset+int(clientIDLen)])
header.ClientID = &clientID
offset += int(clientIDLen)
}
return header, data[offset:], nil
}
// ParseRequestHeader parses a Kafka request header, handling both regular and flexible versions
func ParseRequestHeader(data []byte) (*FlexibleVersionHeader, []byte, error) {
if len(data) < 8 {
return nil, nil, fmt.Errorf("header too short")
}
header := &FlexibleVersionHeader{}
offset := 0
// API Key (2 bytes)
header.APIKey = binary.BigEndian.Uint16(data[offset : offset+2])
offset += 2
// API Version (2 bytes)
header.APIVersion = binary.BigEndian.Uint16(data[offset : offset+2])
offset += 2
// Correlation ID (4 bytes)
header.CorrelationID = binary.BigEndian.Uint32(data[offset : offset+4])
offset += 4
// Client ID handling depends on flexible version
isFlexible := IsFlexibleVersion(header.APIKey, header.APIVersion)
if isFlexible {
// Flexible versions use compact strings
clientID, consumed, err := DecodeFlexibleString(data[offset:])
if err != nil {
return nil, nil, fmt.Errorf("decode flexible client_id: %w", err)
}
offset += consumed
if clientID != "" {
header.ClientID = &clientID
}
// Parse tagged fields in header
taggedFields, consumed, err := DecodeTaggedFields(data[offset:])
if err != nil {
// If tagged fields parsing fails, this might be a regular header sent by kafka-go
// Fall back to regular header parsing
return parseRegularHeader(data)
}
offset += consumed
header.TaggedFields = taggedFields
} else {
// Regular versions use standard strings
if len(data) < offset+2 {
return nil, nil, fmt.Errorf("missing client_id length")
}
clientIDLen := int16(binary.BigEndian.Uint16(data[offset : offset+2]))
offset += 2
if clientIDLen >= 0 {
if len(data) < offset+int(clientIDLen) {
return nil, nil, fmt.Errorf("client_id truncated")
}
clientID := string(data[offset : offset+int(clientIDLen)])
header.ClientID = &clientID
offset += int(clientIDLen)
}
// No tagged fields in regular versions
}
return header, data[offset:], nil
}
// EncodeFlexibleResponse encodes a response with proper flexible version formatting
func EncodeFlexibleResponse(correlationID uint32, data []byte, hasTaggedFields bool) []byte {
response := make([]byte, 4)
binary.BigEndian.PutUint32(response, correlationID)
response = append(response, data...)
if hasTaggedFields {
// Add empty tagged fields for flexible responses
response = append(response, 0)
}
return response
}