| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| package binaryserialization |
| |
| import ( |
| "encoding/binary" |
| "errors" |
| "fmt" |
| "sort" |
| "time" |
| |
| iggcon "github.com/apache/iggy/foreign/go/contracts" |
| ierror "github.com/apache/iggy/foreign/go/errors" |
| "github.com/apache/iggy/foreign/go/internal/batch" |
| "github.com/klauspost/compress/s2" |
| ) |
| |
| func DeserializeLogInResponse(payload []byte) *iggcon.IdentityInfo { |
| userId := binary.LittleEndian.Uint32(payload[0:4]) |
| return &iggcon.IdentityInfo{ |
| UserId: userId, |
| } |
| } |
| |
| func DeserializeOffset(payload []byte) *iggcon.ConsumerOffsetInfo { |
| if len(payload) == 0 { |
| return nil |
| } |
| |
| partitionId := binary.LittleEndian.Uint32(payload[0:4]) |
| currentOffset := binary.LittleEndian.Uint64(payload[4:12]) |
| storedOffset := binary.LittleEndian.Uint64(payload[12:20]) |
| |
| return &iggcon.ConsumerOffsetInfo{ |
| PartitionId: partitionId, |
| CurrentOffset: currentOffset, |
| StoredOffset: storedOffset, |
| } |
| } |
| |
| func DeserializeStream(payload []byte) (*iggcon.StreamDetails, error) { |
| stream, pos, err := DeserializeToStream(payload, 0) |
| if err != nil { |
| return nil, err |
| } |
| // Count-driven: a topic element carries variable-length options blocks, |
| // so "consume until the buffer ends" no longer delimits it. The declared |
| // count is an unvalidated wire u32, so the allocation hint is capped by |
| // what the remaining body could possibly hold. |
| topics := make([]iggcon.Topic, 0, boundedCapacity(stream.TopicsCount, len(payload)-pos, topicMinimumSize)) |
| for i := uint32(0); i < stream.TopicsCount; i++ { |
| topic, readBytes, err := DeserializeToTopic(payload, pos) |
| if err != nil { |
| return nil, err |
| } |
| topics = append(topics, topic) |
| pos += readBytes |
| } |
| |
| sort.Slice(topics, func(i, j int) bool { |
| return topics[i].Id < topics[j].Id |
| }) |
| |
| return &iggcon.StreamDetails{ |
| Stream: stream, |
| Topics: topics, |
| }, nil |
| } |
| |
| func DeserializeStreams(payload []byte) ([]iggcon.Stream, error) { |
| streams := make([]iggcon.Stream, 0) |
| position := 0 |
| |
| for position < len(payload) { |
| stream, readBytes, err := DeserializeToStream(payload, position) |
| if err != nil { |
| return nil, fmt.Errorf("failed to deserialize stream at offset %d: %w", position, err) |
| } |
| streams = append(streams, stream) |
| position += readBytes |
| } |
| |
| return streams, nil |
| } |
| |
| const streamFixedSize = 4 + 8 + 4 + 8 + 8 + 1 // 33 bytes: id + created_at + topics_count + size_bytes + messages_count + name_len |
| |
| func DeserializeToStream(payload []byte, position int) (iggcon.Stream, int, error) { |
| remaining := len(payload) - position |
| if remaining < streamFixedSize { |
| return iggcon.Stream{}, 0, fmt.Errorf( |
| "not enough data to read stream header: need %d bytes, got %d", |
| streamFixedSize, remaining) |
| } |
| |
| id := binary.LittleEndian.Uint32(payload[position : position+4]) |
| createdAt := binary.LittleEndian.Uint64(payload[position+4 : position+12]) |
| topicsCount := binary.LittleEndian.Uint32(payload[position+12 : position+16]) |
| sizeBytes := binary.LittleEndian.Uint64(payload[position+16 : position+24]) |
| messagesCount := binary.LittleEndian.Uint64(payload[position+24 : position+32]) |
| nameLength := int(payload[position+32]) |
| |
| if remaining < streamFixedSize+nameLength { |
| return iggcon.Stream{}, 0, fmt.Errorf( |
| "not enough data to read stream name: need %d bytes, got %d", |
| streamFixedSize+nameLength, remaining) |
| } |
| |
| name := string(payload[position+33 : position+33+nameLength]) |
| |
| options, optionsSize, err := deserializeOptions(payload, position+streamFixedSize+nameLength) |
| if err != nil { |
| return iggcon.Stream{}, 0, fmt.Errorf("failed to read stream options: %w", err) |
| } |
| |
| return iggcon.Stream{ |
| Id: id, |
| TopicsCount: topicsCount, |
| Name: name, |
| SizeBytes: sizeBytes, |
| MessagesCount: messagesCount, |
| CreatedAt: createdAt, |
| Options: options, |
| }, streamFixedSize + nameLength + optionsSize, nil |
| } |
| |
| // deserializeOptions reads a u32-length-prefixed options TLV block at |
| // position and returns the decoded options with the total bytes consumed |
| // (prefix included). A zero-length block decodes to a nil map. |
| func deserializeOptions(payload []byte, position int) (map[string]iggcon.HeaderValue, int, error) { |
| block, consumed, err := readLengthPrefixed(payload, position, "options block") |
| if err != nil { |
| return nil, 0, err |
| } |
| if len(block) == 0 { |
| return nil, consumed, nil |
| } |
| |
| entries, err := iggcon.DeserializeHeaders(block) |
| if err != nil { |
| return nil, 0, err |
| } |
| options := make(map[string]iggcon.HeaderValue, len(entries)) |
| for _, entry := range entries { |
| options[string(entry.Key.Value)] = entry.Value |
| } |
| return options, consumed, nil |
| } |
| |
| // pollPrefixLength covers [partition_id u32][current_offset u64][count u32]. |
| const pollPrefixLength = 16 |
| |
| // DeserializeFetchMessagesResponse decodes a poll reply: the 16-byte prefix |
| // followed by batch records ([256-byte batch header][frames]) walked by their |
| // batch length, with each frame's deltas resolved to absolute values. A |
| // truncated body is a decode error rather than a shorter batch: silently |
| // dropping the tail would let a consumer that commits CurrentOffset skip |
| // messages it never saw. The returned messages alias the reply buffer; a |
| // retained message pins it. |
| func DeserializeFetchMessagesResponse(payload []byte, compression iggcon.IggyMessageCompression) (*iggcon.PolledMessage, error) { |
| if len(payload) == 0 { |
| return &iggcon.PolledMessage{ |
| PartitionId: 0, |
| CurrentOffset: 0, |
| Messages: make([]iggcon.IggyMessage, 0), |
| }, nil |
| } |
| |
| length := len(payload) |
| if length < pollPrefixLength { |
| return nil, fmt.Errorf("poll response: %d bytes is short of the reply prefix", length) |
| } |
| partitionId := binary.LittleEndian.Uint32(payload[0:4]) |
| currentOffset := binary.LittleEndian.Uint64(payload[4:12]) |
| messagesCount := binary.LittleEndian.Uint32(payload[12:16]) |
| position := pollPrefixLength |
| |
| // The declared count is server-controlled; the allocation hint is capped |
| // by what the body could possibly hold. |
| maxMessages := (length - pollPrefixLength) / batch.MessageHeaderSize |
| if int(messagesCount) < maxMessages { |
| maxMessages = int(messagesCount) |
| } |
| messages := make([]iggcon.IggyMessage, 0, maxMessages) |
| for position < length { |
| record, err := batch.DecodeHeader(payload[position:]) |
| if err != nil { |
| return nil, fmt.Errorf("poll response: %w", err) |
| } |
| if record.BatchLength > uint64(length-position) { |
| return nil, fmt.Errorf( |
| "poll response: batch record of %d bytes overruns the body", record.BatchLength) |
| } |
| recordEnd := position + int(record.BatchLength) |
| cursor := position + batch.HeaderSize |
| for cursor < recordEnd { |
| frame, err := batch.DecodeMessageHeader(payload[cursor:recordEnd]) |
| if err != nil { |
| return nil, fmt.Errorf("poll response: %w", err) |
| } |
| payloadStart := cursor + batch.MessageHeaderSize |
| payloadEnd := payloadStart + int(frame.PayloadLength) |
| userHeadersEnd := payloadEnd + int(frame.UserHeadersLength) |
| if userHeadersEnd > recordEnd { |
| return nil, fmt.Errorf( |
| "poll response: message of %d payload and %d user-header bytes overruns the batch record", |
| frame.PayloadLength, frame.UserHeadersLength) |
| } |
| payloadSlice := payload[payloadStart:payloadEnd] |
| var userHeaders []byte |
| if frame.UserHeadersLength > 0 { |
| userHeaders = payload[payloadEnd:userHeadersEnd] |
| } |
| cursor = userHeadersEnd |
| |
| switch compression { |
| case iggcon.MESSAGE_COMPRESSION_S2, iggcon.MESSAGE_COMPRESSION_S2_BETTER, iggcon.MESSAGE_COMPRESSION_S2_BEST: |
| payloadSlice, err = s2.Decode(nil, payloadSlice) |
| if err != nil { |
| return nil, fmt.Errorf("failed to decode s2 payload: %w", err) |
| } |
| } |
| |
| messages = append(messages, iggcon.IggyMessage{ |
| Header: iggcon.MessageHeader{ |
| Checksum: frame.Checksum, |
| Id: iggcon.MessageID(frame.Id), |
| // A record may be a server-sliced view of a larger stored |
| // batch: BaseOffset stays put and the first frame's delta |
| // positions it, so the sum is the absolute offset either way. |
| Offset: record.BaseOffset + uint64(frame.OffsetDelta), |
| // Broker append time is stamped once per batch; the |
| // per-message delta applies to OriginTimestamp only. |
| Timestamp: record.BaseTimestamp, |
| OriginTimestamp: record.OriginTimestamp + uint64(frame.TimestampDelta), |
| UserHeaderLength: frame.UserHeadersLength, |
| PayloadLength: frame.PayloadLength, |
| }, |
| Payload: payloadSlice, |
| UserHeaders: userHeaders, |
| }) |
| } |
| position = recordEnd |
| } |
| if uint32(len(messages)) != messagesCount { |
| return nil, fmt.Errorf( |
| "poll response: %d decoded messages do not match the declared %d", |
| len(messages), messagesCount) |
| } |
| |
| return &iggcon.PolledMessage{ |
| PartitionId: partitionId, |
| CurrentOffset: currentOffset, |
| Messages: messages, |
| MessageCount: messagesCount, |
| }, nil |
| } |
| |
| // optionSpecMinimumSize is the smallest catalog entry: a one-character name |
| // (a length byte plus at least one byte), a kind byte, and empty |
| // length-prefixed default and description. |
| const optionSpecMinimumSize = 2 + 1 + 4 + 4 |
| |
| // DeserializeOptionSpecs reads a DescribeOptions response. |
| // |
| // Wire format: [count:u32][ [key_len:u8][key][kind:u8][default_len:u32][default] |
| // [description_len:u32][description] ]* |
| func DeserializeOptionSpecs(payload []byte) ([]iggcon.OptionSpec, error) { |
| if len(payload) < 4 { |
| return nil, fmt.Errorf( |
| "not enough data to read option count: need 4 bytes, got %d", len(payload)) |
| } |
| count := binary.LittleEndian.Uint32(payload[0:4]) |
| position := 4 |
| |
| specs := make([]iggcon.OptionSpec, 0, |
| boundedCapacity(count, len(payload)-position, optionSpecMinimumSize)) |
| for i := uint32(0); i < count; i++ { |
| if len(payload)-position < 1 { |
| return nil, fmt.Errorf("truncated option key length at offset %d", position) |
| } |
| keyLength := int(payload[position]) |
| position++ |
| if len(payload)-position < keyLength+1 { |
| return nil, fmt.Errorf("truncated option key at offset %d", position) |
| } |
| key := string(payload[position : position+keyLength]) |
| position += keyLength |
| |
| kind := payload[position] |
| position++ |
| |
| defaultValue, read, err := readLengthPrefixed(payload, position, "option default value") |
| if err != nil { |
| return nil, err |
| } |
| position += read |
| |
| description, read, err := readLengthPrefixed(payload, position, "option description") |
| if err != nil { |
| return nil, err |
| } |
| position += read |
| |
| specs = append(specs, iggcon.OptionSpec{ |
| Key: key, |
| DefaultValue: iggcon.HeaderValue{Kind: iggcon.HeaderKind(kind), Value: defaultValue}, |
| Description: string(description), |
| }) |
| } |
| |
| return specs, nil |
| } |
| |
| func readLengthPrefixed(payload []byte, position int, field string) ([]byte, int, error) { |
| if len(payload)-position < 4 { |
| return nil, 0, fmt.Errorf("truncated length prefix for %s at offset %d", field, position) |
| } |
| length := int(binary.LittleEndian.Uint32(payload[position : position+4])) |
| position += 4 |
| // Where int is 32 bits a wire length above MaxInt32 converts to a negative |
| // one, which clears the remaining-bytes check and reaches the slice below |
| // with high < low. |
| if length < 0 || len(payload)-position < length { |
| return nil, 0, fmt.Errorf("truncated %s at offset %d", field, position) |
| } |
| return payload[position : position+length], 4 + length, nil |
| } |
| |
| func DeserializeTopics(payload []byte) ([]iggcon.Topic, error) { |
| if len(payload) < 4 { |
| return nil, fmt.Errorf( |
| "not enough data to read topics count: need 4 bytes, got %d", len(payload)) |
| } |
| topicsCount := binary.LittleEndian.Uint32(payload[0:4]) |
| position := 4 |
| |
| // The declared count is server-controlled; the allocation hint is capped |
| // by what the body could possibly hold. |
| topics := make([]iggcon.Topic, 0, boundedCapacity(topicsCount, len(payload)-position, topicMinimumSize)) |
| for i := uint32(0); i < topicsCount; i++ { |
| topic, readBytes, err := DeserializeToTopic(payload, position) |
| if err != nil { |
| return nil, err |
| } |
| topics = append(topics, topic) |
| position += readBytes |
| } |
| |
| return topics, nil |
| } |
| |
| func DeserializeTopic(payload []byte) (*iggcon.TopicDetails, error) { |
| topic, position, err := DeserializeToTopic(payload, 0) |
| if err != nil { |
| return &iggcon.TopicDetails{}, err |
| } |
| |
| partitions := make([]iggcon.PartitionContract, 0) |
| length := len(payload) |
| |
| for position < length { |
| partition, readBytes := DeserializePartition(payload, position) |
| partitions = append(partitions, partition) |
| position += readBytes |
| } |
| return &iggcon.TopicDetails{ |
| Topic: topic, |
| Partitions: partitions, |
| }, nil |
| } |
| |
| // topicFixedSize covers the fields before the name: |
| // id + created_at + partitions_count + message_expiry + compression + |
| // max_topic_size + size_bytes + messages_count + name_len. |
| const topicFixedSize = 4 + 8 + 4 + 8 + 1 + 8 + 8 + 8 + 1 // 50 bytes |
| |
| // topicMinimumSize is the smallest possible topic element: fixed fields, a |
| // one-character name (the server rejects an empty one), and the two u32 |
| // options-length prefixes (explicit and derived), both zero. |
| const topicMinimumSize = topicFixedSize + 1 + 4 + 4 |
| |
| // boundedCapacity caps a wire-declared element count by what the remaining |
| // bytes could possibly hold. |
| // |
| // The count is an unvalidated u32: at max it asks for a multi-hundred-gigabyte |
| // reservation, and a Go allocation failure cannot be recovered from. |
| func boundedCapacity(declared uint32, remaining int, minItemSize int) int { |
| if remaining <= 0 || minItemSize <= 0 { |
| return 0 |
| } |
| capacity := remaining / minItemSize |
| if uint64(declared) < uint64(capacity) { |
| return int(declared) |
| } |
| return capacity |
| } |
| |
| func DeserializeToTopic(payload []byte, position int) (iggcon.Topic, int, error) { |
| remaining := len(payload) - position |
| if remaining < topicFixedSize { |
| return iggcon.Topic{}, 0, fmt.Errorf( |
| "not enough data to read topic header: need %d bytes, got %d", |
| topicFixedSize, remaining) |
| } |
| |
| topic := iggcon.Topic{} |
| topic.Id = binary.LittleEndian.Uint32(payload[position : position+4]) |
| topic.CreatedAt = binary.LittleEndian.Uint64(payload[position+4 : position+12]) |
| topic.PartitionsCount = binary.LittleEndian.Uint32(payload[position+12 : position+16]) |
| topic.MessageExpiry = iggcon.Duration(binary.LittleEndian.Uint64(payload[position+16 : position+24])) |
| topic.CompressionAlgorithm = payload[position+24] |
| topic.MaxTopicSize = binary.LittleEndian.Uint64(payload[position+25 : position+33]) |
| topic.Size = binary.LittleEndian.Uint64(payload[position+33 : position+41]) |
| topic.MessagesCount = binary.LittleEndian.Uint64(payload[position+41 : position+49]) |
| // Replication factor left the wire protocol together with the old fixed |
| // layout; every topic reports the single-copy default. |
| |
| nameLength := int(payload[position+49]) |
| if remaining < topicFixedSize+nameLength { |
| return iggcon.Topic{}, 0, fmt.Errorf( |
| "not enough data to read topic name: need %d bytes, got %d", |
| topicFixedSize+nameLength, remaining) |
| } |
| topic.Name = string(payload[position+50 : position+50+nameLength]) |
| |
| readBytes := topicFixedSize + nameLength |
| options, optionsSize, err := deserializeOptions(payload, position+readBytes) |
| if err != nil { |
| return iggcon.Topic{}, 0, fmt.Errorf("failed to read topic options: %w", err) |
| } |
| topic.Options = options |
| readBytes += optionsSize |
| |
| derivedOptions, derivedSize, err := deserializeOptions(payload, position+readBytes) |
| if err != nil { |
| return iggcon.Topic{}, 0, fmt.Errorf("failed to read topic derived options: %w", err) |
| } |
| topic.DerivedOptions = derivedOptions |
| readBytes += derivedSize |
| |
| return topic, readBytes, nil |
| } |
| |
| func DeserializePartition(payload []byte, position int) (iggcon.PartitionContract, int) { |
| id := binary.LittleEndian.Uint32(payload[position : position+4]) |
| createdAt := binary.LittleEndian.Uint64(payload[position+4 : position+12]) |
| segmentsCount := binary.LittleEndian.Uint32(payload[position+12 : position+16]) |
| currentOffset := binary.LittleEndian.Uint64(payload[position+16 : position+24]) |
| sizeBytes := binary.LittleEndian.Uint64(payload[position+24 : position+32]) |
| messagesCount := binary.LittleEndian.Uint64(payload[position+32 : position+40]) |
| readBytes := 4 + 4 + 8 + 8 + 8 + 8 |
| |
| partition := iggcon.PartitionContract{ |
| Id: id, |
| CreatedAt: createdAt, |
| SegmentsCount: segmentsCount, |
| CurrentOffset: currentOffset, |
| SizeBytes: sizeBytes, |
| MessagesCount: messagesCount, |
| } |
| |
| return partition, readBytes |
| } |
| |
| func DeserializeConsumerGroups(payload []byte) []iggcon.ConsumerGroup { |
| var consumerGroups []iggcon.ConsumerGroup |
| length := len(payload) |
| position := 0 |
| |
| for position < length { |
| // use slices |
| consumerGroup, readBytes := DeserializeToConsumerGroup(payload, position) |
| consumerGroups = append(consumerGroups, *consumerGroup) |
| position += readBytes |
| } |
| |
| return consumerGroups |
| } |
| |
| func DeserializeToConsumerGroup(payload []byte, position int) (*iggcon.ConsumerGroup, int) { |
| id := binary.LittleEndian.Uint32(payload[position : position+4]) |
| partitionsCount := binary.LittleEndian.Uint32(payload[position+4 : position+8]) |
| membersCount := binary.LittleEndian.Uint32(payload[position+8 : position+12]) |
| nameLength := int(payload[position+12]) |
| name := string(payload[position+13 : position+13+nameLength]) |
| |
| readBytes := 12 + 1 + nameLength |
| |
| consumerGroup := iggcon.ConsumerGroup{ |
| Id: id, |
| MembersCount: membersCount, |
| PartitionsCount: partitionsCount, |
| Name: name, |
| } |
| |
| return &consumerGroup, readBytes |
| } |
| |
| func DeserializeConsumerGroup(payload []byte) *iggcon.ConsumerGroupDetails { |
| consumerGroup, pos := DeserializeToConsumerGroup(payload, 0) |
| members := make([]iggcon.ConsumerGroupMember, 0) |
| for pos < len(payload) { |
| m, readBytes := DeserializeToConsumerGroupMember(payload, pos) |
| members = append(members, m) |
| pos += readBytes |
| } |
| sort.Slice(members, func(i, j int) bool { |
| return members[i].ID < members[j].ID |
| }) |
| return &iggcon.ConsumerGroupDetails{ |
| ConsumerGroup: *consumerGroup, |
| Members: members, |
| } |
| } |
| |
| func DeserializeToConsumerGroupMember(payload []byte, position int) (iggcon.ConsumerGroupMember, int) { |
| id := binary.LittleEndian.Uint32(payload[position : position+4]) |
| partitionsCount := binary.LittleEndian.Uint32(payload[position+4 : position+8]) |
| var partitions []uint32 |
| for i := 0; i < int(partitionsCount); i++ { |
| partitionId := binary.LittleEndian.Uint32(payload[position+8+i*4 : position+12+i*4]) |
| partitions = append(partitions, partitionId) |
| } |
| readBytes := 4 + 4 + int(partitionsCount)*4 |
| return iggcon.ConsumerGroupMember{ |
| ID: id, |
| PartitionsCount: partitionsCount, |
| Partitions: partitions, |
| }, readBytes |
| } |
| |
| func DeserializeUsers(payload []byte) ([]iggcon.UserInfo, error) { |
| if len(payload) == 0 { |
| return nil, errors.New("empty payload") |
| } |
| |
| var result []iggcon.UserInfo |
| length := len(payload) |
| position := 0 |
| |
| for position < length { |
| response, readBytes, err := deserializeToUser(payload, position) |
| if err != nil { |
| return nil, err |
| } |
| result = append(result, *response) |
| position += readBytes |
| } |
| |
| return result, nil |
| } |
| |
| func DeserializeUser(payload []byte) (*iggcon.UserInfoDetails, error) { |
| response, position, err := deserializeToUser(payload, 0) |
| if err != nil { |
| return nil, err |
| } |
| hasPermissions := payload[position] |
| userInfo := iggcon.UserInfo{ |
| Id: response.Id, |
| CreatedAt: response.CreatedAt, |
| Username: response.Username, |
| Status: response.Status, |
| Options: response.Options, |
| } |
| if hasPermissions == 1 { |
| permissionLength := binary.LittleEndian.Uint32(payload[position+1 : position+5]) |
| permissionsPayload := payload[position+5 : position+5+int(permissionLength)] |
| permissions := deserializePermissions(permissionsPayload) |
| return &iggcon.UserInfoDetails{ |
| UserInfo: userInfo, |
| Permissions: permissions, |
| }, err |
| } |
| return &iggcon.UserInfoDetails{ |
| UserInfo: userInfo, |
| Permissions: nil, |
| }, err |
| } |
| |
| func deserializePermissions(bytes []byte) *iggcon.Permissions { |
| streamMap := make(map[int]*iggcon.StreamPermissions) |
| index := 0 |
| |
| globalPermissions := iggcon.GlobalPermissions{ |
| ManageServers: bytes[index] == 1, |
| ReadServers: bytes[index+1] == 1, |
| ManageUsers: bytes[index+2] == 1, |
| ReadUsers: bytes[index+3] == 1, |
| ManageStreams: bytes[index+4] == 1, |
| ReadStreams: bytes[index+5] == 1, |
| ManageTopics: bytes[index+6] == 1, |
| ReadTopics: bytes[index+7] == 1, |
| PollMessages: bytes[index+8] == 1, |
| SendMessages: bytes[index+9] == 1, |
| } |
| |
| index += 10 |
| |
| if bytes[index] == 1 { |
| for { |
| index += 1 |
| streamId := int(binary.LittleEndian.Uint32(bytes[index : index+4])) |
| index += 4 |
| |
| manageStream := bytes[index] == 1 |
| readStream := bytes[index+1] == 1 |
| manageTopics := bytes[index+2] == 1 |
| readTopics := bytes[index+3] == 1 |
| pollMessagesStream := bytes[index+4] == 1 |
| sendMessagesStream := bytes[index+5] == 1 |
| topicsMap := make(map[int]*iggcon.TopicPermissions) |
| |
| index += 6 |
| |
| if bytes[index] == 1 { |
| for { |
| index += 1 |
| topicId := int(binary.LittleEndian.Uint32(bytes[index : index+4])) |
| index += 4 |
| |
| manageTopic := bytes[index] == 1 |
| readTopic := bytes[index+1] == 1 |
| pollMessagesTopic := bytes[index+2] == 1 |
| sendMessagesTopic := bytes[index+3] == 1 |
| |
| topicsMap[topicId] = &iggcon.TopicPermissions{ |
| ManageTopic: manageTopic, |
| ReadTopic: readTopic, |
| PollMessages: pollMessagesTopic, |
| SendMessages: sendMessagesTopic, |
| } |
| |
| index += 4 |
| |
| if bytes[index] == 0 { |
| break |
| } |
| } |
| } |
| |
| streamMap[streamId] = &iggcon.StreamPermissions{ |
| ManageStream: manageStream, |
| ReadStream: readStream, |
| ManageTopics: manageTopics, |
| ReadTopics: readTopics, |
| PollMessages: pollMessagesStream, |
| SendMessages: sendMessagesStream, |
| Topics: topicsMap, |
| } |
| |
| index += 1 |
| |
| if bytes[index] == 0 { |
| break |
| } |
| } |
| } |
| |
| return &iggcon.Permissions{ |
| Global: globalPermissions, |
| Streams: streamMap, |
| } |
| } |
| |
| func deserializeToUser(payload []byte, position int) (*iggcon.UserInfo, int, error) { |
| if len(payload) < position+14 { |
| return nil, 0, errors.New("not enough data to map UserInfo") |
| } |
| |
| id := binary.LittleEndian.Uint32(payload[position : position+4]) |
| createdAt := binary.LittleEndian.Uint64(payload[position+4 : position+12]) |
| status := payload[position+12] |
| var userStatus iggcon.UserStatus |
| switch status { |
| case 1: |
| userStatus = iggcon.Active |
| case 2: |
| userStatus = iggcon.Inactive |
| default: |
| return nil, 0, fmt.Errorf("invalid user status: %d", status) |
| } |
| |
| usernameLength := payload[position+13] |
| if len(payload) < position+14+int(usernameLength) { |
| return nil, 0, errors.New("not enough data to map username") |
| } |
| username := string(payload[position+14 : position+14+int(usernameLength)]) |
| |
| readBytes := 4 + 8 + 1 + 1 + int(usernameLength) |
| options, optionsSize, err := deserializeOptions(payload, position+readBytes) |
| if err != nil { |
| return nil, 0, fmt.Errorf("failed to read user options: %w", err) |
| } |
| readBytes += optionsSize |
| |
| return &iggcon.UserInfo{ |
| Id: id, |
| CreatedAt: createdAt, |
| Status: userStatus, |
| Username: username, |
| Options: options, |
| }, readBytes, nil |
| } |
| |
| func DeserializeClients(payload []byte) ([]iggcon.ClientInfo, error) { |
| if len(payload) == 0 { |
| return []iggcon.ClientInfo{}, nil |
| } |
| |
| var response []iggcon.ClientInfo |
| length := len(payload) |
| position := 0 |
| |
| for position < length { |
| client, readBytes := MapClientInfo(payload, position) |
| response = append(response, client) |
| position += readBytes |
| } |
| |
| return response, nil |
| } |
| |
| func MapClientInfo(payload []byte, position int) (iggcon.ClientInfo, int) { |
| var readBytes int |
| id := binary.LittleEndian.Uint32(payload[position : position+4]) |
| userId := binary.LittleEndian.Uint32(payload[position+4 : position+8]) |
| transport := "Unknown" |
| |
| transportByte := payload[position+8] |
| switch transportByte { |
| case 1: |
| transport = string(iggcon.Tcp) |
| case 2: |
| transport = string(iggcon.Quic) |
| } |
| |
| addressLength := int(binary.LittleEndian.Uint32(payload[position+9 : position+13])) |
| address := string(payload[position+13 : position+13+addressLength]) |
| readBytes = 4 + 1 + 4 + 4 + addressLength |
| position += readBytes |
| consumerGroupsCount := binary.LittleEndian.Uint32(payload[position : position+4]) |
| readBytes += 4 |
| |
| return iggcon.ClientInfo{ |
| ID: id, |
| UserID: userId, |
| Transport: transport, |
| Address: address, |
| ConsumerGroupsCount: consumerGroupsCount, |
| }, readBytes |
| } |
| |
| func DeserializeClient(payload []byte) *iggcon.ClientInfoDetails { |
| clientInfo, position := MapClientInfo(payload, 0) |
| consumerGroups := make([]iggcon.ConsumerGroupInfo, 0, clientInfo.ConsumerGroupsCount) |
| |
| for i := uint32(0); i < clientInfo.ConsumerGroupsCount; i++ { |
| streamId := binary.LittleEndian.Uint32(payload[position : position+4]) |
| topicId := binary.LittleEndian.Uint32(payload[position+4 : position+8]) |
| groupId := binary.LittleEndian.Uint32(payload[position+8 : position+12]) |
| |
| consumerGroup := iggcon.ConsumerGroupInfo{ |
| StreamId: streamId, |
| TopicId: topicId, |
| GroupId: groupId, |
| } |
| consumerGroups = append(consumerGroups, consumerGroup) |
| position += 12 |
| } |
| |
| return &iggcon.ClientInfoDetails{ |
| ClientInfo: clientInfo, |
| ConsumerGroups: consumerGroups, |
| } |
| } |
| |
| func DeserializeAccessToken(payload []byte) (*iggcon.RawPersonalAccessToken, error) { |
| tokenLength := int(payload[0]) |
| token := string(payload[1 : 1+tokenLength]) |
| return &iggcon.RawPersonalAccessToken{ |
| Token: token, |
| }, nil |
| } |
| |
| func DeserializeAccessTokens(payload []byte) ([]iggcon.PersonalAccessTokenInfo, error) { |
| if len(payload) == 0 { |
| return []iggcon.PersonalAccessTokenInfo{}, ierror.ErrEmptyMessagePayload |
| } |
| |
| var result []iggcon.PersonalAccessTokenInfo |
| position := 0 |
| length := len(payload) |
| |
| for position < length { |
| response, readBytes := deserializeToPersonalAccessTokenResponse(payload, position) |
| result = append(result, response) |
| position += readBytes |
| } |
| |
| return result, nil |
| } |
| |
| func deserializeToPersonalAccessTokenResponse(payload []byte, position int) (iggcon.PersonalAccessTokenInfo, int) { |
| nameLength := int(payload[position]) |
| name := string(payload[position+1 : position+1+nameLength]) |
| expiryBytes := payload[position+1+nameLength:] |
| var expiry *time.Time |
| |
| if len(expiryBytes) >= 8 { |
| unixMicroSeconds := binary.LittleEndian.Uint64(expiryBytes) |
| expiryTime := time.Unix(0, int64(unixMicroSeconds)) |
| expiry = &expiryTime |
| } |
| |
| readBytes := 1 + nameLength + 8 |
| |
| return iggcon.PersonalAccessTokenInfo{ |
| Name: name, |
| Expiry: expiry, |
| }, readBytes |
| } |
| |
| // confirmationEntryLength is the width of one send confirmation: |
| // [stream_id u32][topic_id u32][partition_id u32][base_offset u64]. |
| const confirmationEntryLength = 20 |
| |
| // DeserializeSendMessagesConfirmations decodes [count u32] followed by count |
| // confirmation entries. An empty payload is an empty list. Trailing bytes are |
| // a decode error, because a frame the client cannot account for means the |
| // stream is out of sync. |
| func DeserializeSendMessagesConfirmations(payload []byte) (*iggcon.SendMessagesResponse, error) { |
| if len(payload) == 0 { |
| return &iggcon.SendMessagesResponse{}, nil |
| } |
| if len(payload) < 4 { |
| return nil, fmt.Errorf("send confirmations: %d bytes is short of the count", len(payload)) |
| } |
| |
| count := binary.LittleEndian.Uint32(payload) |
| body := payload[4:] |
| if uint64(len(body)) != uint64(count)*confirmationEntryLength { |
| return nil, fmt.Errorf( |
| "send confirmations: %d entries do not fill %d body bytes", count, len(body)) |
| } |
| |
| confirmations := make([]iggcon.SendMessagesConfirmation, 0, count) |
| for offset := 0; offset < len(body); offset += confirmationEntryLength { |
| entry := body[offset : offset+confirmationEntryLength] |
| confirmations = append(confirmations, iggcon.SendMessagesConfirmation{ |
| StreamId: binary.LittleEndian.Uint32(entry[0:4]), |
| TopicId: binary.LittleEndian.Uint32(entry[4:8]), |
| PartitionId: binary.LittleEndian.Uint32(entry[8:12]), |
| BaseOffset: binary.LittleEndian.Uint64(entry[12:20]), |
| }) |
| } |
| return &iggcon.SendMessagesResponse{Confirmations: confirmations}, nil |
| } |
| |
| // assignmentHeaderLength covers [generation u64][partitions_count u32]. |
| const assignmentHeaderLength = 12 |
| |
| // DeserializeConsumerGroupAssignment decodes |
| // [generation u64][partitions_count u32][partition_id u32 x n]. An empty |
| // payload means the client is not a member of the group, reported as |
| // ErrConsumerGroupMemberNotFound. A member holding no partition arrives as a |
| // header with a zero count and decodes to an empty Partitions slice. |
| func DeserializeConsumerGroupAssignment(payload []byte) (*iggcon.ConsumerGroupAssignment, error) { |
| if len(payload) == 0 { |
| return nil, ierror.ErrConsumerGroupMemberNotFound |
| } |
| if len(payload) < assignmentHeaderLength { |
| return nil, fmt.Errorf("group assignment: %d bytes is short of the header", len(payload)) |
| } |
| |
| count := binary.LittleEndian.Uint32(payload[8:12]) |
| body := payload[assignmentHeaderLength:] |
| if uint64(len(body)) != uint64(count)*4 { |
| return nil, fmt.Errorf( |
| "group assignment: %d partitions do not fill %d body bytes", count, len(body)) |
| } |
| |
| partitions := make([]uint32, 0, count) |
| for offset := 0; offset < len(body); offset += 4 { |
| partitions = append(partitions, binary.LittleEndian.Uint32(body[offset:offset+4])) |
| } |
| return &iggcon.ConsumerGroupAssignment{ |
| Generation: binary.LittleEndian.Uint64(payload[0:8]), |
| Partitions: partitions, |
| }, nil |
| } |