DBusObjectPath.java
/*
* SPDX-FileCopyrightText: 2025 Lucimber UG
* SPDX-License-Identifier: Apache-2.0
*/
package com.lucimber.dbus.type;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* D-Bus objects are identified within an application via their object path. The object path
* intentionally looks just like a standard Unix file system path. The primary difference is that
* the path may contain only numbers, letters, underscores, and the / character.
*
* <p>From a functional standpoint, the primary purpose of object paths is simply to be a unique
* identifier for an object. The "hierarchy" implied the path structure is almost purely
* conventional. Applications with a naturally hierarchical structure will likely take advantage of
* this feature while others may choose to ignore it completely.
*
* @see <a href="https://pythonhosted.org/txdbus/dbus_overview.html">DBus Overview (Key
* Components)</a>
*/
public final class DBusObjectPath implements DBusBasicType {
private static final Pattern PATTERN = Pattern.compile("^/|(/[a-zA-Z0-9_]+)+$");
private static final int MAX_PATH_LENGTH = 268435455; // 2^28 - 1 (256MB - 1)
private final String delegate;
private DBusObjectPath(final CharSequence sequence) {
this.delegate = sequence.toString();
}
/**
* Constructs a new {@link DBusObjectPath} instance by parsing a {@link CharSequence}. Validates
* that the path is valid UTF-8, follows D-Bus object path syntax, and is within size limits.
*
* @param sequence The sequence composed of a valid object path.
* @return A new instance of {@link DBusObjectPath}.
* @throws ObjectPathException If the given {@link CharSequence} is not well formed.
*/
public static DBusObjectPath valueOf(final CharSequence sequence) throws ObjectPathException {
Objects.requireNonNull(sequence, "sequence must not be null");
String pathStr = sequence.toString();
// Validate UTF-8 and check for NUL characters
byte[] utf8Bytes = pathStr.getBytes(StandardCharsets.UTF_8);
String roundTrip = new String(utf8Bytes, StandardCharsets.UTF_8);
if (!pathStr.equals(roundTrip)) {
throw new ObjectPathException("Object path contains invalid UTF-8 sequences");
}
if (pathStr.contains("\u0000")) {
throw new ObjectPathException("Object path must not contain NUL characters");
}
// Check size limit
if (utf8Bytes.length > MAX_PATH_LENGTH) {
throw new ObjectPathException(
"Object path too long: "
+ utf8Bytes.length
+ " bytes, maximum "
+ MAX_PATH_LENGTH);
}
// Validate enhanced object path syntax
if (!isValidObjectPath(pathStr)) {
throw new ObjectPathException("Invalid object path syntax: " + pathStr);
}
return new DBusObjectPath(sequence);
}
/**
* Enhanced validation for D-Bus object path syntax.
*
* @param path the path to validate
* @return true if valid, false otherwise
*/
private static boolean isValidObjectPath(String path) {
// Basic pattern check
final Matcher matcher = PATTERN.matcher(path);
if (!matcher.matches()) {
return false;
}
// Root path is always valid
if (path.equals("/")) {
return true;
}
// Additional edge case validation for non-root paths
// No trailing slash (except root)
if (path.endsWith("/")) {
return false;
}
// No consecutive slashes
if (path.contains("//")) {
return false;
}
// Validate components (skip first empty element from split)
String[] components = path.split("/");
for (int i = 1; i < components.length; i++) { // Start at 1 to skip empty first element
String component = components[i];
if (component.isEmpty()) {
return false; // Empty component (from consecutive slashes)
}
}
return true;
}
/**
* Gets the wrapped string value of this object path.
*
* @return a string value
*/
public CharSequence getWrappedValue() {
return delegate;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final DBusObjectPath path = (DBusObjectPath) o;
return delegate.equals(path.delegate);
}
@Override
public int hashCode() {
return Objects.hash(delegate);
}
@Override
public String toString() {
return delegate;
}
/**
* Tests if this path start with the specified prefix.
*
* @param prefix the prefix
* @return {@code true} if the object path represented by the argument is a prefix; {@code
* false} otherwise.
*/
public boolean startsWith(final DBusObjectPath prefix) {
return delegate.startsWith(prefix.delegate);
}
/**
* Tests if this path ends with the specified suffix.
*
* @param suffix the suffix
* @return {@code true} if the object path represented by the argument is a suffix; {@code
* false} otherwise.
*/
public boolean endsWith(final DBusObjectPath suffix) {
return delegate.endsWith(suffix.delegate);
}
@Override
public Type getType() {
return Type.OBJECT_PATH;
}
@Override
public String getDelegate() {
return delegate;
}
}