Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[java] Increased the max depth of new session payload #12205

Merged
merged 2 commits into from
Jun 15, 2023
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
8 changes: 6 additions & 2 deletions java/src/org/openqa/selenium/json/Json.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ public class Json {
private final JsonTypeCoercer fromJson = new JsonTypeCoercer();

public String toJson(Object toConvert) {
return toJson(toConvert, JsonOutput.MAX_DEPTH);
}

public String toJson(Object toConvert, int maxDepth) {
try (Writer writer = new StringWriter();
JsonOutput jsonOutput = newOutput(writer)) {
jsonOutput.write(toConvert);
JsonOutput jsonOutput = newOutput(writer)) {
jsonOutput.write(toConvert, maxDepth);
return writer.toString();
} catch (IOException e) {
throw new JsonException(e);
Expand Down
94 changes: 49 additions & 45 deletions java/src/org/openqa/selenium/json/JsonOutput.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@

public class JsonOutput implements Closeable {
private static final Logger LOG = Logger.getLogger(JsonOutput.class.getName());
private static final int MAX_DEPTH = 10;
static final int MAX_DEPTH = 10;

private static final Predicate<Class<?>> GSON_ELEMENT;

Expand Down Expand Up @@ -100,7 +100,7 @@ public class JsonOutput implements Closeable {
ESCAPES = Collections.unmodifiableMap(builder);
}

private final Map<Predicate<Class<?>>, SafeBiConsumer<Object, Integer>> converters;
private final Map<Predicate<Class<?>>, DepthAwareConsumer> converters;
private final Appendable appendable;
private final Consumer<String> appender;
private Deque<Node> stack;
Expand All @@ -126,32 +126,32 @@ public class JsonOutput implements Closeable {

// Order matters, since we want to handle null values first to avoid exceptions, and then then
// common kinds of inputs next.
Map<Predicate<Class<?>>, SafeBiConsumer<Object, Integer>> builder = new LinkedHashMap<>();
builder.put(Objects::isNull, (obj, depth) -> append("null"));
builder.put(CharSequence.class::isAssignableFrom, (obj, depth) -> append(asString(obj)));
builder.put(Number.class::isAssignableFrom, (obj, depth) -> append(obj.toString()));
Map<Predicate<Class<?>>, DepthAwareConsumer> builder = new LinkedHashMap<>();
builder.put(Objects::isNull, (obj, maxDepth, depthRemaining) -> append("null"));
builder.put(CharSequence.class::isAssignableFrom, (obj, maxDepth, depthRemaining) -> append(asString(obj)));
builder.put(Number.class::isAssignableFrom, (obj, maxDepth, depthRemaining) -> append(obj.toString()));
builder.put(
Boolean.class::isAssignableFrom, (obj, depth) -> append((Boolean) obj ? "true" : "false"));
Boolean.class::isAssignableFrom, (obj, maxDepth, depthRemaining) -> append((Boolean) obj ? "true" : "false"));
builder.put(
Date.class::isAssignableFrom,
(obj, depth) -> append(String.valueOf(MILLISECONDS.toSeconds(((Date) obj).getTime()))));
(obj, maxDepth, depthRemaining) -> append(String.valueOf(MILLISECONDS.toSeconds(((Date) obj).getTime()))));
builder.put(
Instant.class::isAssignableFrom,
(obj, depth) -> append(asString(DateTimeFormatter.ISO_INSTANT.format((Instant) obj))));
builder.put(Enum.class::isAssignableFrom, (obj, depth) -> append(asString(obj)));
(obj, maxDepth, depthRemaining) -> append(asString(DateTimeFormatter.ISO_INSTANT.format((Instant) obj))));
builder.put(Enum.class::isAssignableFrom, (obj, maxDepth, depthRemaining) -> append(asString(obj)));
builder.put(
File.class::isAssignableFrom, (obj, depth) -> append(((File) obj).getAbsolutePath()));
builder.put(URI.class::isAssignableFrom, (obj, depth) -> append(asString((obj).toString())));
File.class::isAssignableFrom, (obj, maxDepth, depthRemaining) -> append(((File) obj).getAbsolutePath()));
builder.put(URI.class::isAssignableFrom, (obj, maxDepth, depthRemaining) -> append(asString((obj).toString())));
builder.put(
URL.class::isAssignableFrom,
(obj, depth) -> append(asString(((URL) obj).toExternalForm())));
builder.put(UUID.class::isAssignableFrom, (obj, depth) -> append(asString(obj.toString())));
(obj, maxDepth, depthRemaining) -> append(asString(((URL) obj).toExternalForm())));
builder.put(UUID.class::isAssignableFrom, (obj, maxDepth, depthRemaining) -> append(asString(obj.toString())));
builder.put(
Level.class::isAssignableFrom,
(obj, depth) -> append(asString(LogLevelMapping.getName((Level) obj))));
(obj, maxDepth, depthRemaining) -> append(asString(LogLevelMapping.getName((Level) obj))));
builder.put(
GSON_ELEMENT,
(obj, depth) -> {
(obj, maxDepth, depthRemaining) -> {
LOG.log(
Level.WARNING,
"Attempt to convert JsonElement from GSON. This functionality is deprecated. "
Expand All @@ -162,36 +162,36 @@ public class JsonOutput implements Closeable {
// Special handling of asMap and toJson
builder.put(
cls -> getMethod(cls, "toJson") != null,
(obj, depth) -> convertUsingMethod("toJson", obj, depth));
(obj, maxDepth, depthRemaining) -> convertUsingMethod("toJson", obj, maxDepth, depthRemaining));
builder.put(
cls -> getMethod(cls, "asMap") != null,
(obj, depth) -> convertUsingMethod("asMap", obj, depth));
(obj, maxDepth, depthRemaining) -> convertUsingMethod("asMap", obj, maxDepth, depthRemaining));
builder.put(
cls -> getMethod(cls, "toMap") != null,
(obj, depth) -> convertUsingMethod("toMap", obj, depth));
(obj, maxDepth, depthRemaining) -> convertUsingMethod("toMap", obj, maxDepth, depthRemaining));

// And then the collection types
builder.put(
Collection.class::isAssignableFrom,
(obj, depth) -> {
if (depth < 1) {
(obj, maxDepth, depthRemaining) -> {
if (depthRemaining < 1) {
throw new JsonException(
"Reached the maximum depth of " + MAX_DEPTH + " while writing JSON");
"Reached the maximum depth of " + maxDepth + " while writing JSON");
}
beginArray();
((Collection<?>) obj)
.stream()
.filter(o -> (!(o instanceof Optional) || ((Optional<?>) o).isPresent()))
.forEach(o -> write(o, depth - 1));
.forEach(o -> write0(o, maxDepth, depthRemaining - 1));
endArray();
});

builder.put(
Map.class::isAssignableFrom,
(obj, depth) -> {
if (depth < 1) {
(obj, maxDepth, depthRemaining) -> {
if (depthRemaining < 1) {
throw new JsonException(
"Reached the maximum depth of " + MAX_DEPTH + " while writing JSON");
"Reached the maximum depth of " + maxDepth + " while writing JSON");
}
beginObject();
((Map<?, ?>) obj)
Expand All @@ -200,45 +200,45 @@ public class JsonOutput implements Closeable {
if (value instanceof Optional && !((Optional) value).isPresent()) {
return;
}
name(String.valueOf(key)).write(value, depth - 1);
name(String.valueOf(key)).write0(value, maxDepth, depthRemaining - 1);
});
endObject();
});
builder.put(
Class::isArray,
(obj, depth) -> {
if (depth < 1) {
(obj, maxDepth, depthRemaining) -> {
if (depthRemaining < 1) {
throw new JsonException(
"Reached the maximum depth of " + MAX_DEPTH + " while writing JSON");
"Reached the maximum depth of " + maxDepth + " while writing JSON");
}
beginArray();
Stream.of((Object[]) obj)
.filter(o -> (!(o instanceof Optional) || ((Optional<?>) o).isPresent()))
.forEach(o -> write(o, depth - 1));
.forEach(o -> write0(o, maxDepth, depthRemaining - 1));
endArray();
});

builder.put(
Optional.class::isAssignableFrom,
(obj, depth) -> {
(obj, maxDepth, depthRemaining) -> {
Optional<?> optional = (Optional<?>) obj;
if (!optional.isPresent()) {
append("null");
return;
}

write(optional.get(), depth);
write0(optional.get(), maxDepth, depthRemaining);
});

// Finally, attempt to convert as an object
builder.put(
cls -> true,
(obj, depth) -> {
if (depth < 1) {
(obj, maxDepth, depthRemaining) -> {
if (depthRemaining < 1) {
throw new JsonException(
"Reached the maximum depth of " + MAX_DEPTH + " while writing JSON");
"Reached the maximum depth of " + maxDepth + " while writing JSON");
}
mapObject(obj, depth - 1);
mapObject(obj, maxDepth, depthRemaining - 1);
});

this.converters = Collections.unmodifiableMap(builder);
Expand Down Expand Up @@ -313,13 +313,17 @@ public JsonOutput write(Object value) {
return write(value, MAX_DEPTH);
}

public JsonOutput write(Object input, int depthRemaining) {
public JsonOutput write(Object value, int maxDepth) {
return write0(value, maxDepth, maxDepth);
}

private JsonOutput write0(Object input, int maxDepth, int depthRemaining) {
converters.entrySet().stream()
.filter(entry -> entry.getKey().test(input == null ? null : input.getClass()))
.findFirst()
.map(Map.Entry::getValue)
.orElseThrow(() -> new JsonException("Unable to write " + input))
.consume(input, depthRemaining);
.consume(input, maxDepth, depthRemaining);

return this;
}
Expand Down Expand Up @@ -381,7 +385,7 @@ private Method getMethod(Class<?> clazz, String methodName) {
}
}

private JsonOutput convertUsingMethod(String methodName, Object toConvert, int depth) {
private JsonOutput convertUsingMethod(String methodName, Object toConvert, int maxDepth, int depthRemaining) {
try {
Method method = getMethod(toConvert.getClass(), methodName);
if (method == null) {
Expand All @@ -390,13 +394,13 @@ private JsonOutput convertUsingMethod(String methodName, Object toConvert, int d
}
Object value = method.invoke(toConvert);

return write(value, depth);
return write0(value, maxDepth, depthRemaining);
} catch (ReflectiveOperationException e) {
throw new JsonException(e);
}
}

private void mapObject(Object toConvert, int depthRemaining) {
private void mapObject(Object toConvert, int maxDepth, int depthRemaining) {
if (toConvert instanceof Class) {
write(((Class<?>) toConvert).getName());
return;
Expand All @@ -420,7 +424,7 @@ private void mapObject(Object toConvert, int depthRemaining) {
Object value = pd.getReadMethod().apply(toConvert);
if (!Optional.empty().equals(value)) {
name(pd.getName());
write(value, depthRemaining - 1);
write0(value, maxDepth, depthRemaining - 1);
}
}
endObject();
Expand Down Expand Up @@ -479,7 +483,7 @@ public void write(String text) {
}

@FunctionalInterface
private interface SafeBiConsumer<T, U> {
void consume(T t, U u);
private interface DepthAwareConsumer {
void consume(Object object, int maxDepth, int depthRemaining);
}
}
2 changes: 1 addition & 1 deletion java/src/org/openqa/selenium/remote/NewSessionPayload.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public static NewSessionPayload create(Map<String, ?> source) {
Require.precondition(
source.containsKey("capabilities"), "New session payload must contain capabilities");

String json = new Json().toJson(Require.nonNull("Payload", source));
String json = new Json().toJson(Require.nonNull("Payload", source), 100);
return new NewSessionPayload(new StringReader(json));
}

Expand Down
31 changes: 30 additions & 1 deletion java/test/org/openqa/selenium/json/JsonOutputTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,7 @@ public String getCheese() {
}

@Test
void shouldRespectMaxDepth() {
void shouldRespectDefaultMaxDepth() {
StringBuilder builder = new StringBuilder();

JsonOutput jsonOutput = new Json().newOutput(builder);
Expand All @@ -738,6 +738,35 @@ void shouldRespectMaxDepth() {
assertThatExceptionOfType(JsonException.class).isThrownBy(() -> jsonOutput.write(finalValue));
}

@Test
void shouldRespectCustomHigherMaxDepth() {
shouldRespectMaxDepth(16);
}

@Test
void shouldRespectCustomLowerMaxDepth() {
shouldRespectMaxDepth(8);
}

void shouldRespectMaxDepth(int maxDepth) {
StringBuilder builder = new StringBuilder();

JsonOutput jsonOutput = new Json().newOutput(builder);
jsonOutput.beginArray();

Object value = emptyList();

for (int i = 0; i < maxDepth; i++) {
jsonOutput.write(value, maxDepth);

value = singletonList(value);
}

Object finalValue = value;

assertThatExceptionOfType(JsonException.class).isThrownBy(() -> jsonOutput.write(finalValue, maxDepth));
}

private String convert(Object toConvert) {
try (Writer writer = new StringWriter();
JsonOutput jsonOutput = new Json().newOutput(writer)) {
Expand Down