Skip to content

Commit 3346f34

Browse files
committed
Fix: Require serializers to only produce valid target types
1 parent 0cf9d8e commit 3346f34

File tree

15 files changed

+608
-65
lines changed

15 files changed

+608
-65
lines changed

configlib-core/src/main/java/de/exlll/configlib/Reflect.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
import static de.exlll.configlib.Validator.requireRecord;
1313

1414
final class Reflect {
15+
private static final Set<Class<?>> SIMPLE_TARGET_TYPES = Set.of(
16+
Boolean.class,
17+
Long.class,
18+
Double.class,
19+
String.class
20+
);
1521
private static final Map<Class<?>, Object> DEFAULT_VALUES = initDefaultValues();
1622

1723
private Reflect() {}
@@ -251,4 +257,8 @@ static Object invoke(Method method, Object object, Object... arguments) {
251257
throw new RuntimeException(e);
252258
}
253259
}
260+
261+
static boolean isSimpleTargetType(Class<?> cls) {
262+
return SIMPLE_TARGET_TYPES.contains(cls);
263+
}
254264
}

configlib-core/src/main/java/de/exlll/configlib/Serializer.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,25 @@
55
* {@code T2} and vice versa.
66
* <p>
77
* Which types {@code T2} are serializable depends on the underlying storage system. Currently,
8-
* all storage systems support the following types:
8+
* all storage systems support the following target types:
99
* <ul>
1010
* <li>{@code Boolean}</li>
1111
* <li>{@code Long}</li>
1212
* <li>{@code Double}</li>
1313
* <li>{@code String}</li>
1414
* <li>(Nested) {@code List}s of the other types</li>
15-
* <li>(Nested) {@code Set}s of the other types</li>
1615
* <li>(Nested) {@code Map}s of the other types</li>
1716
* </ul>
1817
* <p>
19-
* That means that if you want to support all currently available configuration store formats,
20-
* your {@code Serializer} implementation should convert an object of type {@code T1} into one
21-
* of the seven types listed above.
18+
* For all custom serializers, {@code T2} must be one of the six types listed above.
2219
*
2320
* @param <T1> the type of the objects that should be serialized
2421
* @param <T2> the serializable type
2522
*/
2623
public interface Serializer<T1, T2> {
2724
/**
2825
* Serializes an element of type {@code T1} into an element of type {@code T2}.
26+
* Type {@code T2} must be a valid target type.
2927
*
3028
* @param element the element of type {@code T1} that is serialized
3129
* @return the serialized element of type {@code T2}

configlib-core/src/main/java/de/exlll/configlib/TypeSerializer.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import java.util.stream.Collectors;
1717

1818
import static de.exlll.configlib.Validator.requireNonNull;
19+
import static de.exlll.configlib.Validator.requireTargetType;
1920

2021
sealed abstract class TypeSerializer<T, E extends ConfigurationElement<?>>
2122
implements Serializer<T, Map<?, ?>>
@@ -76,12 +77,16 @@ protected TypeSerializer(Class<T> type, ConfigurationProperties properties) {
7677
}
7778

7879
protected final Object serializeElement(E element, Object value) {
80+
if (value == null) return null;
81+
7982
// This cast can lead to a ClassCastException if an element of type X is
8083
// serialized by a custom serializer that expects a different type Y.
8184
@SuppressWarnings("unchecked")
8285
final var serializer = (Serializer<Object, Object>) serializers.get(element.name());
8386
try {
84-
return (value != null) ? serializer.serialize(value) : null;
87+
final Object serialized = serializer.serialize(value);
88+
validateTargetType(element, value, serialized);
89+
return serialized;
8590
} catch (ClassCastException e) {
8691
String msg = ("Serialization of value '%s' for element '%s' of type '%s' failed.\n" +
8792
"The type of the object to be serialized does not match the type " +
@@ -96,6 +101,25 @@ protected final Object serializeElement(E element, Object value) {
96101
}
97102
}
98103

104+
private static void validateTargetType(
105+
ConfigurationElement<?> element,
106+
Object value,
107+
Object serialized
108+
) {
109+
try {
110+
requireTargetType(serialized);
111+
} catch (ConfigurationException e) {
112+
String msg = ("Serialization of value '%s' for element '%s' of type '%s' failed. " +
113+
"The serializer produced an invalid target type.")
114+
.formatted(
115+
value,
116+
element.element(),
117+
element.declaringType()
118+
);
119+
throw new ConfigurationException(msg, e);
120+
}
121+
}
122+
99123
protected final Object deserialize(E element, Object value) {
100124
// This unchecked cast leads to an exception if the type of the object which
101125
// is deserialized is not a subtype of the type the deserializer expects.

configlib-core/src/main/java/de/exlll/configlib/Validator.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package de.exlll.configlib;
22

3+
import java.util.List;
4+
import java.util.Map;
35
import java.util.Objects;
46

57
final class Validator {
@@ -52,4 +54,32 @@ static void requirePrimitiveOrWrapperNumberType(Class<?> cls) {
5254
throw new IllegalArgumentException(msg);
5355
}
5456
}
57+
58+
static void requireTargetType(Object object) {
59+
if (object == null) return;
60+
61+
final Class<?> cls = object.getClass();
62+
if (cls == Boolean.class ||
63+
cls == Long.class ||
64+
cls == Double.class ||
65+
cls == String.class) {
66+
return;
67+
}
68+
69+
if (object instanceof List<?> list) {
70+
list.forEach(Validator::requireTargetType);
71+
return;
72+
}
73+
74+
if (object instanceof Map<?, ?> map) {
75+
map.keySet().forEach(Validator::requireTargetType);
76+
map.values().forEach(Validator::requireTargetType);
77+
return;
78+
}
79+
80+
final String msg =
81+
"Object '" + object + "' does not have a valid target type. " +
82+
"Its type is: " + object.getClass();
83+
throw new ConfigurationException(msg);
84+
}
5585
}

configlib-core/src/test/java/de/exlll/configlib/ReflectTest.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@
77
import java.lang.reflect.Constructor;
88
import java.lang.reflect.Field;
99
import java.lang.reflect.Method;
10-
import java.util.ArrayList;
11-
import java.util.HashMap;
12-
import java.util.HashSet;
10+
import java.util.*;
1311

1412
import static de.exlll.configlib.TestUtils.*;
1513
import static org.hamcrest.MatcherAssert.assertThat;
@@ -447,4 +445,22 @@ void invokeMethod() throws Exception {
447445
Object object = Reflect.invoke(method, new ReflectTest());
448446
assertThat(object, is("AB"));
449447
}
448+
449+
@Test
450+
void isSimpleTargetType() {
451+
enum E {E;}
452+
record R(Long l) {}
453+
454+
assertThat(Reflect.isSimpleTargetType(Boolean.class), is(true));
455+
assertThat(Reflect.isSimpleTargetType(Long.class), is(true));
456+
assertThat(Reflect.isSimpleTargetType(Double.class), is(true));
457+
assertThat(Reflect.isSimpleTargetType(String.class), is(true));
458+
459+
assertThat(Reflect.isSimpleTargetType(Map.class), is(false));
460+
assertThat(Reflect.isSimpleTargetType(Set.class), is(false));
461+
assertThat(Reflect.isSimpleTargetType(List.class), is(false));
462+
assertThat(Reflect.isSimpleTargetType(E.class), is(false));
463+
assertThat(Reflect.isSimpleTargetType(R.class), is(false));
464+
assertThat(Reflect.isSimpleTargetType(Object.class), is(false));
465+
}
450466
}

configlib-core/src/test/java/de/exlll/configlib/TypeSerializerTest.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@
2121
import static de.exlll.configlib.TestUtils.assertThrowsConfigurationException;
2222
import static org.hamcrest.MatcherAssert.assertThat;
2323
import static org.hamcrest.Matchers.*;
24-
import static org.junit.jupiter.api.Assertions.assertFalse;
25-
import static org.junit.jupiter.api.Assertions.assertTrue;
24+
import static org.junit.jupiter.api.Assertions.*;
2625

2726
class TypeSerializerTest {
2827
private static <T> TypeSerializer<T, ?> newTypeSerializer(
@@ -581,6 +580,27 @@ record S(@SerializeWith(serializer = DoubleIntSerializer.class) String s) {}
581580
"the custom serializer of type " +
582581
"'class de.exlll.configlib.TestUtils$DoubleIntSerializer' expects."
583582
);
583+
}
584584

585+
@Test
586+
void serializingObjectThatProducesInvalidTargetTypeFails() {
587+
record S(@SerializeWith(serializer = DoubleIntSerializer.class) Integer i) {}
588+
final var serializer = newTypeSerializer(S.class);
589+
590+
ConfigurationException ex1 = assertThrows(
591+
ConfigurationException.class,
592+
() -> serializer.serialize(new S(10))
593+
);
594+
assertThat(ex1.getMessage(), is(
595+
"Serialization of value '10' for element 'java.lang.Integer i' of type " +
596+
"'class de.exlll.configlib.TypeSerializerTest$2S' failed. " +
597+
"The serializer produced an invalid target type."
598+
));
599+
600+
ConfigurationException ex2 = (ConfigurationException) ex1.getCause();
601+
assertThat(ex2.getMessage(), is(
602+
"Object '20' does not have a valid target type. " +
603+
"Its type is: class java.lang.Integer"
604+
));
585605
}
586606
}

0 commit comments

Comments
 (0)