diff --git a/src/main/java/io/socket/engineio/client/Transport.java b/src/main/java/io/socket/engineio/client/Transport.java index eda3d82..f5414c5 100644 --- a/src/main/java/io/socket/engineio/client/Transport.java +++ b/src/main/java/io/socket/engineio/client/Transport.java @@ -5,6 +5,7 @@ import io.socket.emitter.Emitter; import io.socket.engineio.parser.Packet; import io.socket.engineio.parser.Parser; import io.socket.thread.EventThread; +import io.socket.utf8.UTF8Exception; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; @@ -98,7 +99,11 @@ public abstract class Transport extends Emitter { @Override public void run() { if (Transport.this.readyState == ReadyState.OPEN) { - Transport.this.write(packets); + try { + Transport.this.write(packets); + } catch (UTF8Exception err) { + throw new RuntimeException(err); + } } else { throw new RuntimeException("Transport not open"); } @@ -129,7 +134,7 @@ public abstract class Transport extends Emitter { this.emit(EVENT_CLOSE); } - abstract protected void write(Packet[] packets); + abstract protected void write(Packet[] packets) throws UTF8Exception; abstract protected void doOpen(); diff --git a/src/main/java/io/socket/engineio/client/transports/Polling.java b/src/main/java/io/socket/engineio/client/transports/Polling.java index e010214..2227cda 100644 --- a/src/main/java/io/socket/engineio/client/transports/Polling.java +++ b/src/main/java/io/socket/engineio/client/transports/Polling.java @@ -7,6 +7,7 @@ import io.socket.engineio.parser.Parser; import io.socket.parseqs.ParseQS; import io.socket.thread.EventThread; import io.socket.emitter.Emitter; +import io.socket.utf8.UTF8Exception; import java.util.Date; import java.util.HashMap; @@ -152,7 +153,11 @@ abstract public class Polling extends Transport { @Override public void call(Object... args) { logger.fine("writing close packet"); - self.write(new Packet[] {new Packet(Packet.CLOSE)}); + try { + self.write(new Packet[]{new Packet(Packet.CLOSE)}); + } catch (UTF8Exception err) { + throw new RuntimeException(err); + } } }; @@ -167,7 +172,7 @@ abstract public class Polling extends Transport { } } - protected void write(Packet[] packets) { + protected void write(Packet[] packets) throws UTF8Exception { final Polling self = this; this.writable = false; final Runnable callbackfn = new Runnable() { diff --git a/src/main/java/io/socket/engineio/client/transports/WebSocket.java b/src/main/java/io/socket/engineio/client/transports/WebSocket.java index ee350b5..f5c21dc 100644 --- a/src/main/java/io/socket/engineio/client/transports/WebSocket.java +++ b/src/main/java/io/socket/engineio/client/transports/WebSocket.java @@ -12,6 +12,7 @@ import com.squareup.okhttp.Response; import com.squareup.okhttp.ws.WebSocket.PayloadType; import com.squareup.okhttp.ws.WebSocketCall; import com.squareup.okhttp.ws.WebSocketListener; +import io.socket.utf8.UTF8Exception; import okio.Buffer; import okio.BufferedSource; @@ -142,7 +143,7 @@ public class WebSocket extends Transport { client.getDispatcher().getExecutorService().shutdown(); } - protected void write(Packet[] packets) { + protected void write(Packet[] packets) throws UTF8Exception { final WebSocket self = this; this.writable = false; for (Packet packet : packets) { diff --git a/src/main/java/io/socket/engineio/parser/Parser.java b/src/main/java/io/socket/engineio/parser/Parser.java index bad33a0..8b75b28 100644 --- a/src/main/java/io/socket/engineio/parser/Parser.java +++ b/src/main/java/io/socket/engineio/parser/Parser.java @@ -38,11 +38,11 @@ public class Parser { private Parser() {} - public static void encodePacket(Packet packet, EncodeCallback callback) { + public static void encodePacket(Packet packet, EncodeCallback callback) throws UTF8Exception { encodePacket(packet, false, callback); } - public static void encodePacket(Packet packet, boolean utf8encode, EncodeCallback callback) { + public static void encodePacket(Packet packet, boolean utf8encode, EncodeCallback callback) throws UTF8Exception { if (packet.data instanceof byte[]) { @SuppressWarnings("unchecked") Packet _packet = packet; @@ -109,7 +109,7 @@ public class Parser { return new Packet(packetslist.get(type), intArray); } - public static void encodePayload(Packet[] packets, EncodeCallback callback) { + public static void encodePayload(Packet[] packets, EncodeCallback callback) throws UTF8Exception { if (packets.length == 0) { callback.call(new byte[0]); return; diff --git a/src/main/java/io/socket/utf8/UTF8.java b/src/main/java/io/socket/utf8/UTF8.java index 75d26eb..52a0060 100644 --- a/src/main/java/io/socket/utf8/UTF8.java +++ b/src/main/java/io/socket/utf8/UTF8.java @@ -14,8 +14,8 @@ public class UTF8 { private static int byteCount; private static int byteIndex; - public static String encode(String string) { - int[] codePoints = uc2decode(string); + public static String encode(String string) throws UTF8Exception { + int[] codePoints = ucs2decode(string); int length = codePoints.length; int index = -1; int codePoint; @@ -28,7 +28,7 @@ public class UTF8 { } public static String decode(String byteString) throws UTF8Exception { - byteArray = uc2decode(byteString); + byteArray = ucs2decode(byteString); byteCount = byteArray.length; byteIndex = 0; List codePoints = new ArrayList(); @@ -39,7 +39,7 @@ public class UTF8 { return ucs2encode(listToArray(codePoints)); } - private static int[] uc2decode(String string) { + private static int[] ucs2decode(String string) { int length = string.length(); int[] output = new int[string.codePointCount(0, length)]; int counter = 0; @@ -51,7 +51,7 @@ public class UTF8 { return output; } - private static String encodeCodePoint(int codePoint) { + private static String encodeCodePoint(int codePoint) throws UTF8Exception { StringBuilder symbol = new StringBuilder(); if ((codePoint & 0xFFFFFF80) == 0) { return symbol.append(Character.toChars(codePoint)).toString(); @@ -59,6 +59,7 @@ public class UTF8 { if ((codePoint & 0xFFFFF800) == 0) { symbol.append(Character.toChars(((codePoint >> 6) & 0x1F) | 0xC0)); } else if ((codePoint & 0xFFFF0000) == 0) { + checkScalarValue(codePoint); symbol.append(Character.toChars(((codePoint >> 12) & 0x0F) | 0xE0)); symbol.append(createByte(codePoint, 6)); } else if ((codePoint & 0xFFE00000) == 0) { @@ -111,6 +112,7 @@ public class UTF8 { byte3 = readContinuationByte(); codePoint = ((byte1 & 0x0F) << 12) | (byte2 << 6) | byte3; if (codePoint >= 0x0800) { + checkScalarValue(codePoint); return codePoint; } else { throw new UTF8Exception("Invalid continuation byte"); @@ -153,6 +155,15 @@ public class UTF8 { return output.toString(); } + private static void checkScalarValue(int codePoint) throws UTF8Exception { + if (codePoint >= 0xD800 && codePoint <= 0xDFFF) { + throw new UTF8Exception( + "Lone surrogate U+" + Integer.toHexString(codePoint).toUpperCase() + + " is not a scalar value" + ); + } + } + private static int[] listToArray(List list) { int size = list.size(); int[] array = new int[size]; diff --git a/src/test/java/io/socket/engineio/client/ConnectionTest.java b/src/test/java/io/socket/engineio/client/ConnectionTest.java index 950c50c..ac0997e 100644 --- a/src/test/java/io/socket/engineio/client/ConnectionTest.java +++ b/src/test/java/io/socket/engineio/client/ConnectionTest.java @@ -72,7 +72,7 @@ public class ConnectionTest extends Connection { socket.on(Socket.EVENT_OPEN, new Emitter.Listener() { @Override public void call(Object... args) { - socket.send("\uD800-\uDB7F\uDB80-\uDBFF\uDC00-\uDFFF\uE000-\uF8FF"); + socket.send("\uD800\uDC00-\uDB7F\uDFFF\uDB80\uDC00-\uDBFF\uDFFF\uE000-\uF8FF"); socket.on(Socket.EVENT_MESSAGE, new Emitter.Listener() { @Override public void call(Object... args) { @@ -85,7 +85,7 @@ public class ConnectionTest extends Connection { }); socket.open(); - assertThat((String)values.take(), is("\uD800-\uDB7F\uDB80-\uDBFF\uDC00-\uDFFF\uE000-\uF8FF")); + assertThat((String)values.take(), is("\uD800\uDC00-\uDB7F\uDFFF\uDB80\uDC00-\uDBFF\uDFFF\uE000-\uF8FF")); } @Test(timeout = TIMEOUT) diff --git a/src/test/java/io/socket/engineio/parser/ParserTest.java b/src/test/java/io/socket/engineio/parser/ParserTest.java index 70a2d78..a904312 100644 --- a/src/test/java/io/socket/engineio/parser/ParserTest.java +++ b/src/test/java/io/socket/engineio/parser/ParserTest.java @@ -1,5 +1,6 @@ package io.socket.engineio.parser; +import io.socket.utf8.UTF8Exception; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -14,7 +15,7 @@ public class ParserTest { static final String ERROR_DATA = "parser error"; @Test - public void encodeAsString() { + public void encodeAsString() throws UTF8Exception { encodePacket(new Packet(Packet.MESSAGE, "test"), new EncodeCallback() { @Override public void call(String data) { @@ -24,7 +25,7 @@ public class ParserTest { } @Test - public void decodeAsPacket() { + public void decodeAsPacket() throws UTF8Exception { encodePacket(new Packet(Packet.MESSAGE, "test"), new EncodeCallback() { @Override public void call(String data) { @@ -34,7 +35,7 @@ public class ParserTest { } @Test - public void noData() { + public void noData() throws UTF8Exception { encodePacket(new Packet(Packet.MESSAGE), new EncodeCallback() { @Override public void call(String data) { @@ -46,7 +47,7 @@ public class ParserTest { } @Test - public void encodeOpenPacket() { + public void encodeOpenPacket() throws UTF8Exception { encodePacket(new Packet(Packet.OPEN, "{\"some\":\"json\"}"), new EncodeCallback() { @Override public void call(String data) { @@ -58,7 +59,7 @@ public class ParserTest { } @Test - public void encodeClosePacket() { + public void encodeClosePacket() throws UTF8Exception { encodePacket(new Packet(Packet.CLOSE), new EncodeCallback() { @Override public void call(String data) { @@ -69,7 +70,7 @@ public class ParserTest { } @Test - public void encodePingPacket() { + public void encodePingPacket() throws UTF8Exception { encodePacket(new Packet(Packet.PING, "1"), new EncodeCallback() { @Override public void call(String data) { @@ -81,7 +82,7 @@ public class ParserTest { } @Test - public void encodePongPacket() { + public void encodePongPacket() throws UTF8Exception { encodePacket(new Packet(Packet.PONG, "1"), new EncodeCallback() { @Override public void call(String data) { @@ -93,7 +94,7 @@ public class ParserTest { } @Test - public void encodeMessagePacket() { + public void encodeMessagePacket() throws UTF8Exception { encodePacket(new Packet(Packet.MESSAGE, "aaa"), new EncodeCallback() { @Override public void call(String data) { @@ -105,7 +106,7 @@ public class ParserTest { } @Test - public void encodeUTF8SpecialCharsMessagePacket() { + public void encodeUTF8SpecialCharsMessagePacket() throws UTF8Exception { encodePacket(new Packet(Packet.MESSAGE, "utf8 — string"), new EncodeCallback() { @Override public void call(String data) { @@ -117,7 +118,7 @@ public class ParserTest { } @Test - public void encodeMessagePacketCoercingToString() { + public void encodeMessagePacketCoercingToString() throws UTF8Exception { encodePacket(new Packet(Packet.MESSAGE, 1), new EncodeCallback() { @Override public void call(String data) { @@ -129,7 +130,7 @@ public class ParserTest { } @Test - public void encodeUpgradePacket() { + public void encodeUpgradePacket() throws UTF8Exception { encodePacket(new Packet(Packet.UPGRADE), new EncodeCallback() { @Override public void call(String data) { @@ -140,7 +141,7 @@ public class ParserTest { } @Test - public void encodingFormat() { + public void encodingFormat() throws UTF8Exception { encodePacket(new Packet(Packet.MESSAGE, "test"), new EncodeCallback() { @Override public void call(String data) { @@ -177,7 +178,7 @@ public class ParserTest { } @Test - public void encodePayloads() { + public void encodePayloads() throws UTF8Exception { encodePayload(new Packet[]{new Packet(Packet.PING), new Packet(Packet.PONG)}, new EncodeCallback() { @Override public void call(byte[] data) { @@ -187,7 +188,7 @@ public class ParserTest { } @Test - public void encodeAndDecodePayloads() { + public void encodeAndDecodePayloads() throws UTF8Exception { encodePayload(new Packet[] {new Packet(Packet.MESSAGE, "a")}, new EncodeCallback() { @Override public void call(byte[] data) { @@ -221,7 +222,7 @@ public class ParserTest { } @Test - public void encodeAndDecodeEmptyPayloads() { + public void encodeAndDecodeEmptyPayloads() throws UTF8Exception { encodePayload(new Packet[] {}, new EncodeCallback() { @Override public void call(byte[] data) { @@ -335,7 +336,7 @@ public class ParserTest { } @Test - public void encodeBinaryMessage() { + public void encodeBinaryMessage() throws UTF8Exception { final byte[] data = new byte[5]; for (int i = 0; i < data.length; i++) { data[0] = (byte)i; @@ -351,7 +352,7 @@ public class ParserTest { } @Test - public void encodeBinaryContents() { + public void encodeBinaryContents() throws UTF8Exception { final byte[] firstBuffer = new byte[5]; for (int i = 0 ; i < firstBuffer.length; i++) { firstBuffer[0] = (byte)i; @@ -385,7 +386,7 @@ public class ParserTest { } @Test - public void encodeMixedBinaryAndStringContents() { + public void encodeMixedBinaryAndStringContents() throws UTF8Exception { final byte[] firstBuffer = new byte[123]; for (int i = 0 ; i < firstBuffer.length; i++) { firstBuffer[0] = (byte)i; diff --git a/src/test/java/io/socket/utf8/UTF8Test.java b/src/test/java/io/socket/utf8/UTF8Test.java index 10aa4c1..8357d7f 100644 --- a/src/test/java/io/socket/utf8/UTF8Test.java +++ b/src/test/java/io/socket/utf8/UTF8Test.java @@ -26,25 +26,25 @@ public class UTF8Test { new Data(0x07FF, "\uFFFF", "\u00EF\u00BF\u00BF"), // unmatched surrogate halves // high surrogates: 0xD800 to 0xDBFF - new Data(0xD800, "\uD800", "\u00ED\u00A0\u0080"), + new Data(0xD800, "\uD800", "\u00ED\u00A0\u0080", true), new Data("High surrogate followed by another high surrogate", - "\uD800\uD800", "\u00ED\u00A0\u0080\u00ED\u00A0\u0080"), + "\uD800\uD800", "\u00ED\u00A0\u0080\u00ED\u00A0\u0080", true), new Data("High surrogate followed by a symbol that is not a surrogate", - "\uD800A", "\u00ED\u00A0\u0080A"), + "\uD800A", "\u00ED\u00A0\u0080A", true), new Data("Unmatched high surrogate, followed by a surrogate pair, followed by an unmatched high surrogate", - "\uD800\uD834\uDF06\uD800", "\u00ED\u00A0\u0080\u00F0\u009D\u008C\u0086\u00ED\u00A0\u0080"), - new Data(0xD9AF, "\uD9AF", "\u00ED\u00A6\u00AF"), - new Data(0xDBFF, "\uDBFF", "\u00ED\u00AF\u00BF"), + "\uD800\uD834\uDF06\uD800", "\u00ED\u00A0\u0080\u00F0\u009D\u008C\u0086\u00ED\u00A0\u0080", true), + new Data(0xD9AF, "\uD9AF", "\u00ED\u00A6\u00AF", true), + new Data(0xDBFF, "\uDBFF", "\u00ED\u00AF\u00BF", true), // low surrogates: 0xDC00 to 0xDFFF - new Data(0xDC00, "\uDC00", "\u00ED\u00B0\u0080"), + new Data(0xDC00, "\uDC00", "\u00ED\u00B0\u0080", true), new Data("Low surrogate followed by another low surrogate", - "\uDC00\uDC00", "\u00ED\u00B0\u0080\u00ED\u00B0\u0080"), + "\uDC00\uDC00", "\u00ED\u00B0\u0080\u00ED\u00B0\u0080", true), new Data("Low surrogate followed by a symbol that is not a surrogate", - "\uDC00A", "\u00ED\u00B0\u0080A"), + "\uDC00A", "\u00ED\u00B0\u0080A", true), new Data("Unmatched low surrogate, followed by a surrogate pair, followed by an unmatched low surrogate", - "\uDC00\uD834\uDF06\uDC00", "\u00ED\u00B0\u0080\u00F0\u009D\u008C\u0086\u00ED\u00B0\u0080"), - new Data(0xDEEE, "\uDEEE", "\u00ED\u00BB\u00AE"), - new Data(0xDFFF, "\uDFFF", "\u00ED\u00BF\u00BF"), + "\uDC00\uD834\uDF06\uDC00", "\u00ED\u00B0\u0080\u00F0\u009D\u008C\u0086\u00ED\u00B0\u0080", true), + new Data(0xDEEE, "\uDEEE", "\u00ED\u00BB\u00AE", true), + new Data(0xDFFF, "\uDFFF", "\u00ED\u00BF\u00BF", true), // 4-byte new Data(0x010000, "\uD800\uDC00", "\u00F0\u0090\u0080\u0080"), new Data(0x01D306, "\uD834\uDF06", "\u00F0\u009D\u008C\u0086"), @@ -58,8 +58,15 @@ public class UTF8Test { public void encodeAndDecode() throws UTF8Exception { for (Data data : DATA) { String reason = data.description != null? data.description : "U+" + Integer.toHexString(data.codePoint).toUpperCase(); - assertThat("Encoding: " + reason, data.encoded, is(UTF8.encode(data.decoded))); - assertThat("Decoding: " + reason, data.decoded, is(UTF8.decode(data.encoded))); + if (data.error) { + exception.expect(UTF8Exception.class); + UTF8.decode(data.encoded); + exception.expect(UTF8Exception.class); + UTF8.encode(data.decoded); + } else { + assertThat("Encoding: " + reason, data.encoded, is(UTF8.encode(data.decoded))); + assertThat("Decoding: " + reason, data.decoded, is(UTF8.decode(data.encoded))); + } } exception.expect(UTF8Exception.class); @@ -80,17 +87,28 @@ public class UTF8Test { public String description; public String decoded; public String encoded; + public boolean error; public Data(int codePoint, String decoded, String encoded) { + this(codePoint, decoded, encoded, false); + } + + public Data(int codePoint, String decoded, String encoded, boolean error) { this.codePoint = codePoint; this.decoded = decoded; this.encoded = encoded; + this.error = error; } public Data(String description, String decoded, String encoded) { + this(description, decoded, encoded, false); + } + + public Data(String description, String decoded, String encoded, boolean error) { this.description = description; this.decoded = decoded; this.encoded = encoded; + this.error = error; } } }