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

Remove serialization workarounds for ie 6/7 and rhino (#9578) #9876

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public abstract class AbstractSerializationStream {
/**
* The current RPC protocol version.
*/
public static final int SERIALIZATION_STREAM_VERSION = 7;
public static final int SERIALIZATION_STREAM_VERSION = 8;

/**
* The oldest supported RPC protocol version.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,78 +42,6 @@
public final class ServerSerializationStreamWriter extends
AbstractSerializationStreamWriter {

/**
* Builds a string that evaluates into an array containing the given elements.
* This class exists to work around a bug in IE6/7 that limits the size of
* array literals.
*/
public static class LengthConstrainedArray {
public static final int MAXIMUM_ARRAY_LENGTH = 1 << 15;
private static final String POSTLUDE = "])";
private static final String PRELUDE = "].concat([";

private final StringBuffer buffer;
private int count = 0;
private boolean needsComma = false;
private int total = 0;
private boolean javascript = false;

public LengthConstrainedArray() {
buffer = new StringBuffer();
}

public LengthConstrainedArray(int capacityGuess) {
buffer = new StringBuffer(capacityGuess);
}

public void addToken(CharSequence token) {
total++;
if (count++ == MAXIMUM_ARRAY_LENGTH) {
if (total == MAXIMUM_ARRAY_LENGTH + 1) {
buffer.append(PRELUDE);
javascript = true;
} else {
buffer.append("],[");
}
count = 0;
needsComma = false;
}

if (needsComma) {
buffer.append(",");
} else {
needsComma = true;
}

buffer.append(token);
}

public void addEscapedToken(String token) {
addToken(escapeString(token, true, this));
}

public void addToken(int i) {
addToken(String.valueOf(i));
}

public boolean isJavaScript() {
return javascript;
}

public void setJavaScript(boolean javascript) {
this.javascript = javascript;
}

@Override
public String toString() {
if (total > MAXIMUM_ARRAY_LENGTH) {
return "[" + buffer.toString() + POSTLUDE;
} else {
return "[" + buffer.toString() + "]";
}
}
}

/**
* Enumeration used to provided typed instance writers.
*/
Expand Down Expand Up @@ -337,14 +265,6 @@ abstract void write(ServerSerializationStreamWriter stream, Object instance)

private static final char NON_BREAKING_HYPHEN = '\u2011';

/**
* Maximum length of a string node in RPC responses, not including surrounding
* quote characters (2 ^ 16 - 1) = 65535.
* This exists to work around a Rhino parser bug in the hosted mode client
* that limits string node lengths to 64KB.
*/
private static final int MAX_STRING_NODE_LENGTH = 0xFFFF;

static {
/*
* NOTE: The JS VM in IE6 & IE7 do not interpret \v correctly. They convert
Expand Down Expand Up @@ -382,36 +302,15 @@ abstract void write(ServerSerializationStreamWriter stream, Object instance)
CLASS_TO_VALUE_WRITER.put(String.class, ValueWriter.STRING);
}

/**
* This method takes a string and outputs a JavaScript string literal. The
* data is surrounded with quotes, and any contained characters that need to
* be escaped are mapped onto their escape sequence.
*
* Assumptions: We are targeting a version of JavaScript that that is later
* than 1.3 that supports unicode strings.
*/
public static String escapeString(String toEscape) {
return escapeString(toEscape, false, null);
}

/**
* This method takes a string and outputs a JavaScript string literal. The
* data is surrounded with quotes, and any contained characters that need to
* be escaped are mapped onto their escape sequence.
*
* This splits strings into 64KB chunks to workaround an issue with the hosted mode client where
* the Rhino parser can't handle string nodes larger than 64KB, e.g. {@code "longstring"} is
* converted to {@code "long" + "string"}.
*
* Assumptions: We are targeting a version of JavaScript that that is later
* than 1.3 that supports unicode strings.
*/
public static String escapeStringSplitNodes(String toEscape) {
return escapeString(toEscape, true, null);
}

private static String escapeString(String toEscape, boolean splitNodes,
LengthConstrainedArray array) {
public static String escapeString(String toEscape) {
// Since escaped characters will increase the output size, allocate extra room to start.
int length = toEscape.length();
int capacityIncrement = Math.max(length, 16);
Expand All @@ -422,30 +321,14 @@ private static String escapeString(String toEscape, boolean splitNodes,
int i = 0;
while (i < length) {

// Add one segment at a time, up to maxNodeLength characters. Note this always leave room
// for at least 6 characters at the end (maximum unicode escaped character size).
int maxSegmentVectorSize = splitNodes
? (charVector.getSize() + MAX_STRING_NODE_LENGTH - 5)
: Integer.MAX_VALUE;

while (i < length && charVector.getSize() < maxSegmentVectorSize) {
while (i < length) {
Copy link
Member

Choose a reason for hiding this comment

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

This can be removed. The same loop definition is in the line above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the extra loop

char c = toEscape.charAt(i++);
if (needsUnicodeEscape(c)) {
unicodeEscape(c, charVector);
} else {
charVector.add(c);
}
}

// If there's another segment left, insert a '+' operator.
if (splitNodes && i < length) {
charVector.add(JS_QUOTE_CHAR);
charVector.add('+');
charVector.add(JS_QUOTE_CHAR);
if (array != null) {
array.setJavaScript(true);
}
}
}

charVector.add(JS_QUOTE_CHAR);
Expand Down Expand Up @@ -502,7 +385,7 @@ private static Class<?> getClassForSerialization(Object instance) {
* <li>Total Characters Escaped: 2082</li></li>
* </ul> </li>
* </ol>
*
*
* @param ch character to check
* @return <code>true</code> if the character requires the \\uXXXX unicode
* character escape
Expand Down Expand Up @@ -557,7 +440,7 @@ private static boolean needsUnicodeEscape(char ch) {
* Writes a safe escape sequence for a character. Some characters have a short
* form, such as \n for U+000D, while others are represented as \\xNN or
* \\uNNNN.
*
*
* @param ch character to unicode escape
* @param charVector char vector to receive the unicode escaped representation
*/
Expand Down Expand Up @@ -610,7 +493,7 @@ public void serializeValue(Object value, Class<?> type)
/**
* Build an array of JavaScript string literals that can be decoded by the
* client via the eval function.
*
*
* NOTE: We build the array in reverse so the client can simply use the pop
* function to remove the next item from the list.
*/
Expand All @@ -620,14 +503,14 @@ public String toString() {
// We take a guess at how big to make to buffer to avoid numerous resizes.
//
int capacityGuess = 2 * tokenListCharCount + 2 * tokenList.size();
LengthConstrainedArray stream = new LengthConstrainedArray(capacityGuess);
writePayload(stream);
writeStringTable(stream);
writeHeader(stream);
StringBuilder buffer = new StringBuilder(capacityGuess);
writePayload(buffer);
writeStringTable(buffer);
writeHeader(buffer);

return stream.toString();
return "[" + buffer.toString() + "]";
}

@Override
public void writeLong(long value) {
if (getVersion() == SERIALIZATION_STREAM_MIN_VERSION) {
Expand Down Expand Up @@ -702,7 +585,7 @@ protected void serialize(Object instance, String typeSignature)
* Serialize an instance that is an array. Will default to serializing the
* instance as an Object vector if the instance is not a vector of primitives,
* Strings or Object.
*
*
* @param instanceClass
* @param instance
* @throws SerializationException
Expand Down Expand Up @@ -735,14 +618,14 @@ private void serializeClass(Object instance, Class<?> instanceClass)
List<Field> serverFields = new ArrayList<Field>();
for (Field declField : serializableFields) {
assert (declField != null);

// Identify server-only fields
if (!clientFieldNames.contains(declField.getName())) {
serverFields.add(declField);
continue;
}
}

// Serialize the server-only fields into a byte array and encode as a String
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Expand All @@ -765,7 +648,7 @@ private void serializeClass(Object instance, Class<?> instanceClass)
throw new SerializationException(e);
}
}

// Write the client-visible field data
for (Field declField : serializableFields) {
if ((clientFieldNames != null) && !clientFieldNames.contains(declField.getName())) {
Expand Down Expand Up @@ -861,29 +744,39 @@ private void serializeWithCustomSerializer(Class<?> customSerializer,
* Notice that the field are written in reverse order that the client can just
* pop items out of the stream.
*/
private void writeHeader(LengthConstrainedArray stream) {
stream.addToken(getFlags());
if (stream.isJavaScript() && getVersion() >= SERIALIZATION_STREAM_JSON_VERSION) {
// Ensure we are not using the JSON supported version if stream is Javascript instead of JSON
stream.addToken(SERIALIZATION_STREAM_JSON_VERSION - 1);
} else {
stream.addToken(getVersion());
}
private void writeHeader(StringBuilder buffer) {
addToken(buffer, getFlags());
addToken(buffer, getVersion());
}

private void writePayload(LengthConstrainedArray stream) {
private void writePayload(StringBuilder buffer) {
ListIterator<String> tokenIterator = tokenList.listIterator(tokenList.size());
while (tokenIterator.hasPrevious()) {
stream.addToken(tokenIterator.previous());
addToken(buffer, tokenIterator.previous());
}
}

private void writeStringTable(LengthConstrainedArray stream) {
LengthConstrainedArray tableStream = new LengthConstrainedArray();
private void writeStringTable(StringBuilder buffer) {
StringBuilder tableBuffer = new StringBuilder();
for (String s : getStringTable()) {
tableStream.addEscapedToken(s);
addEscapedToken(tableBuffer, s);
}
stream.addToken(tableStream.toString());
stream.setJavaScript(stream.isJavaScript() || tableStream.isJavaScript());
addToken(buffer, "[" + tableBuffer + "]");
}

public void addToken(StringBuilder buffer, CharSequence token) {
if (buffer.length() > 0) {
buffer.append(",");
}

buffer.append(token);
}

public void addEscapedToken(StringBuilder buffer, String token) {
addToken(buffer, escapeString(token));
}

public void addToken(StringBuilder buffer, int i) {
addToken(buffer, String.valueOf(i));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ public void testEscapeString() {
}

public void testEscapeStringSplitNodes() {
String escaped = ServerSerializationStreamWriter.escapeStringSplitNodes("test");
String escaped = ServerSerializationStreamWriter.escapeString("test");
assertEquals("\"test\"", escaped);
}

public void testEscapeStringSplitNodes_unicodeEscape() {
String escaped = ServerSerializationStreamWriter.escapeStringSplitNodes(
String escaped = ServerSerializationStreamWriter.escapeString(
"测试" // Unicode characters
+ "\"" // JS quote char
+ "\\" // JS escape char
Expand Down Expand Up @@ -75,11 +75,11 @@ public void testEscapeStringSplitNodes_over64KB() {
secondNodeBuilder.append('2');
}

String escaped = ServerSerializationStreamWriter.escapeStringSplitNodes(
String escaped = ServerSerializationStreamWriter.escapeString(
firstNodeBuilder.toString() + secondNodeBuilder.toString());

assertEquals(
"\"" + firstNodeBuilder.toString() + "\"+\"" + secondNodeBuilder.toString() + "\"",
"\"" + firstNodeBuilder.toString() + secondNodeBuilder.toString() + "\"",
escaped);
}

Expand All @@ -104,12 +104,11 @@ public void testEscapeStringSplitNodes_over64KBEscaped() {
}
String secondNode = secondNodeBuilder.toString();

String escaped = ServerSerializationStreamWriter.escapeStringSplitNodes(
String escaped = ServerSerializationStreamWriter.escapeString(
firstNodeBuilder.toString() + secondNode);

assertEquals(
"\"" + firstNodeNoUnicode + "\\u2011" // first node (including escaped unicode character)
+ "\"+\""
+ secondNode + "\"", // second node
escaped);
}
Expand All @@ -122,28 +121,4 @@ public void testWritingRpcVersion8Message() {
assertEquals("[\"NaN\",\"Infinity\",\"-Infinity\",[],0,8]", writer.toString());
}

public void testVersion8Fallbacks() {
StringBuilder longString = new StringBuilder(66000);
for (int i = 0; i < 660000; i++) {
longString.append("a");
}

// Fallbacks to 7 if string gets concatenated
ServerSerializationStreamWriter writer = new ServerSerializationStreamWriter(null, 8);
writer.writeString(longString.toString());
String encoded = writer.toString();
assertEquals("7", encoded.substring(encoded.lastIndexOf(",") + 1, encoded.lastIndexOf("]")));

// Fallbacks to 7 if array size reached maximum
int maxArrayLength =
ServerSerializationStreamWriter.LengthConstrainedArray.MAXIMUM_ARRAY_LENGTH + 100;
writer = new ServerSerializationStreamWriter(null, 8);
for (int i = 0; i < maxArrayLength; i++) {
writer.writeInt(i);
}

encoded = writer.toString();
assertEquals("7", encoded.substring(encoded.lastIndexOf(",") + 1, encoded.lastIndexOf("]")));
}

}