Improve server-side validation of RPC payloads.
Dynamically allocate storage space for arrays to better handle large payloads.
Patch by: bobv
Review by: mmendez
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@2260 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/client/rpc/impl/AbstractSerializationStreamReader.java b/user/src/com/google/gwt/user/client/rpc/impl/AbstractSerializationStreamReader.java
index 31f3a95..cc7118a 100644
--- a/user/src/com/google/gwt/user/client/rpc/impl/AbstractSerializationStreamReader.java
+++ b/user/src/com/google/gwt/user/client/rpc/impl/AbstractSerializationStreamReader.java
@@ -83,4 +83,8 @@
seenArray.add(o);
}
+ protected final void replaceRememberedObject(Object old, Object replacement) {
+ seenArray.set(seenArray.indexOf(old), replacement);
+ }
+
}
diff --git a/user/src/com/google/gwt/user/client/rpc/impl/RemoteServiceProxy.java b/user/src/com/google/gwt/user/client/rpc/impl/RemoteServiceProxy.java
index 79832ce..063413f 100644
--- a/user/src/com/google/gwt/user/client/rpc/impl/RemoteServiceProxy.java
+++ b/user/src/com/google/gwt/user/client/rpc/impl/RemoteServiceProxy.java
@@ -233,6 +233,8 @@
this, methodName, invocationCount, callback, responseReader);
RequestBuilder rb = new RequestBuilder(RequestBuilder.POST,
getServiceEntryPoint());
+ rb.setHeader("Content-Type", "text/x-gwt-rpc; charset=utf-8");
+
try {
return rb.sendRequest(requestData, responseHandler);
} catch (RequestException ex) {
diff --git a/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java b/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java
index 5053f73..63179bd 100644
--- a/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java
+++ b/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2007 Google Inc.
+ * Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
@@ -32,13 +32,21 @@
public class RPCServletUtils {
private static final String ACCEPT_ENCODING = "Accept-Encoding";
+ private static final String ATTACHMENT = "attachment";
+
private static final String CHARSET_UTF8 = "UTF-8";
+ private static final String CONTENT_DISPOSITION = "Content-Disposition";
+
private static final String CONTENT_ENCODING = "Content-Encoding";
private static final String CONTENT_ENCODING_GZIP = "gzip";
- private static final String CONTENT_TYPE_TEXT_PLAIN_UTF8 = "text/plain; charset=utf-8";
+ private static final String CONTENT_TYPE_APPLICATION_JSON_UTF8 = "application/json; charset=utf-8";
+
+ private static final String EXPECTED_CHARSET = "charset=utf-8";
+
+ private static final String EXPECTED_CONTENT_TYPE = "text/x-gwt-rpc";
private static final String GENERIC_FAILURE_MSG = "The call failed on the server; see server log for details";
@@ -90,8 +98,8 @@
* @throws IOException if the requests input stream cannot be accessed, read
* from or closed
* @throws ServletException if the content length of the request is not
- * specified of if the request's content type is not 'text/plain' or
- * 'charset=utf-8'
+ * specified of if the request's content type is not
+ * 'text/x-gwt-rpc' and 'charset=utf-8'
*/
public static String readContentAsUtf8(HttpServletRequest request)
throws IOException, ServletException {
@@ -106,21 +114,17 @@
// Content-Type must be specified.
if (contentType != null) {
contentType = contentType.toLowerCase();
- // The type must be plain text.
- if (contentType.startsWith("text/plain")) {
- // And it must be UTF-8 encoded (or unspecified, in which case we assume
- // that it's either UTF-8 or ASCII).
- if (contentType.indexOf("charset=") == -1) {
- contentTypeIsOkay = true;
- } else if (contentType.indexOf("charset=utf-8") != -1) {
+ // The type must be be distinct
+ if (contentType.startsWith(EXPECTED_CONTENT_TYPE)) {
+ if (contentType.indexOf(EXPECTED_CHARSET) != -1) {
contentTypeIsOkay = true;
}
}
}
if (!contentTypeIsOkay) {
- throw new ServletException(
- "Content-Type must be 'text/plain' with 'charset=utf-8' (or unspecified charset)");
+ throw new ServletException("Content-Type must be '"
+ + EXPECTED_CONTENT_TYPE + "' with '" + EXPECTED_CHARSET + "'.");
}
InputStream in = request.getInputStream();
@@ -215,8 +219,9 @@
// Send the reply.
//
response.setContentLength(responseBytes.length);
- response.setContentType(CONTENT_TYPE_TEXT_PLAIN_UTF8);
+ response.setContentType(CONTENT_TYPE_APPLICATION_JSON_UTF8);
response.setStatus(HttpServletResponse.SC_OK);
+ response.setHeader(CONTENT_DISPOSITION, ATTACHMENT);
response.getOutputStream().write(responseBytes);
}
diff --git a/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamReader.java b/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamReader.java
index 341ec10..4d568fc 100644
--- a/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamReader.java
+++ b/user/src/com/google/gwt/user/server/rpc/impl/ServerSerializationStreamReader.java
@@ -30,6 +30,7 @@
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.IdentityHashMap;
+import java.util.LinkedList;
import java.util.Map;
/**
@@ -40,6 +41,37 @@
AbstractSerializationStreamReader {
/**
+ * Used to accumulate elements while deserializing array types. The generic
+ * type of the BoundedList will vary from the component type of the array it
+ * is intended to create when the array is of a primitive type.
+ *
+ * @param <T> The type of object used to hold the data in the buffer
+ */
+ private static class BoundedList<T> extends LinkedList<T> {
+ private final Class<?> componentType;
+ private final int expectedSize;
+
+ public BoundedList(Class<?> componentType, int expectedSize) {
+ this.componentType = componentType;
+ this.expectedSize = expectedSize;
+ }
+
+ @Override
+ public boolean add(T o) {
+ assert size() < getExpectedSize();
+ return super.add(o);
+ }
+
+ public Class<?> getComponentType() {
+ return componentType;
+ }
+
+ public int getExpectedSize() {
+ return expectedSize;
+ }
+ }
+
+ /**
* Enumeration used to provided typed instance readers.
*/
private enum ValueReader {
@@ -115,99 +147,159 @@
private enum VectorReader {
BOOLEAN_VECTOR {
@Override
- void read(ServerSerializationStreamReader stream, Object instance) {
- boolean[] vector = (boolean[]) instance;
- for (int i = 0, n = vector.length; i < n; ++i) {
- vector[i] = stream.readBoolean();
- }
+ protected Object readSingleValue(ServerSerializationStreamReader stream)
+ throws SerializationException {
+ return stream.readBoolean();
+ }
+
+ @Override
+ protected void setSingleValue(Object array, int index, Object value) {
+ Array.setBoolean(array, index, (Boolean) value);
}
},
BYTE_VECTOR {
@Override
- void read(ServerSerializationStreamReader stream, Object instance) {
- byte[] vector = (byte[]) instance;
- for (int i = 0, n = vector.length; i < n; ++i) {
- vector[i] = stream.readByte();
- }
+ protected Object readSingleValue(ServerSerializationStreamReader stream)
+ throws SerializationException {
+ return stream.readByte();
+ }
+
+ @Override
+ protected void setSingleValue(Object array, int index, Object value) {
+ Array.setByte(array, index, (Byte) value);
}
},
CHAR_VECTOR {
@Override
- void read(ServerSerializationStreamReader stream, Object instance) {
- char[] vector = (char[]) instance;
- for (int i = 0, n = vector.length; i < n; ++i) {
- vector[i] = stream.readChar();
- }
+ protected Object readSingleValue(ServerSerializationStreamReader stream)
+ throws SerializationException {
+ return stream.readChar();
+ }
+
+ @Override
+ protected void setSingleValue(Object array, int index, Object value) {
+ Array.setChar(array, index, (Character) value);
}
},
DOUBLE_VECTOR {
@Override
- void read(ServerSerializationStreamReader stream, Object instance) {
- double[] vector = (double[]) instance;
- for (int i = 0, n = vector.length; i < n; ++i) {
- vector[i] = stream.readDouble();
- }
+ protected Object readSingleValue(ServerSerializationStreamReader stream)
+ throws SerializationException {
+ return stream.readDouble();
+ }
+
+ @Override
+ protected void setSingleValue(Object array, int index, Object value) {
+ Array.setDouble(array, index, (Double) value);
}
},
FLOAT_VECTOR {
@Override
- void read(ServerSerializationStreamReader stream, Object instance) {
- float[] vector = (float[]) instance;
- for (int i = 0, n = vector.length; i < n; ++i) {
- vector[i] = stream.readFloat();
- }
+ protected Object readSingleValue(ServerSerializationStreamReader stream)
+ throws SerializationException {
+ return stream.readFloat();
+ }
+
+ @Override
+ protected void setSingleValue(Object array, int index, Object value) {
+ Array.setFloat(array, index, (Float) value);
}
},
-
INT_VECTOR {
@Override
- void read(ServerSerializationStreamReader stream, Object instance) {
- int[] vector = (int[]) instance;
- for (int i = 0, n = vector.length; i < n; ++i) {
- vector[i] = stream.readInt();
- }
+ protected Object readSingleValue(ServerSerializationStreamReader stream)
+ throws SerializationException {
+ return stream.readInt();
+ }
+
+ @Override
+ protected void setSingleValue(Object array, int index, Object value) {
+ Array.setInt(array, index, (Integer) value);
}
},
LONG_VECTOR {
@Override
- void read(ServerSerializationStreamReader stream, Object instance) {
- long[] vector = (long[]) instance;
- for (int i = 0, n = vector.length; i < n; ++i) {
- vector[i] = stream.readLong();
- }
+ protected Object readSingleValue(ServerSerializationStreamReader stream)
+ throws SerializationException {
+ return stream.readLong();
+ }
+
+ @Override
+ protected void setSingleValue(Object array, int index, Object value) {
+ Array.setLong(array, index, (Long) value);
}
},
OBJECT_VECTOR {
@Override
- void read(ServerSerializationStreamReader stream, Object instance)
+ protected Object readSingleValue(ServerSerializationStreamReader stream)
throws SerializationException {
- Object[] vector = (Object[]) instance;
- for (int i = 0, n = vector.length; i < n; ++i) {
- vector[i] = stream.readObject();
- }
+ return stream.readObject();
+ }
+
+ @Override
+ protected void setSingleValue(Object array, int index, Object value) {
+ Array.set(array, index, value);
}
},
SHORT_VECTOR {
@Override
- void read(ServerSerializationStreamReader stream, Object instance) {
- short[] vector = (short[]) instance;
- for (int i = 0, n = vector.length; i < n; ++i) {
- vector[i] = stream.readShort();
- }
+ protected Object readSingleValue(ServerSerializationStreamReader stream)
+ throws SerializationException {
+ return stream.readShort();
+ }
+
+ @Override
+ protected void setSingleValue(Object array, int index, Object value) {
+ Array.setShort(array, index, (Short) value);
}
},
STRING_VECTOR {
@Override
- void read(ServerSerializationStreamReader stream, Object instance) {
- String[] vector = (String[]) instance;
- for (int i = 0, n = vector.length; i < n; ++i) {
- vector[i] = stream.readString();
- }
+ protected Object readSingleValue(ServerSerializationStreamReader stream)
+ throws SerializationException {
+ return stream.readString();
+ }
+
+ @Override
+ protected void setSingleValue(Object array, int index, Object value) {
+ Array.set(array, index, value);
}
};
- abstract void read(ServerSerializationStreamReader stream, Object instance)
- throws SerializationException;
+ protected abstract Object readSingleValue(
+ ServerSerializationStreamReader stream) throws SerializationException;
+
+ protected abstract void setSingleValue(Object array, int index, Object value);
+
+ /**
+ * Convert a BoundedList to an array of the correct type. This
+ * implementation consumes the BoundedList.
+ */
+ protected Object toArray(Class<?> componentType, BoundedList<Object> buffer)
+ throws SerializationException {
+ if (buffer.getExpectedSize() != buffer.size()) {
+ throw new SerializationException(
+ "Inconsistent number of elements received. Received "
+ + buffer.size() + " but expecting " + buffer.getExpectedSize());
+ }
+
+ Object arr = Array.newInstance(componentType, buffer.size());
+
+ for (int i = 0, n = buffer.size(); i < n; i++) {
+ setSingleValue(arr, i, buffer.removeFirst());
+ }
+
+ return arr;
+ }
+
+ Object read(ServerSerializationStreamReader stream,
+ BoundedList<Object> instance) throws SerializationException {
+ for (int i = 0, n = instance.getExpectedSize(); i < n; ++i) {
+ instance.add(readSingleValue(stream));
+ }
+
+ return toArray(instance.getComponentType(), instance);
+ }
}
/**
@@ -222,15 +314,15 @@
private final ClassLoader classLoader;
+ private SerializationPolicy serializationPolicy = RPC.getDefaultSerializationPolicy();
+
+ private final SerializationPolicyProvider serializationPolicyProvider;
+
private String[] stringTable;
private final ArrayList<String> tokenList = new ArrayList<String>();
- private SerializationPolicy serializationPolicy = RPC.getDefaultSerializationPolicy();
-
private int tokenListIndex;
-
- private final SerializationPolicyProvider serializationPolicyProvider;
{
CLASS_TO_VECTOR_READER.put(boolean[].class, VectorReader.BOOLEAN_VECTOR);
CLASS_TO_VECTOR_READER.put(byte[].class, VectorReader.BYTE_VECTOR);
@@ -370,7 +462,15 @@
rememberDecodedObject(instance);
- deserializeImpl(customSerializer, instanceClass, instance);
+ Object replacement = deserializeImpl(customSerializer, instanceClass,
+ instance);
+
+ // It's possible that deserializing an object requires the original proxy
+ // object to be replaced.
+ if (instance != replacement) {
+ replaceRememberedObject(instance, replacement);
+ instance = replacement;
+ }
return instance;
@@ -413,15 +513,17 @@
* @param instance
* @throws SerializationException
*/
- private void deserializeArray(Class<?> instanceClass, Object instance)
+ @SuppressWarnings("unchecked")
+ private Object deserializeArray(Class<?> instanceClass, Object instance)
throws SerializationException {
assert (instanceClass.isArray());
+ BoundedList<Object> buffer = (BoundedList<Object>) instance;
VectorReader instanceReader = CLASS_TO_VECTOR_READER.get(instanceClass);
if (instanceReader != null) {
- instanceReader.read(this, instance);
+ return instanceReader.read(this, buffer);
} else {
- VectorReader.OBJECT_VECTOR.read(this, instance);
+ return VectorReader.OBJECT_VECTOR.read(this, buffer);
}
}
@@ -453,7 +555,7 @@
}
}
- private void deserializeImpl(Class<?> customSerializer,
+ private Object deserializeImpl(Class<?> customSerializer,
Class<?> instanceClass, Object instance) throws NoSuchMethodException,
IllegalArgumentException, IllegalAccessException,
InvocationTargetException, SerializationException, ClassNotFoundException {
@@ -462,20 +564,30 @@
deserializeWithCustomFieldDeserializer(customSerializer, instanceClass,
instance);
} else if (instanceClass.isArray()) {
- deserializeArray(instanceClass, instance);
+ instance = deserializeArray(instanceClass, instance);
} else if (instanceClass.isEnum()) {
// Enums are deserialized when they are instantiated
} else {
deserializeClass(instanceClass, instance);
}
+
+ return instance;
}
- private void deserializeStringTable() {
+ private void deserializeStringTable() throws SerializationException {
int typeNameCount = readInt();
- stringTable = new String[typeNameCount];
+ BoundedList<String> buffer = new BoundedList<String>(String.class,
+ typeNameCount);
for (int typeNameIndex = 0; typeNameIndex < typeNameCount; ++typeNameIndex) {
- stringTable[typeNameIndex] = extract();
+ buffer.add(extract());
}
+
+ if (buffer.size() != buffer.getExpectedSize()) {
+ throw new SerializationException("Expected " + buffer.getExpectedSize()
+ + " string table elements; received " + buffer.size());
+ }
+
+ stringTable = buffer.toArray(new String[buffer.getExpectedSize()]);
}
private void deserializeWithCustomFieldDeserializer(
@@ -509,10 +621,10 @@
if (instanceClass.isArray()) {
int length = readInt();
- Class<?> componentType = instanceClass.getComponentType();
- return Array.newInstance(componentType, length);
+ // We don't pre-allocate the array; this prevents an allocation attack
+ return new BoundedList<Object>(instanceClass.getComponentType(), length);
} else if (instanceClass.isEnum()) {
- Enum[] enumConstants = (Enum[]) instanceClass.getEnumConstants();
+ Enum<?>[] enumConstants = (Enum[]) instanceClass.getEnumConstants();
int ordinal = readInt();
assert (ordinal >= 0 && ordinal < enumConstants.length);
return enumConstants[ordinal];
diff --git a/user/test/com/google/gwt/user/client/rpc/ObjectGraphTest.java b/user/test/com/google/gwt/user/client/rpc/ObjectGraphTest.java
index 3bf3757..1819e85 100644
--- a/user/test/com/google/gwt/user/client/rpc/ObjectGraphTest.java
+++ b/user/test/com/google/gwt/user/client/rpc/ObjectGraphTest.java
@@ -19,6 +19,7 @@
import com.google.gwt.junit.client.GWTTestCase;
import com.google.gwt.user.client.rpc.TestSetFactory.SerializableDoublyLinkedNode;
import com.google.gwt.user.client.rpc.TestSetFactory.SerializablePrivateNoArg;
+import com.google.gwt.user.client.rpc.TestSetFactory.SerializableWithTwoArrays;
/**
* TODO: document me.
@@ -84,6 +85,24 @@
});
}
+ public void testDoublyReferencedArray() {
+ delayTestFinish(TEST_DELAY);
+
+ ObjectGraphTestServiceAsync service = getServiceAsync();
+ final SerializableWithTwoArrays node = TestSetFactory.createDoublyReferencedArray();
+ service.echo_SerializableWithTwoArrays(node, new AsyncCallback() {
+ public void onFailure(Throwable caught) {
+ TestSetValidator.rethrowException(caught);
+ }
+
+ public void onSuccess(Object result) {
+ assertNotNull(result);
+ assertTrue(TestSetValidator.isValid((SerializableWithTwoArrays) result));
+ finishTest();
+ }
+ });
+ }
+
public void testPrivateNoArg() {
delayTestFinish(TEST_DELAY);
diff --git a/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestService.java b/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestService.java
index abaa9f2..f806677 100644
--- a/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestService.java
+++ b/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestService.java
@@ -17,6 +17,7 @@
import com.google.gwt.user.client.rpc.TestSetFactory.SerializableDoublyLinkedNode;
import com.google.gwt.user.client.rpc.TestSetFactory.SerializablePrivateNoArg;
+import com.google.gwt.user.client.rpc.TestSetFactory.SerializableWithTwoArrays;
/**
* TODO: document me.
@@ -34,6 +35,8 @@
SerializablePrivateNoArg echo_PrivateNoArg(SerializablePrivateNoArg node);
+ SerializableWithTwoArrays echo_SerializableWithTwoArrays(SerializableWithTwoArrays node);
+
SerializableDoublyLinkedNode echo_TrivialCyclicGraph(
SerializableDoublyLinkedNode node);
}
diff --git a/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestServiceAsync.java b/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestServiceAsync.java
index 68ff8be..4b4d42b 100644
--- a/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestServiceAsync.java
+++ b/user/test/com/google/gwt/user/client/rpc/ObjectGraphTestServiceAsync.java
@@ -17,6 +17,7 @@
import com.google.gwt.user.client.rpc.TestSetFactory.SerializableDoublyLinkedNode;
import com.google.gwt.user.client.rpc.TestSetFactory.SerializablePrivateNoArg;
+import com.google.gwt.user.client.rpc.TestSetFactory.SerializableWithTwoArrays;
/**
* TODO: document me.
@@ -33,6 +34,8 @@
void echo_PrivateNoArg(SerializablePrivateNoArg node, AsyncCallback callback);
+ void echo_SerializableWithTwoArrays(SerializableWithTwoArrays node, AsyncCallback callback);
+
void echo_TrivialCyclicGraph(SerializableDoublyLinkedNode node,
AsyncCallback callback);
}
diff --git a/user/test/com/google/gwt/user/client/rpc/TestSetFactory.java b/user/test/com/google/gwt/user/client/rpc/TestSetFactory.java
index 693bfdc..49ca013 100644
--- a/user/test/com/google/gwt/user/client/rpc/TestSetFactory.java
+++ b/user/test/com/google/gwt/user/client/rpc/TestSetFactory.java
@@ -196,6 +196,11 @@
public static class SerializableVector extends Vector implements
IsSerializable {
}
+
+ public static class SerializableWithTwoArrays implements IsSerializable {
+ String[] one;
+ String[] two;
+ }
/**
* TODO: document me.
@@ -393,6 +398,12 @@
return n1;
}
+ static SerializableWithTwoArrays createDoublyReferencedArray() {
+ SerializableWithTwoArrays o = new SerializableWithTwoArrays();
+ o.two = o.one = createStringArray();
+ return o;
+ }
+
static SerializableClass createSerializableClass() {
SerializableClass cls = new SerializableClass();
IsSerializable[] elements = new IsSerializable[] {
diff --git a/user/test/com/google/gwt/user/client/rpc/TestSetValidator.java b/user/test/com/google/gwt/user/client/rpc/TestSetValidator.java
index 010ef97..188ba01 100644
--- a/user/test/com/google/gwt/user/client/rpc/TestSetValidator.java
+++ b/user/test/com/google/gwt/user/client/rpc/TestSetValidator.java
@@ -18,6 +18,7 @@
import com.google.gwt.user.client.rpc.TestSetFactory.SerializableClass;
import com.google.gwt.user.client.rpc.TestSetFactory.SerializableDoublyLinkedNode;
import com.google.gwt.user.client.rpc.TestSetFactory.SerializablePrivateNoArg;
+import com.google.gwt.user.client.rpc.TestSetFactory.SerializableWithTwoArrays;
import java.util.ArrayList;
import java.util.HashMap;
@@ -304,6 +305,13 @@
return actual.getValue() == 1;
}
+
+ /**
+ * We want to assert that the two fields have object identity.
+ */
+ public static boolean isValid(SerializableWithTwoArrays node) {
+ return node.one == node.two;
+ }
public static boolean isValid(Vector expected, Vector actual) {
if (actual == null) {
diff --git a/user/test/com/google/gwt/user/server/rpc/ObjectGraphTestServiceImpl.java b/user/test/com/google/gwt/user/server/rpc/ObjectGraphTestServiceImpl.java
index 5bcab6d..511fe0a 100644
--- a/user/test/com/google/gwt/user/server/rpc/ObjectGraphTestServiceImpl.java
+++ b/user/test/com/google/gwt/user/server/rpc/ObjectGraphTestServiceImpl.java
@@ -19,6 +19,7 @@
import com.google.gwt.user.client.rpc.TestSetValidator;
import com.google.gwt.user.client.rpc.TestSetFactory.SerializableDoublyLinkedNode;
import com.google.gwt.user.client.rpc.TestSetFactory.SerializablePrivateNoArg;
+import com.google.gwt.user.client.rpc.TestSetFactory.SerializableWithTwoArrays;
/**
* TODO: document me.
@@ -62,6 +63,15 @@
return node;
}
+ public SerializableWithTwoArrays echo_SerializableWithTwoArrays(
+ SerializableWithTwoArrays node) {
+ if (!TestSetValidator.isValid(node)) {
+ throw new RuntimeException();
+ }
+
+ return node;
+ }
+
public SerializableDoublyLinkedNode echo_ComplexCyclicGraph(
SerializableDoublyLinkedNode node1, SerializableDoublyLinkedNode node2) {
if (node1 != node2) {