Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
javaVersion=25
mcVersion=1.21.11
group=dev.slne.surf
version=1.21.11-2.70.3
version=1.21.11-2.71.0
relocationPrefix=dev.slne.surf.surfapi.libs
snapshot=false
37 changes: 37 additions & 0 deletions surf-api-core/surf-api-core-api/api/surf-api-core-api.api
Original file line number Diff line number Diff line change
Expand Up @@ -10633,6 +10633,43 @@ public final class dev/slne/surf/surfapi/core/api/util/ParticleFactory$Companion
public final fun of (Lcom/github/retrooper/packetevents/protocol/particle/type/ParticleType;Lcom/github/retrooper/packetevents/util/Vector3i;Lcom/github/retrooper/packetevents/util/Vector3i;I)Lcom/github/retrooper/packetevents/protocol/particle/Particle;
}

public final class dev/slne/surf/surfapi/core/api/util/SerializableError {
public static final field Companion Ldev/slne/surf/surfapi/core/api/util/SerializableError$Companion;
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ldev/slne/surf/surfapi/core/api/util/SerializableError;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ldev/slne/surf/surfapi/core/api/util/SerializableError;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun buildFakeThrowable ()Ljava/lang/Throwable;
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Ldev/slne/surf/surfapi/core/api/util/SerializableError;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ldev/slne/surf/surfapi/core/api/util/SerializableError;)Ldev/slne/surf/surfapi/core/api/util/SerializableError;
public static synthetic fun copy$default (Ldev/slne/surf/surfapi/core/api/util/SerializableError;Ljava/lang/String;Ljava/lang/String;Ldev/slne/surf/surfapi/core/api/util/SerializableError;ILjava/lang/Object;)Ldev/slne/surf/surfapi/core/api/util/SerializableError;
public fun equals (Ljava/lang/Object;)Z
public final fun getCause ()Ldev/slne/surf/surfapi/core/api/util/SerializableError;
public final fun getMessage ()Ljava/lang/String;
public final fun getType ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final synthetic class dev/slne/surf/surfapi/core/api/util/SerializableError$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public static final field INSTANCE Ldev/slne/surf/surfapi/core/api/util/SerializableError$$serializer;
public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ldev/slne/surf/surfapi/core/api/util/SerializableError;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Ldev/slne/surf/surfapi/core/api/util/SerializableError;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}

public final class dev/slne/surf/surfapi/core/api/util/SerializableError$Companion {
public final fun serializer ()Lkotlinx/serialization/KSerializer;
}

public final class dev/slne/surf/surfapi/core/api/util/SerializableErrorKt {
public static final fun toSerializableError (Ljava/lang/Throwable;)Ldev/slne/surf/surfapi/core/api/util/SerializableError;
}

public abstract class dev/slne/surf/surfapi/core/api/util/SurfTypeParameterMatcher {
public static final field Companion Ldev/slne/surf/surfapi/core/api/util/SurfTypeParameterMatcher$Companion;
public fun <init> ()V
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package dev.slne.surf.surfapi.core.api.util

import kotlinx.serialization.Serializable

/**
* Represents an error in a serializable structure, which can encapsulate information about
* the type, an optional message, and an optional nested cause.
*
* This class is primarily used for the structured serialization of error information and offers
* the ability to generate a throwable representation of the error.
*
* @property type The type or category of the error, describing the nature of the issue.
* @property message An optional detailed message providing more context about the error.
* @property cause An optional nested instance of `SerializableError` representing the underlying cause of the error.
*/
@Serializable
data class SerializableError(
val type: String,
val message: String? = null,
val cause: SerializableError? = null
) {

/**
* Constructs a Throwable representation of the SerializableError instance.
* The resulting Throwable includes the error type as its primary message, optionally followed
* by the detailed message if provided. If the error has a nested cause, it recursively builds
* a Throwable for the cause as well.
*
* @return A Throwable instance representing the SerializableError, including its type,
* optional message, and optional cause.
*/
fun buildFakeThrowable(): Throwable {
val throwableMessage = buildString {
append(type)
if (!message.isNullOrBlank()) {
append(": ")
append(message)
}
}

return RuntimeException(throwableMessage, cause?.buildFakeThrowable())
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildFakeThrowable() always constructs a new RuntimeException, which will capture a fresh local stacktrace. That stacktrace will point to the reconstruction site (not the original error) and can be surprisingly expensive if this is used frequently or for deep cause chains. Consider disabling stacktrace generation (e.g., using the RuntimeException(String, Throwable, boolean, boolean) ctor with writableStackTrace=false, or a custom Throwable overriding fillInStackTrace()), and/or explicitly clearing stackTrace on the created Throwable.

Suggested change
return RuntimeException(throwableMessage, cause?.buildFakeThrowable())
return RuntimeException(throwableMessage, cause?.buildFakeThrowable(), false, false)

Copilot uses AI. Check for mistakes.
}
Comment on lines +40 to +42
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cause?.buildFakeThrowable() recurses without any depth limit or cycle detection. If a SerializableError graph is very deep or (maliciously) cyclic, this can trigger unbounded recursion and a StackOverflowError during reconstruction. Consider guarding with a max depth and/or tracking visited nodes.

Copilot uses AI. Check for mistakes.
}

/**
* Converts a `Throwable` instance into a `SerializableError` representation.
* This allows for structured serialization of exception details, including the type,
* message, and nested causes of the original `Throwable`.
*
* @return A `SerializableError` containing the type of the `Throwable`, its message,
* and recursively serialized causes (if any).
*/
fun Throwable.toSerializableError(): SerializableError = SerializableError(
type = this::class.qualifiedName ?: this::class.simpleName ?: "UnknownThrowable",
message = message,
cause = cause?.toSerializableError()
)
Comment on lines +50 to +57
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cause = cause?.toSerializableError() recurses through the Throwable cause chain without any depth limit or cycle detection. A very deep chain (or a cycle) can lead to unbounded recursion and StackOverflowError during serialization. Consider adding a max depth and/or tracking visited Throwables by identity when converting.

Suggested change
* @return A `SerializableError` containing the type of the `Throwable`, its message,
* and recursively serialized causes (if any).
*/
fun Throwable.toSerializableError(): SerializableError = SerializableError(
type = this::class.qualifiedName ?: this::class.simpleName ?: "UnknownThrowable",
message = message,
cause = cause?.toSerializableError()
)
* To avoid unbounded recursion on very deep or cyclic cause chains, this conversion
* limits the maximum depth and tracks visited throwables by identity.
*
* @return A `SerializableError` containing the type of the `Throwable`, its message,
* and recursively serialized causes (if any).
*/
fun Throwable.toSerializableError(): SerializableError =
toSerializableErrorInternal(
maxDepth = 32,
visited = mutableSetOf()
)
private fun Throwable.toSerializableErrorInternal(
maxDepth: Int,
visited: MutableSet<Throwable>
): SerializableError {
val typeName = this::class.qualifiedName ?: this::class.simpleName ?: "UnknownThrowable"
if (maxDepth <= 0 || !visited.add(this)) {
// Depth limit reached or cycle detected; stop following the cause chain.
return SerializableError(
type = typeName,
message = message,
cause = null
)
}
val causeError = cause?.toSerializableErrorInternal(maxDepth - 1, visited)
return SerializableError(
type = typeName,
message = message,
cause = causeError
)
}

Copilot uses AI. Check for mistakes.
Loading