DictDecoder.java

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

import com.lucimber.dbus.type.DBusBasicType;
import com.lucimber.dbus.type.DBusDict;
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.TypeCode;
import com.lucimber.dbus.type.TypeUtils;
import com.lucimber.dbus.util.LoggerUtils;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A decoder which unmarshals a dictionary from the byte stream format used by D-Bus.
 *
 * @param <KeyT> The data type of the key.
 * @param <ValueT> The data type of the value.
 */
public final class DictDecoder<KeyT extends DBusBasicType, ValueT extends DBusType>
        implements Decoder<ByteBuffer, DBusDict<KeyT, ValueT>> {

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

    private final TypeCode keyTypeCode;
    private final DBusSignature signature;
    private final DBusSignature valueSignature;

    /**
     * Creates a new decoder for D-Bus dictionary types.
     *
     * @param signature the dictionary signature to decode
     * @throws IllegalArgumentException if signature is not a dictionary
     */
    public DictDecoder(DBusSignature signature) {
        this.signature = Objects.requireNonNull(signature, "signature must not be null");
        if (!signature.isDictionary()) {
            throw new IllegalArgumentException("Signature must describe a dictionary.");
        }

        DBusSignature keyValueSig = signature.subContainer().subContainer();
        List<DBusSignature> children = keyValueSig.getChildren();
        char keyChar = children.get(0).toString().charAt(0);
        this.keyTypeCode =
                TypeUtils.getCodeFromChar(keyChar)
                        .orElseThrow(
                                () ->
                                        new RuntimeException(
                                                "Cannot map char to type code: " + keyChar));
        this.valueSignature = children.get(1);
    }

    @Override
    public DecoderResult<DBusDict<KeyT, ValueT>> decode(ByteBuffer buffer, int offset)
            throws DecoderException {
        Objects.requireNonNull(buffer, "buffer must not be null");
        try {
            int consumedBytes = 0;

            int arrayPadding = DecoderUtils.skipPadding(buffer, offset, Type.ARRAY);
            consumedBytes += arrayPadding;

            int lengthOffset = offset + consumedBytes;
            DecoderResult<DBusUInt32> lengthResult =
                    DecoderUtils.decodeBasicType(TypeCode.UINT32, buffer, lengthOffset);
            DBusUInt32 length = lengthResult.getValue();
            DecoderUtils.verifyArrayLength(length);
            consumedBytes += lengthResult.getConsumedBytes();

            int entriesOffset = offset + consumedBytes;
            DecoderResult<DBusDict<KeyT, ValueT>> entriesResult =
                    decodeEntries(buffer, entriesOffset, length);
            consumedBytes += entriesResult.getConsumedBytes();

            DecoderResult<DBusDict<KeyT, ValueT>> finalResult =
                    new DecoderResultImpl<>(consumedBytes, entriesResult.getValue());

            LOGGER.debug(
                    LoggerUtils.MARSHALLING,
                    "DICT: {}; Offset: {}; Padding: {}; Consumed bytes: {};",
                    signature,
                    offset,
                    arrayPadding,
                    consumedBytes);

            return finalResult;
        } catch (Exception ex) {
            throw new DecoderException("Could not decode DICT.", ex);
        }
    }

    private DecoderResult<DBusDict<KeyT, ValueT>> decodeEntries(
            ByteBuffer buffer, int offset, DBusUInt32 length) throws DecoderException {
        DBusDict<KeyT, ValueT> dict = new DBusDict<>(signature);
        int consumedBytes = 0;

        if (length.getDelegate() == 0) {
            int padding = DecoderUtils.skipPadding(buffer, offset, Type.DICT_ENTRY);
            consumedBytes += padding;
            return new DecoderResultImpl<>(consumedBytes, dict);
        }

        while (Integer.compareUnsigned(consumedBytes, length.getDelegate()) < 0) {
            int entryOffset = offset + consumedBytes;
            int padding = DecoderUtils.skipPadding(buffer, entryOffset, Type.DICT_ENTRY);
            consumedBytes += padding;

            int keyOffset = offset + consumedBytes;
            DecoderResult<KeyT> keyResult =
                    DecoderUtils.decodeBasicType(keyTypeCode, buffer, keyOffset);
            consumedBytes += keyResult.getConsumedBytes();

            int valueOffset = offset + consumedBytes;
            DecoderResult<ValueT> valueResult =
                    DecoderUtils.decode(valueSignature, buffer, valueOffset);
            consumedBytes += valueResult.getConsumedBytes();

            dict.put(keyResult.getValue(), valueResult.getValue());
        }

        return new DecoderResultImpl<>(consumedBytes, dict);
    }
}