DBusSignature.java
/*
* SPDX-FileCopyrightText: 2025 Lucimber UG
* SPDX-License-Identifier: Apache-2.0
*/
package com.lucimber.dbus.type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* D-Bus uses a string-based type encoding mechanism called Signatures to describe the number and
* types of arguments required by methods and signals. Signatures are used for interface
* declaration/documentation, data marshalling, and validity checking. Their string encoding uses a
* simple, though expressive, format and a basic understanding of it is required for effective D-Bus
* use.
*
* @see <a href="https://pythonhosted.org/txdbus/dbus_overview.html">DBus Overview (Key
* Components)</a>
*/
public final class DBusSignature implements DBusBasicType {
private static final short MAX_SIGNATURE_LENGTH = 255;
private final Node rootNode;
private DBusSignature(final Node rootNode) {
this.rootNode = Objects.requireNonNull(rootNode);
}
/**
* Constructs a new {@link DBusSignature} instance by parsing a {@link CharSequence}.
*
* @param sequence The sequence composed of one or multiple single complete types.
* @return A new instance of {@link DBusSignature}.
* @throws SignatureException If the given {@link CharSequence} is not well-formed.
*/
public static DBusSignature valueOf(final CharSequence sequence) throws SignatureException {
Objects.requireNonNull(sequence, "sequence must not be null");
if (sequence.length() == 0) {
throw new SignatureException("sequence must not be empty");
}
if (sequence.length() > MAX_SIGNATURE_LENGTH) {
throw new SignatureException("Sequence exceeds maximum allowed length.");
}
final TypeCode[] codes = TypeCodeLexer.produceTokens(sequence);
return parse(codes);
}
private static int countCode(final TypeCode[] codes, final TypeCode code) {
int quantity = 0;
for (TypeCode typeCode : codes) {
if (typeCode == code) {
quantity++;
}
}
return quantity;
}
private static DBusSignature parse(final TypeCode[] codes) {
ensureBracketBalance(codes);
final Node rootNode = new Node();
Node node = rootNode;
for (int i = 0; i < codes.length; i++) {
final TypeCode code = codes[i];
if (code == TypeCode.ARRAY) {
final Optional<Node> optNode = parseArrayTypeCode(node, code, i, codes.length - 1);
if (optNode.isPresent()) {
node = optNode.get();
}
} else if (code == TypeCode.DICT_ENTRY_START) {
node = parseDictEntryStartTypeCode(node, i, codes.length - 1);
} else if (code == TypeCode.DICT_ENTRY_END) {
node = parseDictEntryEndTypeCode(node, i);
} else if (code == TypeCode.STRUCT_START) {
node = parseStructStartTypeCode(node, i, codes.length - 1);
} else if (code == TypeCode.STRUCT_END) {
node = parseStructEndTypeCode(node, i);
} else {
final Optional<Node> optNode = parseTypeCode(node, code, i, codes.length - 1);
if (optNode.isPresent()) {
node = optNode.get();
}
}
}
if (rootNode.type == null && rootNode.children != null && rootNode.children.size() == 1) {
final Node child = rootNode.children.get(0);
rootNode.type = child.type;
rootNode.children = new ArrayList<>();
for (Node subChild : child.children) {
rootNode.children.add(deepClone(subChild, rootNode));
}
}
return new DBusSignature(rootNode);
}
private static void ensureBracketBalance(final TypeCode[] codes) {
final int numDictEntryStart = countCode(codes, TypeCode.DICT_ENTRY_START);
final int numDictEntryEnd = countCode(codes, TypeCode.DICT_ENTRY_END);
if (numDictEntryStart != numDictEntryEnd) {
throw new SignatureException("mismatch of dict-entry braces");
}
final int numStructStart = countCode(codes, TypeCode.STRUCT_START);
final int numStructEnd = countCode(codes, TypeCode.STRUCT_END);
if (numStructStart != numStructEnd) {
throw new SignatureException("mismatch of struct parentheses");
}
}
private static Optional<Node> parseTypeCode(
final Node node, final TypeCode code, final int idx, final int lastIdx) {
final Type type =
TypeUtils.getTypeFromCode(code)
.orElseThrow(
() -> new SignatureException("can not map code to type: " + code));
if (idx == lastIdx && node.children == null && node.type == null) {
node.type = type;
} else {
node.addChild(type);
if (node.type == Type.ARRAY) {
return Optional.ofNullable(node.parent);
}
}
return Optional.empty();
}
private static Node parseStructEndTypeCode(final Node node, final int idx) {
if (node.type != Type.STRUCT) {
final String msg = "Error at position %d." + " Missing opening parenthesis of struct.";
throw new SignatureException(String.format(msg, idx));
}
if (node.children == null || node.children.isEmpty()) {
final String msg = "Error at position %d." + " Empty structures are not allowed.";
throw new SignatureException(String.format(msg, idx));
}
if (node.parent != null && node.parent.type == Type.ARRAY) {
Node refNode = node.parent;
while (refNode.type == Type.ARRAY) {
refNode = refNode.parent;
}
return refNode;
} else {
return node.parent;
}
}
private static Optional<Node> parseArrayTypeCode(
final Node node, final TypeCode code, final int idx, final int lastIdx) {
if (idx == lastIdx) {
final String msg =
"Error at position %d."
+ " Array type code must be followed by a single complete type.";
throw new SignatureException(String.format(msg, idx));
}
final Type type =
TypeUtils.getTypeFromCode(code)
.orElseThrow(
() -> new SignatureException("can not map code to type: " + code));
if (idx == lastIdx - 1 && node.type == null && node.children == null) {
node.type = type;
return Optional.empty();
} else {
final Node child = node.addChild(type);
return Optional.of(child);
}
}
private static Node parseDictEntryStartTypeCode(
final Node node, final int idx, final int lastIdx) {
if (node.type != null && node.type != Type.ARRAY) {
final String msg =
"Error at position %d."
+ " Dict-entry must occur only as an array element type.";
throw new SignatureException(String.format(msg, idx));
}
if (idx == lastIdx) {
final String msg =
"Error at position %d." + " Missing closing curly bracket of dict-entry.";
throw new SignatureException(String.format(msg, idx));
}
return node.addChild(Type.DICT_ENTRY);
}
private static Node parseDictEntryEndTypeCode(final Node node, final int idx) {
if (node.type != Type.DICT_ENTRY) {
final String msg =
"Error at position %d." + " Missing opening curly bracket of dict-entry.";
throw new SignatureException(String.format(msg, idx));
}
if (node.children == null || node.children.size() != 2) {
final String msg =
"Error at position %d."
+ " Dict-entry must consists of two single complete types.";
throw new SignatureException(String.format(msg, idx));
}
// Maybe: Move checks to parseTypeCode method
final Node keyNode = node.children.get(0);
if (keyNode.type == Type.ARRAY
|| keyNode.type == Type.DICT_ENTRY
|| keyNode.type == Type.STRUCT
|| keyNode.type == Type.VARIANT) {
final String msg =
"Error at position %d."
+ " The first single complete type of a dict-entry must be a basic type.";
throw new SignatureException(String.format(msg, idx));
}
if (node.parent != null && node.parent.type == Type.ARRAY) {
Node refNode = node.parent;
while (refNode.type == Type.ARRAY) {
refNode = refNode.parent;
}
return refNode;
} else {
return node.parent;
}
}
private static Node parseStructStartTypeCode(
final Node node, final int idx, final int lastIdx) {
if (idx == lastIdx) {
final String msg = "Error at position %d." + " Missing closing parenthesis of struct.";
throw new SignatureException(String.format(msg, idx));
}
return node.addChild(Type.STRUCT);
}
private static void appendTypeCodeToStringBuilder(
final StringBuilder builder, final Node node) {
if (node.type == null) {
if (node.children != null) {
for (Node child : node.children) {
appendTypeCodeToStringBuilder(builder, child);
}
}
} else {
if (node.type == Type.ARRAY) {
appendArrayToStringBuilder(builder, node);
} else if (node.type == Type.DICT_ENTRY) {
appendDictEntryToStringBuilder(builder, node);
} else if (node.type == Type.STRUCT) {
appendStructToStringBuilder(builder, node);
} else {
builder.append(node.type.getCode().getChar());
}
}
}
private static void appendStructToStringBuilder(final StringBuilder builder, final Node node) {
builder.append(TypeCode.STRUCT_START.getChar());
if (node.children != null) {
for (Node child : node.children) {
appendTypeCodeToStringBuilder(builder, child);
}
}
builder.append(TypeCode.STRUCT_END.getChar());
}
private static void appendDictEntryToStringBuilder(
final StringBuilder builder, final Node node) {
builder.append(TypeCode.DICT_ENTRY_START.getChar());
if (node.children != null) {
for (Node child : node.children) {
appendTypeCodeToStringBuilder(builder, child);
}
}
builder.append(TypeCode.DICT_ENTRY_END.getChar());
}
private static void appendArrayToStringBuilder(final StringBuilder builder, final Node node) {
builder.append(TypeCode.ARRAY.getChar());
if (node.children != null) {
for (Node child : node.children) {
appendTypeCodeToStringBuilder(builder, child);
}
}
}
private static Node deepClone(final Node node, final Node newParent) {
final Node clonedNode = new Node();
clonedNode.parent = newParent;
clonedNode.type = node.type;
if (node.children != null) {
clonedNode.children = new ArrayList<>();
for (Node child : node.children) {
clonedNode.children.add(deepClone(child, clonedNode));
}
}
return clonedNode;
}
/**
* Gets a list of signatures that are a subset of this signature. Each signature describes a
* single complete type.
*
* @return a {@link List} of {@link DBusSignature}s
*/
public List<DBusSignature> getChildren() {
return rootNode.children == null
? Collections.emptyList()
: rootNode.children.stream()
.map(child -> deepClone(child, null))
.map(DBusSignature::new)
.collect(Collectors.toList());
}
/**
* Gets the number of single complete types of which this signature consists of.
*
* @return an {@link Integer}
*/
public int getQuantity() {
return rootNode.type != null ? 1 : rootNode.children == null ? 1 : rootNode.children.size();
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
appendTypeCodeToStringBuilder(builder, rootNode);
return builder.toString();
}
@Override
public Type getType() {
return Type.SIGNATURE;
}
@Override
public String getDelegate() {
return toString();
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final DBusSignature signature = (DBusSignature) o;
return Objects.equals(rootNode, signature.rootNode);
}
@Override
public int hashCode() {
return Objects.hash(rootNode);
}
/**
* Returns {@code TRUE}, if this signature describes a dictionary and {@code FALSE} otherwise. A
* dictionary is an array of dict-entries.
*
* @return a {@link Boolean}
*/
public boolean isDictionary() {
if (rootNode.children == null || rootNode.children.size() != 1) {
return false;
}
return rootNode.type == Type.ARRAY && rootNode.children.get(0).type == Type.DICT_ENTRY;
}
/**
* Returns {@code TRUE}, if this signature describes an array and {@code FALSE} otherwise.
*
* @return a {@link Boolean}
*/
public boolean isArray() {
if (rootNode.children == null || rootNode.children.size() != 1) {
return false;
}
return rootNode.type == Type.ARRAY && rootNode.children.get(0).type != Type.DICT_ENTRY;
}
/**
* Returns {@code TRUE}, if this signature describes a dictionary-entry and {@code FALSE}
* otherwise. A dictionary-entry is a key-value pair.
*
* @return a {@link Boolean}
*/
public boolean isDictionaryEntry() {
return rootNode.type == Type.DICT_ENTRY;
}
/**
* Returns {@code TRUE}, if this signature describes a struct and {@code FALSE} otherwise.
*
* @return a {@link Boolean}
*/
public boolean isStruct() {
return rootNode.type == Type.STRUCT;
}
/**
* Returns {@code TRUE}, if this signature describes a variant and {@code FALSE} otherwise.
*
* @return a {@link Boolean}
*/
public boolean isVariant() {
return rootNode.type == Type.VARIANT;
}
/**
* Returns a new signature without the enclosing container description. The signature must
* describe an array, a dict-entry or a struct type.
*
* @return a {@link DBusSignature}
* @throws IllegalArgumentException If the signature does not describe an array, a dict-entry or
* a struct type.
*/
public DBusSignature subContainer() {
if (rootNode.children == null
|| !(rootNode.type == Type.ARRAY
|| rootNode.type == Type.DICT_ENTRY
|| rootNode.type == Type.STRUCT)) {
throw new IllegalArgumentException(
"Signature must describe an array, a dict-entry or a struct type.");
}
if (rootNode.children.size() == 1) {
final Node clonedChild = deepClone(rootNode.children.get(0), null);
return new DBusSignature(clonedChild);
} else {
final Node subRootNode = new Node();
subRootNode.children = new ArrayList<>();
for (Node child : rootNode.children) {
final Node clonedChild = deepClone(child, subRootNode);
subRootNode.children.add(clonedChild);
}
return new DBusSignature(subRootNode);
}
}
/**
* Returns {@code TRUE}, if this signature describes a container and {@code FALSE} otherwise. A
* container can be an ARRAY, a DICT-ENTRY, a STRUCT or a VARIANT.
*
* @return a {@link Boolean}
* @throws IllegalArgumentException If this signature consists of more than one single complete
* type.
*/
public boolean isContainerType() {
return rootNode.type != null
&& (rootNode.type == Type.ARRAY
|| rootNode.type == Type.DICT_ENTRY
|| rootNode.type == Type.STRUCT
|| rootNode.type == Type.VARIANT);
}
private static final class Node {
private Node parent = null;
private Type type = null;
private List<Node> children = null;
Node addChild(final Type code) {
if (children == null) {
children = new ArrayList<>();
}
final Node child = new Node();
child.parent = this;
child.type = code;
children.add(child);
return child;
}
@Override
public String toString() {
return "Node";
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final Node node = (Node) o;
return type == node.type && Objects.equals(children, node.children);
}
@Override
public int hashCode() {
return Objects.hash(type, children);
}
}
}