ArrayEncoder.java

/*
 * SPDX-FileCopyrightText: 2023-2025 Lucimber UG
 * SPDX-License-Identifier: Apache-2.0
 */
package com.lucimber.dbus.codec.encoder;

import com.lucimber.dbus.type.DBusArray;
import com.lucimber.dbus.type.DBusSignature;
import com.lucimber.dbus.type.DBusType;
import com.lucimber.dbus.type.DBusUInt32;
import com.lucimber.dbus.type.Type;
import com.lucimber.dbus.type.TypeUtils;
import com.lucimber.dbus.util.LoggerUtils;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An encoder which encodes an array to the D-Bus marshalling format using ByteBuffer.
 *
 * @param <E> The element's data type.
 * @see Encoder
 * @see DBusArray
 */
public final class ArrayEncoder<E extends DBusType> implements Encoder<DBusArray<E>, ByteBuffer> {

    private static final Logger LOGGER = LoggerFactory.getLogger(ArrayEncoder.class);

    // D-Bus specification: maximum array size is 64 MiB (67,108,864 bytes)
    private static final int MAX_ARRAY_SIZE = 67108864; // 2^26

    private final ByteOrder order;
    private final DBusSignature signature;

    /**
     * Constructs a new instance.
     *
     * @param order the byte order of the produced bytes
     * @param signature the array signature (must be an array)
     */
    public ArrayEncoder(ByteOrder order, DBusSignature signature) {
        this.order = Objects.requireNonNull(order, "order must not be null");
        this.signature = Objects.requireNonNull(signature, "signature must not be null");
        if (!signature.isArray()) {
            throw new IllegalArgumentException("signature does not describe an array");
        }
    }

    @Override
    public EncoderResult<ByteBuffer> encode(DBusArray<E> array, int offset)
            throws EncoderException {
        Objects.requireNonNull(array, "array must not be null");

        try {
            // Alignment of the array itself
            int padding = EncoderUtils.calculateAlignmentPadding(Type.ARRAY.getAlignment(), offset);

            // Determine element type and alignment
            char typeChar = signature.subContainer().toString().charAt(0);
            Type elementType =
                    TypeUtils.getTypeFromChar(typeChar)
                            .orElseThrow(
                                    () ->
                                            new EncoderException(
                                                    "Cannot map char to type: " + typeChar));
            int arraySizeBytes = 4;
            int typeOffset = offset + padding + arraySizeBytes;
            int typePadding =
                    EncoderUtils.calculateAlignmentPadding(elementType.getAlignment(), typeOffset);

            // Encode elements into temporary buffers
            int entryOffsetBase = offset + padding + arraySizeBytes + typePadding;
            List<ByteBuffer> encodedElements = new ArrayList<>();
            int elementsSize = 0;
            for (E element : array) {
                EncoderResult<ByteBuffer> result =
                        EncoderUtils.encode(element, entryOffsetBase + elementsSize, order);
                elementsSize += result.getProducedBytes();
                encodedElements.add(result.getBuffer());
            }

            // Check array size limit before encoding
            if (elementsSize > MAX_ARRAY_SIZE) {
                throw new EncoderException(
                        "Array too large: "
                                + elementsSize
                                + " bytes, maximum "
                                + MAX_ARRAY_SIZE
                                + " bytes");
            }

            // Encode the length prefix
            Encoder<DBusUInt32, ByteBuffer> lengthEncoder = new UInt32Encoder(order);
            int lengthOffset = offset + padding;
            EncoderResult<ByteBuffer> lengthResult =
                    lengthEncoder.encode(DBusUInt32.valueOf(elementsSize), lengthOffset);
            ByteBuffer lengthBuffer = lengthResult.getBuffer();

            // Compose the final buffer
            int totalSize = padding + lengthResult.getProducedBytes() + typePadding + elementsSize;
            ByteBuffer buffer = ByteBuffer.allocate(totalSize).order(order);

            // Write padding
            for (int i = 0; i < padding; i++) {
                buffer.put((byte) 0);
            }

            // Write array length
            buffer.put(lengthBuffer);

            // Write type alignment padding
            for (int i = 0; i < typePadding; i++) {
                buffer.put((byte) 0);
            }

            // Write array elements
            for (ByteBuffer part : encodedElements) {
                buffer.put(part);
            }

            buffer.flip();

            LOGGER.debug(
                    LoggerUtils.MARSHALLING,
                    "ARRAY: {}; Offset: {}; Padding: {}; Produced bytes: {};",
                    signature,
                    offset,
                    padding + typePadding,
                    totalSize);

            return new EncoderResultImpl<>(totalSize, buffer);
        } catch (Exception ex) {
            throw new EncoderException("Could not encode ARRAY of type " + signature, ex);
        }
    }
}