feat: add support for Engine.IO v4
Reference: https://github.com/socketio/engine.io-protocol#difference-between-v3-and-v4
This commit is contained in:
@@ -128,7 +128,6 @@ public class Socket extends Emitter {
|
|||||||
/*package*/ LinkedList<Packet> writeBuffer = new LinkedList<Packet>();
|
/*package*/ LinkedList<Packet> writeBuffer = new LinkedList<Packet>();
|
||||||
/*package*/ Transport transport;
|
/*package*/ Transport transport;
|
||||||
private Future pingTimeoutTimer;
|
private Future pingTimeoutTimer;
|
||||||
private Future pingIntervalTimer;
|
|
||||||
private okhttp3.WebSocket.Factory webSocketFactory;
|
private okhttp3.WebSocket.Factory webSocketFactory;
|
||||||
private okhttp3.Call.Factory callFactory;
|
private okhttp3.Call.Factory callFactory;
|
||||||
|
|
||||||
@@ -137,7 +136,7 @@ public class Socket extends Emitter {
|
|||||||
private final Listener onHeartbeatAsListener = new Listener() {
|
private final Listener onHeartbeatAsListener = new Listener() {
|
||||||
@Override
|
@Override
|
||||||
public void call(Object... args) {
|
public void call(Object... args) {
|
||||||
Socket.this.onHeartbeat(args.length > 0 ? (Long)args[0]: 0);
|
Socket.this.onHeartbeat();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -540,9 +539,14 @@ public class Socket extends Emitter {
|
|||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
this.emit(EVENT_ERROR, new EngineIOException(e));
|
this.emit(EVENT_ERROR, new EngineIOException(e));
|
||||||
}
|
}
|
||||||
} else if (Packet.PONG.equals(packet.type)) {
|
} else if (Packet.PING.equals(packet.type)) {
|
||||||
this.setPing();
|
this.emit(EVENT_PING);
|
||||||
this.emit(EVENT_PONG);
|
EventThread.exec(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Socket.this.sendPacket(Packet.PONG, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else if (Packet.ERROR.equals(packet.type)) {
|
} else if (Packet.ERROR.equals(packet.type)) {
|
||||||
EngineIOException err = new EngineIOException("server error");
|
EngineIOException err = new EngineIOException("server error");
|
||||||
err.code = packet.data;
|
err.code = packet.data;
|
||||||
@@ -568,20 +572,18 @@ public class Socket extends Emitter {
|
|||||||
this.onOpen();
|
this.onOpen();
|
||||||
// In case open handler closes socket
|
// In case open handler closes socket
|
||||||
if (ReadyState.CLOSED == this.readyState) return;
|
if (ReadyState.CLOSED == this.readyState) return;
|
||||||
this.setPing();
|
this.onHeartbeat();
|
||||||
|
|
||||||
this.off(EVENT_HEARTBEAT, this.onHeartbeatAsListener);
|
this.off(EVENT_HEARTBEAT, this.onHeartbeatAsListener);
|
||||||
this.on(EVENT_HEARTBEAT, this.onHeartbeatAsListener);
|
this.on(EVENT_HEARTBEAT, this.onHeartbeatAsListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onHeartbeat(long timeout) {
|
private void onHeartbeat() {
|
||||||
if (this.pingTimeoutTimer != null) {
|
if (this.pingTimeoutTimer != null) {
|
||||||
pingTimeoutTimer.cancel(false);
|
pingTimeoutTimer.cancel(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeout <= 0) {
|
long timeout = this.pingInterval + this.pingTimeout;
|
||||||
timeout = this.pingInterval + this.pingTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Socket self = this;
|
final Socket self = this;
|
||||||
this.pingTimeoutTimer = this.getHeartbeatScheduler().schedule(new Runnable() {
|
this.pingTimeoutTimer = this.getHeartbeatScheduler().schedule(new Runnable() {
|
||||||
@@ -598,46 +600,6 @@ public class Socket extends Emitter {
|
|||||||
}, timeout, TimeUnit.MILLISECONDS);
|
}, timeout, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setPing() {
|
|
||||||
if (this.pingIntervalTimer != null) {
|
|
||||||
pingIntervalTimer.cancel(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
final Socket self = this;
|
|
||||||
this.pingIntervalTimer = this.getHeartbeatScheduler().schedule(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
EventThread.exec(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if (logger.isLoggable(Level.FINE)) {
|
|
||||||
logger.fine(String.format("writing ping packet - expecting pong within %sms", self.pingTimeout));
|
|
||||||
}
|
|
||||||
self.ping();
|
|
||||||
self.onHeartbeat(self.pingTimeout);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, this.pingInterval, TimeUnit.MILLISECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a ping packet.
|
|
||||||
*/
|
|
||||||
private void ping() {
|
|
||||||
EventThread.exec(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Socket.this.sendPacket(Packet.PING, new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Socket.this.emit(EVENT_PING);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onDrain() {
|
private void onDrain() {
|
||||||
for (int i = 0; i < this.prevBufferLen; i++) {
|
for (int i = 0; i < this.prevBufferLen; i++) {
|
||||||
this.writeBuffer.poll();
|
this.writeBuffer.poll();
|
||||||
@@ -833,9 +795,6 @@ public class Socket extends Emitter {
|
|||||||
final Socket self = this;
|
final Socket self = this;
|
||||||
|
|
||||||
// clear timers
|
// clear timers
|
||||||
if (this.pingIntervalTimer != null) {
|
|
||||||
this.pingIntervalTimer.cancel(false);
|
|
||||||
}
|
|
||||||
if (this.pingTimeoutTimer != null) {
|
if (this.pingTimeoutTimer != null) {
|
||||||
this.pingTimeoutTimer.cancel(false);
|
this.pingTimeoutTimer.cancel(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import io.socket.emitter.Emitter;
|
|||||||
import io.socket.engineio.parser.Packet;
|
import io.socket.engineio.parser.Packet;
|
||||||
import io.socket.engineio.parser.Parser;
|
import io.socket.engineio.parser.Parser;
|
||||||
import io.socket.thread.EventThread;
|
import io.socket.thread.EventThread;
|
||||||
import io.socket.utf8.UTF8Exception;
|
|
||||||
import okhttp3.Call;
|
import okhttp3.Call;
|
||||||
import okhttp3.WebSocket;
|
import okhttp3.WebSocket;
|
||||||
|
|
||||||
@@ -96,11 +95,7 @@ public abstract class Transport extends Emitter {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
if (Transport.this.readyState == ReadyState.OPEN) {
|
if (Transport.this.readyState == ReadyState.OPEN) {
|
||||||
try {
|
|
||||||
Transport.this.write(packets);
|
Transport.this.write(packets);
|
||||||
} catch (UTF8Exception err) {
|
|
||||||
throw new RuntimeException(err);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw new RuntimeException("Transport not open");
|
throw new RuntimeException("Transport not open");
|
||||||
}
|
}
|
||||||
@@ -131,7 +126,7 @@ public abstract class Transport extends Emitter {
|
|||||||
this.emit(EVENT_CLOSE);
|
this.emit(EVENT_CLOSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract protected void write(Packet[] packets) throws UTF8Exception;
|
abstract protected void write(Packet[] packets);
|
||||||
|
|
||||||
abstract protected void doOpen();
|
abstract protected void doOpen();
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import io.socket.engineio.parser.Packet;
|
|||||||
import io.socket.engineio.parser.Parser;
|
import io.socket.engineio.parser.Parser;
|
||||||
import io.socket.parseqs.ParseQS;
|
import io.socket.parseqs.ParseQS;
|
||||||
import io.socket.thread.EventThread;
|
import io.socket.thread.EventThread;
|
||||||
import io.socket.utf8.UTF8Exception;
|
|
||||||
import io.socket.yeast.Yeast;
|
import io.socket.yeast.Yeast;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -129,13 +128,7 @@ abstract public class Polling extends Transport {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (data instanceof String) {
|
Parser.decodePayload((String) data, callback);
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
Parser.DecodePayloadCallback<String> tempCallback = callback;
|
|
||||||
Parser.decodePayload((String)data, tempCallback);
|
|
||||||
} else if (data instanceof byte[]) {
|
|
||||||
Parser.decodePayload((byte[])data, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.readyState != ReadyState.CLOSED) {
|
if (this.readyState != ReadyState.CLOSED) {
|
||||||
this.polling = false;
|
this.polling = false;
|
||||||
@@ -158,11 +151,7 @@ abstract public class Polling extends Transport {
|
|||||||
@Override
|
@Override
|
||||||
public void call(Object... args) {
|
public void call(Object... args) {
|
||||||
logger.fine("writing close packet");
|
logger.fine("writing close packet");
|
||||||
try {
|
|
||||||
self.write(new Packet[]{new Packet(Packet.CLOSE)});
|
self.write(new Packet[]{new Packet(Packet.CLOSE)});
|
||||||
} catch (UTF8Exception err) {
|
|
||||||
throw new RuntimeException(err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -177,7 +166,7 @@ abstract public class Polling extends Transport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void write(Packet[] packets) throws UTF8Exception {
|
protected void write(Packet[] packets) {
|
||||||
final Polling self = this;
|
final Polling self = this;
|
||||||
this.writable = false;
|
this.writable = false;
|
||||||
final Runnable callbackfn = new Runnable() {
|
final Runnable callbackfn = new Runnable() {
|
||||||
@@ -188,16 +177,10 @@ abstract public class Polling extends Transport {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Parser.encodePayload(packets, new Parser.EncodeCallback() {
|
Parser.encodePayload(packets, new Parser.EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(Object data) {
|
public void call(String data) {
|
||||||
if (data instanceof byte[]) {
|
self.doWrite(data, callbackfn);
|
||||||
self.doWrite((byte[])data, callbackfn);
|
|
||||||
} else if (data instanceof String) {
|
|
||||||
self.doWrite((String)data, callbackfn);
|
|
||||||
} else {
|
|
||||||
logger.warning("Unexpected data: " + data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -229,8 +212,6 @@ abstract public class Polling extends Transport {
|
|||||||
return schema + "://" + (ipv6 ? "[" + this.hostname + "]" : this.hostname) + port + this.path + derivedQuery;
|
return schema + "://" + (ipv6 ? "[" + this.hostname + "]" : this.hostname) + port + this.path + derivedQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract protected void doWrite(byte[] data, Runnable fn);
|
|
||||||
|
|
||||||
abstract protected void doWrite(String data, Runnable fn);
|
abstract protected void doWrite(String data, Runnable fn);
|
||||||
|
|
||||||
abstract protected void doPoll();
|
abstract protected void doPoll();
|
||||||
|
|||||||
@@ -67,17 +67,8 @@ public class PollingXHR extends Polling {
|
|||||||
return req;
|
return req;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void doWrite(byte[] data, final Runnable fn) {
|
|
||||||
this.doWrite((Object) data, fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doWrite(String data, final Runnable fn) {
|
protected void doWrite(String data, final Runnable fn) {
|
||||||
this.doWrite((Object) data, fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doWrite(Object data, final Runnable fn) {
|
|
||||||
Request.Options opts = new Request.Options();
|
Request.Options opts = new Request.Options();
|
||||||
opts.method = "POST";
|
opts.method = "POST";
|
||||||
opts.data = data;
|
opts.data = data;
|
||||||
@@ -121,11 +112,7 @@ public class PollingXHR extends Polling {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
Object arg = args.length > 0 ? args[0] : null;
|
Object arg = args.length > 0 ? args[0] : null;
|
||||||
if (arg instanceof String) {
|
|
||||||
self.onData((String)arg);
|
self.onData((String)arg);
|
||||||
} else if (arg instanceof byte[]) {
|
|
||||||
self.onData((byte[])arg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -153,16 +140,14 @@ public class PollingXHR extends Polling {
|
|||||||
public static final String EVENT_REQUEST_HEADERS = "requestHeaders";
|
public static final String EVENT_REQUEST_HEADERS = "requestHeaders";
|
||||||
public static final String EVENT_RESPONSE_HEADERS = "responseHeaders";
|
public static final String EVENT_RESPONSE_HEADERS = "responseHeaders";
|
||||||
|
|
||||||
private static final String BINARY_CONTENT_TYPE = "application/octet-stream";
|
|
||||||
private static final String TEXT_CONTENT_TYPE = "text/plain;charset=UTF-8";
|
private static final String TEXT_CONTENT_TYPE = "text/plain;charset=UTF-8";
|
||||||
|
|
||||||
private static final MediaType BINARY_MEDIA_TYPE = MediaType.parse(BINARY_CONTENT_TYPE);
|
|
||||||
private static final MediaType TEXT_MEDIA_TYPE = MediaType.parse(TEXT_CONTENT_TYPE);
|
private static final MediaType TEXT_MEDIA_TYPE = MediaType.parse(TEXT_CONTENT_TYPE);
|
||||||
|
|
||||||
private String method;
|
private String method;
|
||||||
private String uri;
|
private String uri;
|
||||||
|
|
||||||
private Object data;
|
private String data;
|
||||||
|
|
||||||
private Call.Factory callFactory;
|
private Call.Factory callFactory;
|
||||||
private Response response;
|
private Response response;
|
||||||
@@ -181,20 +166,15 @@ public class PollingXHR extends Polling {
|
|||||||
Map<String, List<String>> headers = new TreeMap<String, List<String>>(String.CASE_INSENSITIVE_ORDER);
|
Map<String, List<String>> headers = new TreeMap<String, List<String>>(String.CASE_INSENSITIVE_ORDER);
|
||||||
|
|
||||||
if ("POST".equals(this.method)) {
|
if ("POST".equals(this.method)) {
|
||||||
if (this.data instanceof byte[]) {
|
|
||||||
headers.put("Content-type", new LinkedList<String>(Collections.singletonList(BINARY_CONTENT_TYPE)));
|
|
||||||
} else {
|
|
||||||
headers.put("Content-type", new LinkedList<String>(Collections.singletonList(TEXT_CONTENT_TYPE)));
|
headers.put("Content-type", new LinkedList<String>(Collections.singletonList(TEXT_CONTENT_TYPE)));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
headers.put("Accept", new LinkedList<String>(Collections.singletonList("*/*")));
|
headers.put("Accept", new LinkedList<String>(Collections.singletonList("*/*")));
|
||||||
|
|
||||||
this.onRequestHeaders(headers);
|
this.onRequestHeaders(headers);
|
||||||
|
|
||||||
if (LOGGABLE_FINE) {
|
if (LOGGABLE_FINE) {
|
||||||
logger.fine(String.format("sending xhr with url %s | data %s", this.uri,
|
logger.fine(String.format("sending xhr with url %s | data %s", this.uri, this.data));
|
||||||
this.data instanceof byte[] ? Arrays.toString((byte[]) this.data) : this.data));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder();
|
okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder();
|
||||||
@@ -204,10 +184,8 @@ public class PollingXHR extends Polling {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
RequestBody body = null;
|
RequestBody body = null;
|
||||||
if (this.data instanceof byte[]) {
|
if (this.data != null) {
|
||||||
body = RequestBody.create(BINARY_MEDIA_TYPE, (byte[])this.data);
|
body = RequestBody.create(TEXT_MEDIA_TYPE, this.data);
|
||||||
} else if (this.data instanceof String) {
|
|
||||||
body = RequestBody.create(TEXT_MEDIA_TYPE, (String)this.data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
okhttp3.Request request = requestBuilder
|
okhttp3.Request request = requestBuilder
|
||||||
@@ -249,11 +227,6 @@ public class PollingXHR extends Polling {
|
|||||||
this.onSuccess();
|
this.onSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onData(byte[] data) {
|
|
||||||
this.emit(EVENT_DATA, data);
|
|
||||||
this.onSuccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onError(Exception err) {
|
private void onError(Exception err) {
|
||||||
this.emit(EVENT_ERROR, err);
|
this.emit(EVENT_ERROR, err);
|
||||||
}
|
}
|
||||||
@@ -268,14 +241,9 @@ public class PollingXHR extends Polling {
|
|||||||
|
|
||||||
private void onLoad() {
|
private void onLoad() {
|
||||||
ResponseBody body = response.body();
|
ResponseBody body = response.body();
|
||||||
MediaType mediaType = body.contentType();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (mediaType != null && BINARY_CONTENT_TYPE.equalsIgnoreCase(mediaType.toString())) {
|
|
||||||
this.onData(body.bytes());
|
|
||||||
} else {
|
|
||||||
this.onData(body.string());
|
this.onData(body.string());
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
this.onError(e);
|
this.onError(e);
|
||||||
}
|
}
|
||||||
@@ -285,7 +253,7 @@ public class PollingXHR extends Polling {
|
|||||||
|
|
||||||
public String uri;
|
public String uri;
|
||||||
public String method;
|
public String method;
|
||||||
public Object data;
|
public String data;
|
||||||
public Call.Factory callFactory;
|
public Call.Factory callFactory;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
package io.socket.engineio.client.transports;
|
package io.socket.engineio.client.transports;
|
||||||
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.TreeMap;
|
|
||||||
import java.util.logging.Logger;
|
|
||||||
|
|
||||||
import io.socket.engineio.client.Transport;
|
import io.socket.engineio.client.Transport;
|
||||||
import io.socket.engineio.parser.Packet;
|
import io.socket.engineio.parser.Packet;
|
||||||
import io.socket.engineio.parser.Parser;
|
import io.socket.engineio.parser.Parser;
|
||||||
import io.socket.parseqs.ParseQS;
|
import io.socket.parseqs.ParseQS;
|
||||||
import io.socket.thread.EventThread;
|
import io.socket.thread.EventThread;
|
||||||
import io.socket.utf8.UTF8Exception;
|
|
||||||
import io.socket.yeast.Yeast;
|
import io.socket.yeast.Yeast;
|
||||||
import okhttp3.OkHttpClient;
|
import okhttp3.OkHttpClient;
|
||||||
import okhttp3.Request;
|
import okhttp3.Request;
|
||||||
@@ -20,6 +13,12 @@ import okhttp3.Response;
|
|||||||
import okhttp3.WebSocketListener;
|
import okhttp3.WebSocketListener;
|
||||||
import okio.ByteString;
|
import okio.ByteString;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
|
||||||
public class WebSocket extends Transport {
|
public class WebSocket extends Transport {
|
||||||
|
|
||||||
@@ -111,7 +110,7 @@ public class WebSocket extends Transport {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void write(Packet[] packets) throws UTF8Exception {
|
protected void write(Packet[] packets) {
|
||||||
final WebSocket self = this;
|
final WebSocket self = this;
|
||||||
this.writable = false;
|
this.writable = false;
|
||||||
|
|
||||||
|
|||||||
665
src/main/java/io/socket/engineio/parser/Base64.java
Normal file
665
src/main/java/io/socket/engineio/parser/Base64.java
Normal file
@@ -0,0 +1,665 @@
|
|||||||
|
// from https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/util/Base64.java
|
||||||
|
package io.socket.engineio.parser;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2010 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* 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 the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities for encoding and decoding the Base64 representation of
|
||||||
|
* binary data. See RFCs <a
|
||||||
|
* href="http://www.ietf.org/rfc/rfc2045.txt">2045</a> and <a
|
||||||
|
* href="http://www.ietf.org/rfc/rfc3548.txt">3548</a>.
|
||||||
|
*/
|
||||||
|
public class Base64 {
|
||||||
|
/**
|
||||||
|
* Default values for encoder/decoder flags.
|
||||||
|
*/
|
||||||
|
public static final int DEFAULT = 0;
|
||||||
|
/**
|
||||||
|
* Encoder flag bit to omit the padding '=' characters at the end
|
||||||
|
* of the output (if any).
|
||||||
|
*/
|
||||||
|
public static final int NO_PADDING = 1;
|
||||||
|
/**
|
||||||
|
* Encoder flag bit to omit all line terminators (i.e., the output
|
||||||
|
* will be on one long line).
|
||||||
|
*/
|
||||||
|
public static final int NO_WRAP = 2;
|
||||||
|
/**
|
||||||
|
* Encoder flag bit to indicate lines should be terminated with a
|
||||||
|
* CRLF pair instead of just an LF. Has no effect if {@code
|
||||||
|
* NO_WRAP} is specified as well.
|
||||||
|
*/
|
||||||
|
public static final int CRLF = 4;
|
||||||
|
/**
|
||||||
|
* Encoder/decoder flag bit to indicate using the "URL and
|
||||||
|
* filename safe" variant of Base64 (see RFC 3548 section 4) where
|
||||||
|
* {@code -} and {@code _} are used in place of {@code +} and
|
||||||
|
* {@code /}.
|
||||||
|
*/
|
||||||
|
public static final int URL_SAFE = 8;
|
||||||
|
/**
|
||||||
|
* Flag to pass to Base64OutputStream to indicate that it
|
||||||
|
* should not close the output stream it is wrapping when it
|
||||||
|
* itself is closed.
|
||||||
|
*/
|
||||||
|
public static final int NO_CLOSE = 16;
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// shared code
|
||||||
|
// --------------------------------------------------------
|
||||||
|
/* package */ static abstract class Coder {
|
||||||
|
public byte[] output;
|
||||||
|
public int op;
|
||||||
|
/**
|
||||||
|
* Encode/decode another block of input data. this.output is
|
||||||
|
* provided by the caller, and must be big enough to hold all
|
||||||
|
* the coded data. On exit, this.opwill be set to the length
|
||||||
|
* of the coded data.
|
||||||
|
*
|
||||||
|
* @param finish true if this is the final call to process for
|
||||||
|
* this object. Will finalize the coder state and
|
||||||
|
* include any final bytes in the output.
|
||||||
|
*
|
||||||
|
* @return true if the input so far is good; false if some
|
||||||
|
* error has been detected in the input stream..
|
||||||
|
*/
|
||||||
|
public abstract boolean process(byte[] input, int offset, int len, boolean finish);
|
||||||
|
/**
|
||||||
|
* @return the maximum number of bytes a call to process()
|
||||||
|
* could produce for the given number of input bytes. This may
|
||||||
|
* be an overestimate.
|
||||||
|
*/
|
||||||
|
public abstract int maxOutputSize(int len);
|
||||||
|
}
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// decoding
|
||||||
|
// --------------------------------------------------------
|
||||||
|
/**
|
||||||
|
* Decode the Base64-encoded data in input and return the data in
|
||||||
|
* a new byte array.
|
||||||
|
*
|
||||||
|
* <p>The padding '=' characters at the end are considered optional, but
|
||||||
|
* if any are present, there must be the correct number of them.
|
||||||
|
*
|
||||||
|
* @param str the input String to decode, which is converted to
|
||||||
|
* bytes using the default charset
|
||||||
|
* @param flags controls certain features of the decoded output.
|
||||||
|
* Pass {@code DEFAULT} to decode standard Base64.
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException if the input contains
|
||||||
|
* incorrect padding
|
||||||
|
*/
|
||||||
|
public static byte[] decode(String str, int flags) {
|
||||||
|
return decode(str.getBytes(), flags);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decode the Base64-encoded data in input and return the data in
|
||||||
|
* a new byte array.
|
||||||
|
*
|
||||||
|
* <p>The padding '=' characters at the end are considered optional, but
|
||||||
|
* if any are present, there must be the correct number of them.
|
||||||
|
*
|
||||||
|
* @param input the input array to decode
|
||||||
|
* @param flags controls certain features of the decoded output.
|
||||||
|
* Pass {@code DEFAULT} to decode standard Base64.
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException if the input contains
|
||||||
|
* incorrect padding
|
||||||
|
*/
|
||||||
|
public static byte[] decode(byte[] input, int flags) {
|
||||||
|
return decode(input, 0, input.length, flags);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decode the Base64-encoded data in input and return the data in
|
||||||
|
* a new byte array.
|
||||||
|
*
|
||||||
|
* <p>The padding '=' characters at the end are considered optional, but
|
||||||
|
* if any are present, there must be the correct number of them.
|
||||||
|
*
|
||||||
|
* @param input the data to decode
|
||||||
|
* @param offset the position within the input array at which to start
|
||||||
|
* @param len the number of bytes of input to decode
|
||||||
|
* @param flags controls certain features of the decoded output.
|
||||||
|
* Pass {@code DEFAULT} to decode standard Base64.
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException if the input contains
|
||||||
|
* incorrect padding
|
||||||
|
*/
|
||||||
|
public static byte[] decode(byte[] input, int offset, int len, int flags) {
|
||||||
|
// Allocate space for the most data the input could represent.
|
||||||
|
// (It could contain less if it contains whitespace, etc.)
|
||||||
|
Decoder decoder = new Decoder(flags, new byte[len*3/4]);
|
||||||
|
if (!decoder.process(input, offset, len, true)) {
|
||||||
|
throw new IllegalArgumentException("bad base-64");
|
||||||
|
}
|
||||||
|
// Maybe we got lucky and allocated exactly enough output space.
|
||||||
|
if (decoder.op == decoder.output.length) {
|
||||||
|
return decoder.output;
|
||||||
|
}
|
||||||
|
// Need to shorten the array, so allocate a new one of the
|
||||||
|
// right size and copy.
|
||||||
|
byte[] temp = new byte[decoder.op];
|
||||||
|
System.arraycopy(decoder.output, 0, temp, 0, decoder.op);
|
||||||
|
return temp;
|
||||||
|
}
|
||||||
|
/* package */ static class Decoder extends Coder {
|
||||||
|
/**
|
||||||
|
* Lookup table for turning bytes into their position in the
|
||||||
|
* Base64 alphabet.
|
||||||
|
*/
|
||||||
|
private static final int DECODE[] = {
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
|
||||||
|
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
|
||||||
|
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
|
||||||
|
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
|
||||||
|
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
|
||||||
|
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Decode lookup table for the "web safe" variant (RFC 3548
|
||||||
|
* sec. 4) where - and _ replace + and /.
|
||||||
|
*/
|
||||||
|
private static final int DECODE_WEBSAFE[] = {
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
|
||||||
|
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
|
||||||
|
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
|
||||||
|
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63,
|
||||||
|
-1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
|
||||||
|
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||||
|
};
|
||||||
|
/** Non-data values in the DECODE arrays. */
|
||||||
|
private static final int SKIP = -1;
|
||||||
|
private static final int EQUALS = -2;
|
||||||
|
/**
|
||||||
|
* States 0-3 are reading through the next input tuple.
|
||||||
|
* State 4 is having read one '=' and expecting exactly
|
||||||
|
* one more.
|
||||||
|
* State 5 is expecting no more data or padding characters
|
||||||
|
* in the input.
|
||||||
|
* State 6 is the error state; an error has been detected
|
||||||
|
* in the input and no future input can "fix" it.
|
||||||
|
*/
|
||||||
|
private int state; // state number (0 to 6)
|
||||||
|
private int value;
|
||||||
|
final private int[] alphabet;
|
||||||
|
public Decoder(int flags, byte[] output) {
|
||||||
|
this.output = output;
|
||||||
|
alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE;
|
||||||
|
state = 0;
|
||||||
|
value = 0;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @return an overestimate for the number of bytes {@code
|
||||||
|
* len} bytes could decode to.
|
||||||
|
*/
|
||||||
|
public int maxOutputSize(int len) {
|
||||||
|
return len * 3/4 + 10;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decode another block of input data.
|
||||||
|
*
|
||||||
|
* @return true if the state machine is still healthy. false if
|
||||||
|
* bad base-64 data has been detected in the input stream.
|
||||||
|
*/
|
||||||
|
public boolean process(byte[] input, int offset, int len, boolean finish) {
|
||||||
|
if (this.state == 6) return false;
|
||||||
|
int p = offset;
|
||||||
|
len += offset;
|
||||||
|
// Using local variables makes the decoder about 12%
|
||||||
|
// faster than if we manipulate the member variables in
|
||||||
|
// the loop. (Even alphabet makes a measurable
|
||||||
|
// difference, which is somewhat surprising to me since
|
||||||
|
// the member variable is final.)
|
||||||
|
int state = this.state;
|
||||||
|
int value = this.value;
|
||||||
|
int op = 0;
|
||||||
|
final byte[] output = this.output;
|
||||||
|
final int[] alphabet = this.alphabet;
|
||||||
|
while (p < len) {
|
||||||
|
// Try the fast path: we're starting a new tuple and the
|
||||||
|
// next four bytes of the input stream are all data
|
||||||
|
// bytes. This corresponds to going through states
|
||||||
|
// 0-1-2-3-0. We expect to use this method for most of
|
||||||
|
// the data.
|
||||||
|
//
|
||||||
|
// If any of the next four bytes of input are non-data
|
||||||
|
// (whitespace, etc.), value will end up negative. (All
|
||||||
|
// the non-data values in decode are small negative
|
||||||
|
// numbers, so shifting any of them up and or'ing them
|
||||||
|
// together will result in a value with its top bit set.)
|
||||||
|
//
|
||||||
|
// You can remove this whole block and the output should
|
||||||
|
// be the same, just slower.
|
||||||
|
if (state == 0) {
|
||||||
|
while (p+4 <= len &&
|
||||||
|
(value = ((alphabet[input[p] & 0xff] << 18) |
|
||||||
|
(alphabet[input[p+1] & 0xff] << 12) |
|
||||||
|
(alphabet[input[p+2] & 0xff] << 6) |
|
||||||
|
(alphabet[input[p+3] & 0xff]))) >= 0) {
|
||||||
|
output[op+2] = (byte) value;
|
||||||
|
output[op+1] = (byte) (value >> 8);
|
||||||
|
output[op] = (byte) (value >> 16);
|
||||||
|
op += 3;
|
||||||
|
p += 4;
|
||||||
|
}
|
||||||
|
if (p >= len) break;
|
||||||
|
}
|
||||||
|
// The fast path isn't available -- either we've read a
|
||||||
|
// partial tuple, or the next four input bytes aren't all
|
||||||
|
// data, or whatever. Fall back to the slower state
|
||||||
|
// machine implementation.
|
||||||
|
int d = alphabet[input[p++] & 0xff];
|
||||||
|
switch (state) {
|
||||||
|
case 0:
|
||||||
|
if (d >= 0) {
|
||||||
|
value = d;
|
||||||
|
++state;
|
||||||
|
} else if (d != SKIP) {
|
||||||
|
this.state = 6;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
if (d >= 0) {
|
||||||
|
value = (value << 6) | d;
|
||||||
|
++state;
|
||||||
|
} else if (d != SKIP) {
|
||||||
|
this.state = 6;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
if (d >= 0) {
|
||||||
|
value = (value << 6) | d;
|
||||||
|
++state;
|
||||||
|
} else if (d == EQUALS) {
|
||||||
|
// Emit the last (partial) output tuple;
|
||||||
|
// expect exactly one more padding character.
|
||||||
|
output[op++] = (byte) (value >> 4);
|
||||||
|
state = 4;
|
||||||
|
} else if (d != SKIP) {
|
||||||
|
this.state = 6;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
if (d >= 0) {
|
||||||
|
// Emit the output triple and return to state 0.
|
||||||
|
value = (value << 6) | d;
|
||||||
|
output[op+2] = (byte) value;
|
||||||
|
output[op+1] = (byte) (value >> 8);
|
||||||
|
output[op] = (byte) (value >> 16);
|
||||||
|
op += 3;
|
||||||
|
state = 0;
|
||||||
|
} else if (d == EQUALS) {
|
||||||
|
// Emit the last (partial) output tuple;
|
||||||
|
// expect no further data or padding characters.
|
||||||
|
output[op+1] = (byte) (value >> 2);
|
||||||
|
output[op] = (byte) (value >> 10);
|
||||||
|
op += 2;
|
||||||
|
state = 5;
|
||||||
|
} else if (d != SKIP) {
|
||||||
|
this.state = 6;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
if (d == EQUALS) {
|
||||||
|
++state;
|
||||||
|
} else if (d != SKIP) {
|
||||||
|
this.state = 6;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
if (d != SKIP) {
|
||||||
|
this.state = 6;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!finish) {
|
||||||
|
// We're out of input, but a future call could provide
|
||||||
|
// more.
|
||||||
|
this.state = state;
|
||||||
|
this.value = value;
|
||||||
|
this.op = op;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Done reading input. Now figure out where we are left in
|
||||||
|
// the state machine and finish up.
|
||||||
|
switch (state) {
|
||||||
|
case 0:
|
||||||
|
// Output length is a multiple of three. Fine.
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
// Read one extra input byte, which isn't enough to
|
||||||
|
// make another output byte. Illegal.
|
||||||
|
this.state = 6;
|
||||||
|
return false;
|
||||||
|
case 2:
|
||||||
|
// Read two extra input bytes, enough to emit 1 more
|
||||||
|
// output byte. Fine.
|
||||||
|
output[op++] = (byte) (value >> 4);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// Read three extra input bytes, enough to emit 2 more
|
||||||
|
// output bytes. Fine.
|
||||||
|
output[op++] = (byte) (value >> 10);
|
||||||
|
output[op++] = (byte) (value >> 2);
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
// Read one padding '=' when we expected 2. Illegal.
|
||||||
|
this.state = 6;
|
||||||
|
return false;
|
||||||
|
case 5:
|
||||||
|
// Read all the padding '='s we expected and no more.
|
||||||
|
// Fine.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.state = state;
|
||||||
|
this.op = op;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --------------------------------------------------------
|
||||||
|
// encoding
|
||||||
|
// --------------------------------------------------------
|
||||||
|
/**
|
||||||
|
* Base64-encode the given data and return a newly allocated
|
||||||
|
* String with the result.
|
||||||
|
*
|
||||||
|
* @param input the data to encode
|
||||||
|
* @param flags controls certain features of the encoded output.
|
||||||
|
* Passing {@code DEFAULT} results in output that
|
||||||
|
* adheres to RFC 2045.
|
||||||
|
*/
|
||||||
|
public static String encodeToString(byte[] input, int flags) {
|
||||||
|
try {
|
||||||
|
return new String(encode(input, flags), "US-ASCII");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
// US-ASCII is guaranteed to be available.
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Base64-encode the given data and return a newly allocated
|
||||||
|
* String with the result.
|
||||||
|
*
|
||||||
|
* @param input the data to encode
|
||||||
|
* @param offset the position within the input array at which to
|
||||||
|
* start
|
||||||
|
* @param len the number of bytes of input to encode
|
||||||
|
* @param flags controls certain features of the encoded output.
|
||||||
|
* Passing {@code DEFAULT} results in output that
|
||||||
|
* adheres to RFC 2045.
|
||||||
|
*/
|
||||||
|
public static String encodeToString(byte[] input, int offset, int len, int flags) {
|
||||||
|
try {
|
||||||
|
return new String(encode(input, offset, len, flags), "US-ASCII");
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
// US-ASCII is guaranteed to be available.
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Base64-encode the given data and return a newly allocated
|
||||||
|
* byte[] with the result.
|
||||||
|
*
|
||||||
|
* @param input the data to encode
|
||||||
|
* @param flags controls certain features of the encoded output.
|
||||||
|
* Passing {@code DEFAULT} results in output that
|
||||||
|
* adheres to RFC 2045.
|
||||||
|
*/
|
||||||
|
public static byte[] encode(byte[] input, int flags) {
|
||||||
|
return encode(input, 0, input.length, flags);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Base64-encode the given data and return a newly allocated
|
||||||
|
* byte[] with the result.
|
||||||
|
*
|
||||||
|
* @param input the data to encode
|
||||||
|
* @param offset the position within the input array at which to
|
||||||
|
* start
|
||||||
|
* @param len the number of bytes of input to encode
|
||||||
|
* @param flags controls certain features of the encoded output.
|
||||||
|
* Passing {@code DEFAULT} results in output that
|
||||||
|
* adheres to RFC 2045.
|
||||||
|
*/
|
||||||
|
public static byte[] encode(byte[] input, int offset, int len, int flags) {
|
||||||
|
Encoder encoder = new Encoder(flags, null);
|
||||||
|
// Compute the exact length of the array we will produce.
|
||||||
|
int output_len = len / 3 * 4;
|
||||||
|
// Account for the tail of the data and the padding bytes, if any.
|
||||||
|
if (encoder.do_padding) {
|
||||||
|
if (len % 3 > 0) {
|
||||||
|
output_len += 4;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (len % 3) {
|
||||||
|
case 0: break;
|
||||||
|
case 1: output_len += 2; break;
|
||||||
|
case 2: output_len += 3; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Account for the newlines, if any.
|
||||||
|
if (encoder.do_newline && len > 0) {
|
||||||
|
output_len += (((len-1) / (3 * Encoder.LINE_GROUPS)) + 1) *
|
||||||
|
(encoder.do_cr ? 2 : 1);
|
||||||
|
}
|
||||||
|
encoder.output = new byte[output_len];
|
||||||
|
encoder.process(input, offset, len, true);
|
||||||
|
assert encoder.op == output_len;
|
||||||
|
return encoder.output;
|
||||||
|
}
|
||||||
|
/* package */ static class Encoder extends Coder {
|
||||||
|
/**
|
||||||
|
* Emit a new line every this many output tuples. Corresponds to
|
||||||
|
* a 76-character line length (the maximum allowable according to
|
||||||
|
* <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>).
|
||||||
|
*/
|
||||||
|
public static final int LINE_GROUPS = 19;
|
||||||
|
/**
|
||||||
|
* Lookup table for turning Base64 alphabet positions (6 bits)
|
||||||
|
* into output bytes.
|
||||||
|
*/
|
||||||
|
private static final byte ENCODE[] = {
|
||||||
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
|
||||||
|
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
|
||||||
|
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
|
||||||
|
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Lookup table for turning Base64 alphabet positions (6 bits)
|
||||||
|
* into output bytes.
|
||||||
|
*/
|
||||||
|
private static final byte ENCODE_WEBSAFE[] = {
|
||||||
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
|
||||||
|
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
|
||||||
|
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
|
||||||
|
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',
|
||||||
|
};
|
||||||
|
final private byte[] tail;
|
||||||
|
/* package */ int tailLen;
|
||||||
|
private int count;
|
||||||
|
final public boolean do_padding;
|
||||||
|
final public boolean do_newline;
|
||||||
|
final public boolean do_cr;
|
||||||
|
final private byte[] alphabet;
|
||||||
|
public Encoder(int flags, byte[] output) {
|
||||||
|
this.output = output;
|
||||||
|
do_padding = (flags & NO_PADDING) == 0;
|
||||||
|
do_newline = (flags & NO_WRAP) == 0;
|
||||||
|
do_cr = (flags & CRLF) != 0;
|
||||||
|
alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE;
|
||||||
|
tail = new byte[2];
|
||||||
|
tailLen = 0;
|
||||||
|
count = do_newline ? LINE_GROUPS : -1;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @return an overestimate for the number of bytes {@code
|
||||||
|
* len} bytes could encode to.
|
||||||
|
*/
|
||||||
|
public int maxOutputSize(int len) {
|
||||||
|
return len * 8/5 + 10;
|
||||||
|
}
|
||||||
|
public boolean process(byte[] input, int offset, int len, boolean finish) {
|
||||||
|
// Using local variables makes the encoder about 9% faster.
|
||||||
|
final byte[] alphabet = this.alphabet;
|
||||||
|
final byte[] output = this.output;
|
||||||
|
int op = 0;
|
||||||
|
int count = this.count;
|
||||||
|
int p = offset;
|
||||||
|
len += offset;
|
||||||
|
int v = -1;
|
||||||
|
// First we need to concatenate the tail of the previous call
|
||||||
|
// with any input bytes available now and see if we can empty
|
||||||
|
// the tail.
|
||||||
|
switch (tailLen) {
|
||||||
|
case 0:
|
||||||
|
// There was no tail.
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
if (p+2 <= len) {
|
||||||
|
// A 1-byte tail with at least 2 bytes of
|
||||||
|
// input available now.
|
||||||
|
v = ((tail[0] & 0xff) << 16) |
|
||||||
|
((input[p++] & 0xff) << 8) |
|
||||||
|
(input[p++] & 0xff);
|
||||||
|
tailLen = 0;
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
if (p+1 <= len) {
|
||||||
|
// A 2-byte tail with at least 1 byte of input.
|
||||||
|
v = ((tail[0] & 0xff) << 16) |
|
||||||
|
((tail[1] & 0xff) << 8) |
|
||||||
|
(input[p++] & 0xff);
|
||||||
|
tailLen = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (v != -1) {
|
||||||
|
output[op++] = alphabet[(v >> 18) & 0x3f];
|
||||||
|
output[op++] = alphabet[(v >> 12) & 0x3f];
|
||||||
|
output[op++] = alphabet[(v >> 6) & 0x3f];
|
||||||
|
output[op++] = alphabet[v & 0x3f];
|
||||||
|
if (--count == 0) {
|
||||||
|
if (do_cr) output[op++] = '\r';
|
||||||
|
output[op++] = '\n';
|
||||||
|
count = LINE_GROUPS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// At this point either there is no tail, or there are fewer
|
||||||
|
// than 3 bytes of input available.
|
||||||
|
// The main loop, turning 3 input bytes into 4 output bytes on
|
||||||
|
// each iteration.
|
||||||
|
while (p+3 <= len) {
|
||||||
|
v = ((input[p] & 0xff) << 16) |
|
||||||
|
((input[p+1] & 0xff) << 8) |
|
||||||
|
(input[p+2] & 0xff);
|
||||||
|
output[op] = alphabet[(v >> 18) & 0x3f];
|
||||||
|
output[op+1] = alphabet[(v >> 12) & 0x3f];
|
||||||
|
output[op+2] = alphabet[(v >> 6) & 0x3f];
|
||||||
|
output[op+3] = alphabet[v & 0x3f];
|
||||||
|
p += 3;
|
||||||
|
op += 4;
|
||||||
|
if (--count == 0) {
|
||||||
|
if (do_cr) output[op++] = '\r';
|
||||||
|
output[op++] = '\n';
|
||||||
|
count = LINE_GROUPS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (finish) {
|
||||||
|
// Finish up the tail of the input. Note that we need to
|
||||||
|
// consume any bytes in tail before any bytes
|
||||||
|
// remaining in input; there should be at most two bytes
|
||||||
|
// total.
|
||||||
|
if (p-tailLen == len-1) {
|
||||||
|
int t = 0;
|
||||||
|
v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4;
|
||||||
|
tailLen -= t;
|
||||||
|
output[op++] = alphabet[(v >> 6) & 0x3f];
|
||||||
|
output[op++] = alphabet[v & 0x3f];
|
||||||
|
if (do_padding) {
|
||||||
|
output[op++] = '=';
|
||||||
|
output[op++] = '=';
|
||||||
|
}
|
||||||
|
if (do_newline) {
|
||||||
|
if (do_cr) output[op++] = '\r';
|
||||||
|
output[op++] = '\n';
|
||||||
|
}
|
||||||
|
} else if (p-tailLen == len-2) {
|
||||||
|
int t = 0;
|
||||||
|
v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) |
|
||||||
|
(((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2);
|
||||||
|
tailLen -= t;
|
||||||
|
output[op++] = alphabet[(v >> 12) & 0x3f];
|
||||||
|
output[op++] = alphabet[(v >> 6) & 0x3f];
|
||||||
|
output[op++] = alphabet[v & 0x3f];
|
||||||
|
if (do_padding) {
|
||||||
|
output[op++] = '=';
|
||||||
|
}
|
||||||
|
if (do_newline) {
|
||||||
|
if (do_cr) output[op++] = '\r';
|
||||||
|
output[op++] = '\n';
|
||||||
|
}
|
||||||
|
} else if (do_newline && op > 0 && count != LINE_GROUPS) {
|
||||||
|
if (do_cr) output[op++] = '\r';
|
||||||
|
output[op++] = '\n';
|
||||||
|
}
|
||||||
|
assert tailLen == 0;
|
||||||
|
assert p == len;
|
||||||
|
} else {
|
||||||
|
// Save the leftovers in tail to be consumed on the next
|
||||||
|
// call to encodeInternal.
|
||||||
|
if (p == len-1) {
|
||||||
|
tail[tailLen++] = input[p];
|
||||||
|
} else if (p == len-2) {
|
||||||
|
tail[tailLen++] = input[p];
|
||||||
|
tail[tailLen++] = input[p+1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.op = op;
|
||||||
|
this.count = count;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Base64() { } // don't instantiate
|
||||||
|
}
|
||||||
@@ -1,20 +1,13 @@
|
|||||||
package io.socket.engineio.parser;
|
package io.socket.engineio.parser;
|
||||||
|
|
||||||
|
|
||||||
import io.socket.utf8.UTF8;
|
|
||||||
import io.socket.utf8.UTF8Exception;
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class Parser {
|
public class Parser {
|
||||||
|
|
||||||
private static final int MAX_INT_CHAR_LENGTH = String.valueOf(Integer.MAX_VALUE).length();
|
public static final int PROTOCOL = 4;
|
||||||
|
|
||||||
public static final int PROTOCOL = 3;
|
private static final char SEPARATOR = '\u001e';
|
||||||
|
|
||||||
private static final Map<String, Integer> packets = new HashMap<String, Integer>() {{
|
private static final Map<String, Integer> packets = new HashMap<String, Integer>() {{
|
||||||
put(Packet.OPEN, 0);
|
put(Packet.OPEN, 0);
|
||||||
@@ -26,61 +19,38 @@ public class Parser {
|
|||||||
put(Packet.NOOP, 6);
|
put(Packet.NOOP, 6);
|
||||||
}};
|
}};
|
||||||
|
|
||||||
private static final Map<Integer, String> packetslist = new HashMap<Integer, String>();
|
private static final Map<Integer, String> packetslist = new HashMap<>();
|
||||||
static {
|
static {
|
||||||
for (Map.Entry<String, Integer> entry : packets.entrySet()) {
|
for (Map.Entry<String, Integer> entry : packets.entrySet()) {
|
||||||
packetslist.put(entry.getValue(), entry.getKey());
|
packetslist.put(entry.getValue(), entry.getKey());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Packet<String> err = new Packet<String>(Packet.ERROR, "parser error");
|
private static final Packet<String> err = new Packet<String>(Packet.ERROR, "parser error");
|
||||||
|
|
||||||
private static UTF8.Options utf8Options = new UTF8.Options();
|
|
||||||
static {
|
|
||||||
utf8Options.strict = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Parser() {}
|
private Parser() {}
|
||||||
|
|
||||||
public static void encodePacket(Packet packet, EncodeCallback callback) throws UTF8Exception {
|
public static void encodePacket(Packet packet, EncodeCallback callback) {
|
||||||
encodePacket(packet, false, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void encodePacket(Packet packet, boolean utf8encode, EncodeCallback callback) throws UTF8Exception {
|
|
||||||
if (packet.data instanceof byte[]) {
|
if (packet.data instanceof byte[]) {
|
||||||
@SuppressWarnings("unchecked")
|
((EncodeCallback<byte[]>) callback).call(((Packet<byte[]>) packet).data);
|
||||||
Packet<byte[]> packetToEncode = packet;
|
} else {
|
||||||
@SuppressWarnings("unchecked")
|
String type = String.valueOf(packets.get(packet.type));
|
||||||
EncodeCallback<byte[]> callbackToEncode = callback;
|
String content = packet.data != null ? String.valueOf(packet.data) : "";
|
||||||
encodeByteArray(packetToEncode, callbackToEncode);
|
((EncodeCallback<String>) callback).call(type + content);
|
||||||
return;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String encoded = String.valueOf(packets.get(packet.type));
|
private static void encodePacketAsBase64(Packet packet, EncodeCallback<String> callback) {
|
||||||
|
if (packet.data instanceof byte[]) {
|
||||||
if (null != packet.data) {
|
byte[] data = ((Packet<byte[]>) packet).data;
|
||||||
encoded += utf8encode ? UTF8.encode(String.valueOf(packet.data), utf8Options) : String.valueOf(packet.data);
|
String value = "b" + Base64.encodeToString(data, Base64.DEFAULT);
|
||||||
|
callback.call(value);
|
||||||
|
} else {
|
||||||
|
encodePacket(packet, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
EncodeCallback<String> tempCallback = callback;
|
|
||||||
tempCallback.call(encoded);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void encodeByteArray(Packet<byte[]> packet, EncodeCallback<byte[]> callback) {
|
|
||||||
byte[] data = packet.data;
|
|
||||||
byte[] resultArray = new byte[1 + data.length];
|
|
||||||
resultArray[0] = packets.get(packet.type).byteValue();
|
|
||||||
System.arraycopy(data, 0, resultArray, 1, data.length);
|
|
||||||
callback.call(resultArray);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Packet<String> decodePacket(String data) {
|
public static Packet<String> decodePacket(String data) {
|
||||||
return decodePacket(data, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Packet<String> decodePacket(String data, boolean utf8decode) {
|
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
@@ -92,14 +62,6 @@ public class Parser {
|
|||||||
type = -1;
|
type = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (utf8decode) {
|
|
||||||
try {
|
|
||||||
data = UTF8.decode(data, utf8Options);
|
|
||||||
} catch (UTF8Exception e) {
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type < 0 || type >= packetslist.size()) {
|
if (type < 0 || type >= packetslist.size()) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
@@ -111,23 +73,23 @@ public class Parser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Packet decodeBase64Packet(String data) {
|
||||||
|
if (data == null) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.charAt(0) == 'b') {
|
||||||
|
return new Packet(Packet.MESSAGE, Base64.decode(data.substring(1), Base64.DEFAULT));
|
||||||
|
} else {
|
||||||
|
return decodePacket(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static Packet<byte[]> decodePacket(byte[] data) {
|
public static Packet<byte[]> decodePacket(byte[] data) {
|
||||||
int type = data[0];
|
return new Packet<>(Packet.MESSAGE, data);
|
||||||
byte[] intArray = new byte[data.length - 1];
|
|
||||||
System.arraycopy(data, 1, intArray, 0, intArray.length);
|
|
||||||
return new Packet<byte[]>(packetslist.get(type), intArray);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void encodePayload(Packet[] packets, EncodeCallback callback) throws UTF8Exception {
|
|
||||||
for (Packet packet : packets) {
|
|
||||||
if (packet.data instanceof byte[]) {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
EncodeCallback<byte[]> _callback = (EncodeCallback<byte[]>) callback;
|
|
||||||
encodePayloadAsBinary(packets, _callback);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void encodePayload(Packet[] packets, EncodeCallback<String> callback) {
|
||||||
if (packets.length == 0) {
|
if (packets.length == 0) {
|
||||||
callback.call("0:");
|
callback.call("0:");
|
||||||
return;
|
return;
|
||||||
@@ -135,11 +97,15 @@ public class Parser {
|
|||||||
|
|
||||||
final StringBuilder result = new StringBuilder();
|
final StringBuilder result = new StringBuilder();
|
||||||
|
|
||||||
for (Packet packet : packets) {
|
for (int i = 0, l = packets.length; i < l; i++) {
|
||||||
encodePacket(packet, false, new EncodeCallback() {
|
final boolean isLast = i == l - 1;
|
||||||
|
encodePacketAsBase64(packets[i], new EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(Object message) {
|
public void call(String message) {
|
||||||
result.append(setLengthHeader((String)message));
|
result.append(message);
|
||||||
|
if (!isLast) {
|
||||||
|
result.append(SEPARATOR);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -147,218 +113,36 @@ public class Parser {
|
|||||||
callback.call(result.toString());
|
callback.call(result.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String setLengthHeader(String message) {
|
|
||||||
return message.length() + ":" + message;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void encodePayloadAsBinary(Packet[] packets, EncodeCallback<byte[]> callback) throws UTF8Exception {
|
|
||||||
if (packets.length == 0) {
|
|
||||||
callback.call(new byte[0]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final ArrayList<byte[]> results = new ArrayList<byte[]>(packets.length);
|
|
||||||
|
|
||||||
for (Packet packet : packets) {
|
|
||||||
encodeOneBinaryPacket(packet, new EncodeCallback<byte[]>() {
|
|
||||||
@Override
|
|
||||||
public void call(byte[] data) {
|
|
||||||
results.add(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
callback.call(Buffer.concat(results.toArray(new byte[results.size()][])));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void encodeOneBinaryPacket(Packet p, final EncodeCallback<byte[]> doneCallback) throws UTF8Exception {
|
|
||||||
encodePacket(p, true, new EncodeCallback() {
|
|
||||||
@Override
|
|
||||||
public void call(Object packet) {
|
|
||||||
if (packet instanceof String) {
|
|
||||||
String encodingLength = String.valueOf(((String) packet).length());
|
|
||||||
byte[] sizeBuffer = new byte[encodingLength.length() + 2];
|
|
||||||
|
|
||||||
sizeBuffer[0] = (byte)0; // is a string
|
|
||||||
for (int i = 0; i < encodingLength.length(); i ++) {
|
|
||||||
sizeBuffer[i + 1] = (byte)Character.getNumericValue(encodingLength.charAt(i));
|
|
||||||
}
|
|
||||||
sizeBuffer[sizeBuffer.length - 1] = (byte)255;
|
|
||||||
doneCallback.call(Buffer.concat(new byte[][] {sizeBuffer, stringToByteArray((String)packet)}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String encodingLength = String.valueOf(((byte[])packet).length);
|
|
||||||
byte[] sizeBuffer = new byte[encodingLength.length() + 2];
|
|
||||||
sizeBuffer[0] = (byte)1; // is binary
|
|
||||||
for (int i = 0; i < encodingLength.length(); i ++) {
|
|
||||||
sizeBuffer[i + 1] = (byte)Character.getNumericValue(encodingLength.charAt(i));
|
|
||||||
}
|
|
||||||
sizeBuffer[sizeBuffer.length - 1] = (byte)255;
|
|
||||||
doneCallback.call(Buffer.concat(new byte[][] {sizeBuffer, (byte[])packet}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void decodePayload(String data, DecodePayloadCallback<String> callback) {
|
public static void decodePayload(String data, DecodePayloadCallback<String> callback) {
|
||||||
if (data == null || data.length() == 0) {
|
if (data == null || data.length() == 0) {
|
||||||
callback.call(err, 0, 1);
|
callback.call(err, 0, 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StringBuilder length = new StringBuilder();
|
String[] messages = data.split(String.valueOf(SEPARATOR));
|
||||||
for (int i = 0, l = data.length(); i < l; i++) {
|
|
||||||
char chr = data.charAt(i);
|
|
||||||
|
|
||||||
if (':' != chr) {
|
for (int i = 0, l = messages.length; i < l; i++) {
|
||||||
length.append(chr);
|
Packet<String> packet = decodeBase64Packet(messages[i]);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
int n;
|
|
||||||
try {
|
|
||||||
n = Integer.parseInt(length.toString());
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
callback.call(err, 0, 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String msg;
|
|
||||||
try {
|
|
||||||
msg = data.substring(i + 1, i + 1 + n);
|
|
||||||
} catch (IndexOutOfBoundsException e) {
|
|
||||||
callback.call(err, 0, 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.length() != 0) {
|
|
||||||
Packet<String> packet = decodePacket(msg, false);
|
|
||||||
if (err.type.equals(packet.type) && err.data.equals(packet.data)) {
|
if (err.type.equals(packet.type) && err.data.equals(packet.data)) {
|
||||||
callback.call(err, 0, 1);
|
callback.call(err, 0, 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean ret = callback.call(packet, i + n, l);
|
boolean ret = callback.call(packet, i, l);
|
||||||
if (!ret) {
|
if (!ret) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i += n;
|
|
||||||
length = new StringBuilder();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (length.length() > 0) {
|
public interface EncodeCallback<T> {
|
||||||
callback.call(err, 0, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void decodePayload(byte[] data, DecodePayloadCallback callback) {
|
void call(T data);
|
||||||
ByteBuffer bufferTail = ByteBuffer.wrap(data);
|
|
||||||
List<Object> buffers = new ArrayList<Object>();
|
|
||||||
|
|
||||||
while (bufferTail.capacity() > 0) {
|
|
||||||
StringBuilder strLen = new StringBuilder();
|
|
||||||
boolean isString = (bufferTail.get(0) & 0xFF) == 0;
|
|
||||||
for (int i = 1; ; i++) {
|
|
||||||
int b = bufferTail.get(i) & 0xFF;
|
|
||||||
if (b == 255) break;
|
|
||||||
// supports only integer
|
|
||||||
if (strLen.length() > MAX_INT_CHAR_LENGTH) {
|
|
||||||
callback.call(err, 0, 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
strLen.append(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
bufferTail.position(strLen.length() + 1);
|
|
||||||
bufferTail = bufferTail.slice();
|
|
||||||
|
|
||||||
int msgLength = Integer.parseInt(strLen.toString());
|
|
||||||
|
|
||||||
bufferTail.position(1);
|
|
||||||
bufferTail.limit(msgLength + 1);
|
|
||||||
byte[] msg = new byte[bufferTail.remaining()];
|
|
||||||
bufferTail.get(msg);
|
|
||||||
if (isString) {
|
|
||||||
buffers.add(byteArrayToString(msg));
|
|
||||||
} else {
|
|
||||||
buffers.add(msg);
|
|
||||||
}
|
|
||||||
bufferTail.clear();
|
|
||||||
bufferTail.position(msgLength + 1);
|
|
||||||
bufferTail = bufferTail.slice();
|
|
||||||
}
|
|
||||||
|
|
||||||
int total = buffers.size();
|
|
||||||
for (int i = 0; i < total; i++) {
|
|
||||||
Object buffer = buffers.get(i);
|
|
||||||
if (buffer instanceof String) {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
DecodePayloadCallback<String> tempCallback = callback;
|
|
||||||
tempCallback.call(decodePacket((String)buffer, true), i, total);
|
|
||||||
} else if (buffer instanceof byte[]) {
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
DecodePayloadCallback<byte[]> tempCallback = callback;
|
|
||||||
tempCallback.call(decodePacket((byte[])buffer), i, total);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String byteArrayToString(byte[] bytes) {
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
for (byte b : bytes) {
|
|
||||||
builder.appendCodePoint(b & 0xFF);
|
|
||||||
}
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] stringToByteArray(String string) {
|
|
||||||
int len = string.length();
|
|
||||||
byte[] bytes = new byte[len];
|
|
||||||
for (int i = 0; i < len; i++) {
|
|
||||||
bytes[i] = (byte)Character.codePointAt(string, i);
|
|
||||||
}
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static interface EncodeCallback<T> {
|
|
||||||
|
|
||||||
public void call(T data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static interface DecodePayloadCallback<T> {
|
public interface DecodePayloadCallback<T> {
|
||||||
|
|
||||||
public boolean call(Packet<T> packet, int index, int total);
|
boolean call(Packet<T> packet, int index, int total);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Buffer {
|
|
||||||
|
|
||||||
private Buffer() {}
|
|
||||||
|
|
||||||
public static byte[] concat(byte[][] list) {
|
|
||||||
int length = 0;
|
|
||||||
for (byte[] buf : list) {
|
|
||||||
length += buf.length;
|
|
||||||
}
|
|
||||||
return concat(list, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] concat(byte[][] list, int length) {
|
|
||||||
if (list.length == 0) {
|
|
||||||
return new byte[0];
|
|
||||||
} else if (list.length == 1) {
|
|
||||||
return list[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(length);
|
|
||||||
for (byte[] buf : list) {
|
|
||||||
buffer.put(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
return buffer.array();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,199 +0,0 @@
|
|||||||
package io.socket.utf8;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UTF-8 encoder/decoder ported from utf8.js.
|
|
||||||
*
|
|
||||||
* @see <a href="https://github.com/mathiasbynens/utf8.js">https://github.com/mathiasbynens/utf8.js</a>
|
|
||||||
*/
|
|
||||||
public final class UTF8 {
|
|
||||||
|
|
||||||
private static final String INVALID_CONTINUATION_BYTE = "Invalid continuation byte";
|
|
||||||
private static int[] byteArray;
|
|
||||||
private static int byteCount;
|
|
||||||
private static int byteIndex;
|
|
||||||
|
|
||||||
private UTF8 () {}
|
|
||||||
|
|
||||||
public static String encode(String string) throws UTF8Exception {
|
|
||||||
return encode(string, new Options());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String encode(String string, Options opts) throws UTF8Exception {
|
|
||||||
boolean strict = opts.strict;
|
|
||||||
|
|
||||||
int[] codePoints = ucs2decode(string);
|
|
||||||
int length = codePoints.length;
|
|
||||||
int index = -1;
|
|
||||||
int codePoint;
|
|
||||||
StringBuilder byteString = new StringBuilder();
|
|
||||||
while (++index < length) {
|
|
||||||
codePoint = codePoints[index];
|
|
||||||
byteString.append(encodeCodePoint(codePoint, strict));
|
|
||||||
}
|
|
||||||
return byteString.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String decode(String byteString) throws UTF8Exception {
|
|
||||||
return decode(byteString, new Options());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String decode(String byteString, Options opts) throws UTF8Exception {
|
|
||||||
boolean strict = opts.strict;
|
|
||||||
|
|
||||||
byteArray = ucs2decode(byteString);
|
|
||||||
byteCount = byteArray.length;
|
|
||||||
byteIndex = 0;
|
|
||||||
List<Integer> codePoints = new ArrayList<Integer>();
|
|
||||||
int tmp;
|
|
||||||
while ((tmp = decodeSymbol(strict)) != -1) {
|
|
||||||
codePoints.add(tmp);
|
|
||||||
}
|
|
||||||
return ucs2encode(listToArray(codePoints));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int[] ucs2decode(String string) {
|
|
||||||
int length = string.length();
|
|
||||||
int[] output = new int[string.codePointCount(0, length)];
|
|
||||||
int counter = 0;
|
|
||||||
int value;
|
|
||||||
for (int i = 0; i < length; i += Character.charCount(value)) {
|
|
||||||
value = string.codePointAt(i);
|
|
||||||
output[counter++] = value;
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String encodeCodePoint(int codePoint, boolean strict) throws UTF8Exception {
|
|
||||||
StringBuilder symbol = new StringBuilder();
|
|
||||||
if ((codePoint & 0xFFFFFF80) == 0) {
|
|
||||||
return symbol.append(Character.toChars(codePoint)).toString();
|
|
||||||
}
|
|
||||||
if ((codePoint & 0xFFFFF800) == 0) {
|
|
||||||
symbol.append(Character.toChars(((codePoint >> 6) & 0x1F) | 0xC0));
|
|
||||||
} else if ((codePoint & 0xFFFF0000) == 0) {
|
|
||||||
if (!checkScalarValue(codePoint, strict)) {
|
|
||||||
codePoint = 0xFFFD;
|
|
||||||
}
|
|
||||||
symbol.append(Character.toChars(((codePoint >> 12) & 0x0F) | 0xE0));
|
|
||||||
symbol.append(createByte(codePoint, 6));
|
|
||||||
} else if ((codePoint & 0xFFE00000) == 0) {
|
|
||||||
symbol.append(Character.toChars(((codePoint >> 18) & 0x07) | 0xF0));
|
|
||||||
symbol.append(createByte(codePoint, 12));
|
|
||||||
symbol.append(createByte(codePoint, 6));
|
|
||||||
}
|
|
||||||
symbol.append(Character.toChars((codePoint & 0x3F) | 0x80));
|
|
||||||
return symbol.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static char[] createByte(int codePoint, int shift) {
|
|
||||||
return Character.toChars(((codePoint >> shift) & 0x3F) | 0x80);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int decodeSymbol(boolean strict) throws UTF8Exception {
|
|
||||||
int byte1;
|
|
||||||
int byte2;
|
|
||||||
int byte3;
|
|
||||||
int byte4;
|
|
||||||
int codePoint;
|
|
||||||
|
|
||||||
if (byteIndex > byteCount) {
|
|
||||||
throw new UTF8Exception("Invalid byte index");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (byteIndex == byteCount) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte1 = byteArray[byteIndex] & 0xFF;
|
|
||||||
byteIndex++;
|
|
||||||
|
|
||||||
if ((byte1 & 0x80) == 0) {
|
|
||||||
return byte1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((byte1 & 0xE0) == 0xC0) {
|
|
||||||
byte2 = readContinuationByte();
|
|
||||||
codePoint = ((byte1 & 0x1F) << 6) | byte2;
|
|
||||||
if (codePoint >= 0x80) {
|
|
||||||
return codePoint;
|
|
||||||
} else {
|
|
||||||
throw new UTF8Exception(INVALID_CONTINUATION_BYTE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((byte1 & 0xF0) == 0xE0) {
|
|
||||||
byte2 = readContinuationByte();
|
|
||||||
byte3 = readContinuationByte();
|
|
||||||
codePoint = ((byte1 & 0x0F) << 12) | (byte2 << 6) | byte3;
|
|
||||||
if (codePoint >= 0x0800) {
|
|
||||||
return checkScalarValue(codePoint, strict) ? codePoint : 0xFFFD;
|
|
||||||
} else {
|
|
||||||
throw new UTF8Exception(INVALID_CONTINUATION_BYTE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((byte1 & 0xF8) == 0xF0) {
|
|
||||||
byte2 = readContinuationByte();
|
|
||||||
byte3 = readContinuationByte();
|
|
||||||
byte4 = readContinuationByte();
|
|
||||||
codePoint = ((byte1 & 0x0F) << 0x12) | (byte2 << 0x0C) | (byte3 << 0x06) | byte4;
|
|
||||||
if (codePoint >= 0x010000 && codePoint <= 0x10FFFF) {
|
|
||||||
return codePoint;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new UTF8Exception(INVALID_CONTINUATION_BYTE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int readContinuationByte() throws UTF8Exception {
|
|
||||||
if (byteIndex >= byteCount) {
|
|
||||||
throw new UTF8Exception("Invalid byte index");
|
|
||||||
}
|
|
||||||
|
|
||||||
int continuationByte = byteArray[byteIndex] & 0xFF;
|
|
||||||
byteIndex++;
|
|
||||||
|
|
||||||
if ((continuationByte & 0xC0) == 0x80) {
|
|
||||||
return continuationByte & 0x3F;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new UTF8Exception(INVALID_CONTINUATION_BYTE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String ucs2encode(int[] array) {
|
|
||||||
StringBuilder output = new StringBuilder();
|
|
||||||
for (int value : array) {
|
|
||||||
output.appendCodePoint(value);
|
|
||||||
}
|
|
||||||
return output.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean checkScalarValue(int codePoint, boolean strict) throws UTF8Exception {
|
|
||||||
if (codePoint >= 0xD800 && codePoint <= 0xDFFF) {
|
|
||||||
if (strict) {
|
|
||||||
throw new UTF8Exception(
|
|
||||||
"Lone surrogate U+" + Integer.toHexString(codePoint).toUpperCase() +
|
|
||||||
" is not a scalar value"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int[] listToArray(List<Integer> list) {
|
|
||||||
int size = list.size();
|
|
||||||
int[] array = new int[size];
|
|
||||||
for (int i = 0; i < size; i++) {
|
|
||||||
array[i] = list.get(i);
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Options {
|
|
||||||
public boolean strict = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package io.socket.utf8;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class UTF8Exception extends IOException {
|
|
||||||
|
|
||||||
public UTF8Exception() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public UTF8Exception(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public UTF8Exception(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
|
|
||||||
public UTF8Exception(Throwable cause) {
|
|
||||||
super(cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -242,4 +242,20 @@ public class ConnectionTest extends Connection {
|
|||||||
socket.open();
|
socket.open();
|
||||||
assertThat((Integer) values.take(), is(0));
|
assertThat((Integer) values.take(), is(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test(timeout = TIMEOUT)
|
||||||
|
public void receivePing() throws InterruptedException {
|
||||||
|
final BlockingQueue<String> values = new LinkedBlockingQueue<>();
|
||||||
|
|
||||||
|
socket = new Socket(createOptions());
|
||||||
|
socket.on(Socket.EVENT_PING, new Emitter.Listener() {
|
||||||
|
@Override
|
||||||
|
public void call(Object... args) {
|
||||||
|
values.offer("end");
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
socket.open();
|
||||||
|
assertThat(values.take(), is("end"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package io.socket.engineio.parser;
|
package io.socket.engineio.parser;
|
||||||
|
|
||||||
import io.socket.utf8.UTF8Exception;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.junit.runners.JUnit4;
|
import org.junit.runners.JUnit4;
|
||||||
@@ -15,7 +14,7 @@ public class ParserTest {
|
|||||||
static final String ERROR_DATA = "parser error";
|
static final String ERROR_DATA = "parser error";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void encodeAsString() throws UTF8Exception {
|
public void encodeAsString() {
|
||||||
encodePacket(new Packet<String>(Packet.MESSAGE, "test"), new EncodeCallback<String>() {
|
encodePacket(new Packet<String>(Packet.MESSAGE, "test"), new EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(String data) {
|
public void call(String data) {
|
||||||
@@ -25,7 +24,7 @@ public class ParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeAsPacket() throws UTF8Exception {
|
public void decodeAsPacket() {
|
||||||
encodePacket(new Packet<String>(Packet.MESSAGE, "test"), new EncodeCallback<String>() {
|
encodePacket(new Packet<String>(Packet.MESSAGE, "test"), new EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(String data) {
|
public void call(String data) {
|
||||||
@@ -35,7 +34,7 @@ public class ParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void noData() throws UTF8Exception {
|
public void noData() {
|
||||||
encodePacket(new Packet(Packet.MESSAGE), new EncodeCallback<String>() {
|
encodePacket(new Packet(Packet.MESSAGE), new EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(String data) {
|
public void call(String data) {
|
||||||
@@ -47,7 +46,7 @@ public class ParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void encodeOpenPacket() throws UTF8Exception {
|
public void encodeOpenPacket() {
|
||||||
encodePacket(new Packet<String>(Packet.OPEN, "{\"some\":\"json\"}"), new EncodeCallback<String>() {
|
encodePacket(new Packet<String>(Packet.OPEN, "{\"some\":\"json\"}"), new EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(String data) {
|
public void call(String data) {
|
||||||
@@ -59,7 +58,7 @@ public class ParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void encodeClosePacket() throws UTF8Exception {
|
public void encodeClosePacket() {
|
||||||
encodePacket(new Packet<String>(Packet.CLOSE), new EncodeCallback<String>() {
|
encodePacket(new Packet<String>(Packet.CLOSE), new EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(String data) {
|
public void call(String data) {
|
||||||
@@ -70,7 +69,7 @@ public class ParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void encodePingPacket() throws UTF8Exception {
|
public void encodePingPacket() {
|
||||||
encodePacket(new Packet<String>(Packet.PING, "1"), new EncodeCallback<String>() {
|
encodePacket(new Packet<String>(Packet.PING, "1"), new EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(String data) {
|
public void call(String data) {
|
||||||
@@ -82,7 +81,7 @@ public class ParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void encodePongPacket() throws UTF8Exception {
|
public void encodePongPacket() {
|
||||||
encodePacket(new Packet<String>(Packet.PONG, "1"), new EncodeCallback<String>() {
|
encodePacket(new Packet<String>(Packet.PONG, "1"), new EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(String data) {
|
public void call(String data) {
|
||||||
@@ -94,7 +93,7 @@ public class ParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void encodeMessagePacket() throws UTF8Exception {
|
public void encodeMessagePacket() {
|
||||||
encodePacket(new Packet<String>(Packet.MESSAGE, "aaa"), new EncodeCallback<String>() {
|
encodePacket(new Packet<String>(Packet.MESSAGE, "aaa"), new EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(String data) {
|
public void call(String data) {
|
||||||
@@ -106,7 +105,7 @@ public class ParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void encodeUTF8SpecialCharsMessagePacket() throws UTF8Exception {
|
public void encodeUTF8SpecialCharsMessagePacket() {
|
||||||
encodePacket(new Packet<String>(Packet.MESSAGE, "utf8 — string"), new EncodeCallback<String>() {
|
encodePacket(new Packet<String>(Packet.MESSAGE, "utf8 — string"), new EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(String data) {
|
public void call(String data) {
|
||||||
@@ -118,7 +117,7 @@ public class ParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void encodeMessagePacketCoercingToString() throws UTF8Exception {
|
public void encodeMessagePacketCoercingToString() {
|
||||||
encodePacket(new Packet<Integer>(Packet.MESSAGE, 1), new EncodeCallback<String>() {
|
encodePacket(new Packet<Integer>(Packet.MESSAGE, 1), new EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(String data) {
|
public void call(String data) {
|
||||||
@@ -130,7 +129,7 @@ public class ParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void encodeUpgradePacket() throws UTF8Exception {
|
public void encodeUpgradePacket() {
|
||||||
encodePacket(new Packet<String>(Packet.UPGRADE), new EncodeCallback<String>() {
|
encodePacket(new Packet<String>(Packet.UPGRADE), new EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(String data) {
|
public void call(String data) {
|
||||||
@@ -141,7 +140,7 @@ public class ParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void encodingFormat() throws UTF8Exception {
|
public void encodingFormat() {
|
||||||
encodePacket(new Packet<String>(Packet.MESSAGE, "test"), new EncodeCallback<String>() {
|
encodePacket(new Packet<String>(Packet.MESSAGE, "test"), new EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(String data) {
|
public void call(String data) {
|
||||||
@@ -156,19 +155,6 @@ public class ParserTest {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void encodingStringMessageWithLoneSurrogatesReplacedByUFFFD() throws UTF8Exception {
|
|
||||||
String data = "\uDC00\uD834\uDF06\uDC00 \uD800\uD835\uDF07\uD800";
|
|
||||||
encodePacket(new Packet<String>(Packet.MESSAGE, data), true, new EncodeCallback<String>() {
|
|
||||||
@Override
|
|
||||||
public void call(String encoded) {
|
|
||||||
Packet<String> p = decodePacket(encoded, true);
|
|
||||||
assertThat(p.type, is(Packet.MESSAGE));
|
|
||||||
assertThat(p.data, is("\uFFFD\uD834\uDF06\uFFFD \uFFFD\uD835\uDF07\uFFFD"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeEmptyPayload() {
|
public void decodeEmptyPayload() {
|
||||||
Packet<String> p = decodePacket((String)null);
|
Packet<String> p = decodePacket((String)null);
|
||||||
@@ -191,14 +177,7 @@ public class ParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodeInvalidUTF8() {
|
public void encodePayloads() {
|
||||||
Packet<String> p = decodePacket("4\uffff", true);
|
|
||||||
assertThat(p.type, is(Packet.ERROR));
|
|
||||||
assertThat(p.data, is(ERROR_DATA));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void encodePayloads() throws UTF8Exception {
|
|
||||||
encodePayload(new Packet[]{new Packet(Packet.PING), new Packet(Packet.PONG)}, new EncodeCallback<String>() {
|
encodePayload(new Packet[]{new Packet(Packet.PING), new Packet(Packet.PONG)}, new EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(String data) {
|
public void call(String data) {
|
||||||
@@ -208,7 +187,7 @@ public class ParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void encodeAndDecodePayloads() throws UTF8Exception {
|
public void encodeAndDecodePayloads() {
|
||||||
encodePayload(new Packet[] {new Packet<String>(Packet.MESSAGE, "a")}, new EncodeCallback<String>() {
|
encodePayload(new Packet[] {new Packet<String>(Packet.MESSAGE, "a")}, new EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(String data) {
|
public void call(String data) {
|
||||||
@@ -242,7 +221,7 @@ public class ParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void encodeAndDecodeEmptyPayloads() throws UTF8Exception {
|
public void encodeAndDecodeEmptyPayloads() {
|
||||||
encodePayload(new Packet[] {}, new EncodeCallback<String>() {
|
encodePayload(new Packet[] {}, new EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(String data) {
|
public void call(String data) {
|
||||||
@@ -260,30 +239,20 @@ public class ParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void notUTF8EncodeWhenDealingWithStringsOnly() throws UTF8Exception {
|
public void notUTF8EncodeWhenDealingWithStringsOnly() {
|
||||||
encodePayload(new Packet[] {
|
encodePayload(new Packet[] {
|
||||||
new Packet(Packet.MESSAGE, "€€€"),
|
new Packet(Packet.MESSAGE, "€€€"),
|
||||||
new Packet(Packet.MESSAGE, "α")
|
new Packet(Packet.MESSAGE, "α")
|
||||||
}, new EncodeCallback<String>() {
|
}, new EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(String data) {
|
public void call(String data) {
|
||||||
assertThat(data, is("4:4€€€2:4α"));
|
assertThat(data, is("4€€€\u001e4α"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodePayloadBadFormat() {
|
public void decodePayloadBadFormat() {
|
||||||
decodePayload("1!", new DecodePayloadCallback<String>() {
|
|
||||||
@Override
|
|
||||||
public boolean call(Packet<String> packet, int index, int total) {
|
|
||||||
boolean isLast = index + 1 == total;
|
|
||||||
assertThat(packet.type, is(Packet.ERROR));
|
|
||||||
assertThat(packet.data, is(ERROR_DATA));
|
|
||||||
assertThat(isLast, is(true));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
decodePayload("", new DecodePayloadCallback<String>() {
|
decodePayload("", new DecodePayloadCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean call(Packet<String> packet, int index, int total) {
|
public boolean call(Packet<String> packet, int index, int total) {
|
||||||
@@ -306,23 +275,9 @@ public class ParserTest {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void decodePayloadBadLength() {
|
|
||||||
decodePayload("1:", new DecodePayloadCallback<String>() {
|
|
||||||
@Override
|
|
||||||
public boolean call(Packet<String> packet, int index, int total) {
|
|
||||||
boolean isLast = index + 1 == total;
|
|
||||||
assertThat(packet.type, is(Packet.ERROR));
|
|
||||||
assertThat(packet.data, is(ERROR_DATA));
|
|
||||||
assertThat(isLast, is(true));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void decodePayloadBadPacketFormat() {
|
public void decodePayloadBadPacketFormat() {
|
||||||
decodePayload("3:99:", new DecodePayloadCallback<String>() {
|
decodePayload("99:", new DecodePayloadCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean call(Packet<String> packet, int index, int total) {
|
public boolean call(Packet<String> packet, int index, int total) {
|
||||||
boolean isLast = index + 1 == total;
|
boolean isLast = index + 1 == total;
|
||||||
@@ -332,17 +287,7 @@ public class ParserTest {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
decodePayload("1:aa", new DecodePayloadCallback<String>() {
|
decodePayload("aa", new DecodePayloadCallback<String>() {
|
||||||
@Override
|
|
||||||
public boolean call(Packet<String> packet, int index, int total) {
|
|
||||||
boolean isLast = index + 1 == total;
|
|
||||||
assertThat(packet.type, is(Packet.ERROR));
|
|
||||||
assertThat(packet.data, is(ERROR_DATA));
|
|
||||||
assertThat(isLast, is(true));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
decodePayload("1:a2:b", new DecodePayloadCallback<String>() {
|
|
||||||
@Override
|
@Override
|
||||||
public boolean call(Packet<String> packet, int index, int total) {
|
public boolean call(Packet<String> packet, int index, int total) {
|
||||||
boolean isLast = index + 1 == total;
|
boolean isLast = index + 1 == total;
|
||||||
@@ -355,7 +300,7 @@ public class ParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void encodeBinaryMessage() throws UTF8Exception {
|
public void encodeBinaryMessage() {
|
||||||
final byte[] data = new byte[5];
|
final byte[] data = new byte[5];
|
||||||
for (int i = 0; i < data.length; i++) {
|
for (int i = 0; i < data.length; i++) {
|
||||||
data[0] = (byte)i;
|
data[0] = (byte)i;
|
||||||
@@ -371,7 +316,7 @@ public class ParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void encodeBinaryContents() throws UTF8Exception {
|
public void encodeBinaryContents() {
|
||||||
final byte[] firstBuffer = new byte[5];
|
final byte[] firstBuffer = new byte[5];
|
||||||
for (int i = 0 ; i < firstBuffer.length; i++) {
|
for (int i = 0 ; i < firstBuffer.length; i++) {
|
||||||
firstBuffer[0] = (byte)i;
|
firstBuffer[0] = (byte)i;
|
||||||
@@ -384,9 +329,9 @@ public class ParserTest {
|
|||||||
encodePayload(new Packet[]{
|
encodePayload(new Packet[]{
|
||||||
new Packet<byte[]>(Packet.MESSAGE, firstBuffer),
|
new Packet<byte[]>(Packet.MESSAGE, firstBuffer),
|
||||||
new Packet<byte[]>(Packet.MESSAGE, secondBuffer),
|
new Packet<byte[]>(Packet.MESSAGE, secondBuffer),
|
||||||
}, new EncodeCallback<byte[]>() {
|
}, new EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(byte[] data) {
|
public void call(String data) {
|
||||||
decodePayload(data, new DecodePayloadCallback() {
|
decodePayload(data, new DecodePayloadCallback() {
|
||||||
@Override
|
@Override
|
||||||
public boolean call(Packet packet, int index, int total) {
|
public boolean call(Packet packet, int index, int total) {
|
||||||
@@ -405,7 +350,7 @@ public class ParserTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void encodeMixedBinaryAndStringContents() throws UTF8Exception {
|
public void encodeMixedBinaryAndStringContents() {
|
||||||
final byte[] firstBuffer = new byte[123];
|
final byte[] firstBuffer = new byte[123];
|
||||||
for (int i = 0 ; i < firstBuffer.length; i++) {
|
for (int i = 0 ; i < firstBuffer.length; i++) {
|
||||||
firstBuffer[0] = (byte)i;
|
firstBuffer[0] = (byte)i;
|
||||||
@@ -414,9 +359,9 @@ public class ParserTest {
|
|||||||
new Packet<byte[]>(Packet.MESSAGE, firstBuffer),
|
new Packet<byte[]>(Packet.MESSAGE, firstBuffer),
|
||||||
new Packet<String>(Packet.MESSAGE, "hello"),
|
new Packet<String>(Packet.MESSAGE, "hello"),
|
||||||
new Packet<String>(Packet.CLOSE),
|
new Packet<String>(Packet.CLOSE),
|
||||||
}, new EncodeCallback<byte[]>() {
|
}, new EncodeCallback<String>() {
|
||||||
@Override
|
@Override
|
||||||
public void call(byte[] encoded) {
|
public void call(String encoded) {
|
||||||
decodePayload(encoded, new DecodePayloadCallback() {
|
decodePayload(encoded, new DecodePayloadCallback() {
|
||||||
@Override
|
@Override
|
||||||
public boolean call(Packet packet, int index, int total) {
|
public boolean call(Packet packet, int index, int total) {
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
package io.socket.utf8;
|
|
||||||
|
|
||||||
import org.junit.Rule;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.rules.ExpectedException;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
import org.junit.runners.JUnit4;
|
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
|
||||||
import static org.junit.Assert.assertThat;
|
|
||||||
|
|
||||||
@RunWith(JUnit4.class)
|
|
||||||
public class UTF8Test {
|
|
||||||
private static final Data[] DATA = new Data[] {
|
|
||||||
// 1-byte
|
|
||||||
new Data(0x0000, "\u0000", "\u0000"),
|
|
||||||
new Data(0x005c, "\u005C\u005C", "\u005C\u005C"), // = backslash
|
|
||||||
new Data(0x007f, "\u007F", "\u007F"),
|
|
||||||
// 2-byte
|
|
||||||
new Data(0x0080, "\u0080", "\u00C2\u0080"),
|
|
||||||
new Data(0x05CA, "\u05CA", "\u00D7\u008A"),
|
|
||||||
new Data(0x07FF, "\u07FF", "\u00DF\u00BF"),
|
|
||||||
// 3-byte
|
|
||||||
new Data(0x0800, "\u0800", "\u00E0\u00A0\u0080"),
|
|
||||||
new Data(0x2C3C, "\u2C3C", "\u00E2\u00B0\u00BC"),
|
|
||||||
new Data(0x07FF, "\uFFFF", "\u00EF\u00BF\u00BF"),
|
|
||||||
// unmatched surrogate halves
|
|
||||||
// high surrogates: 0xD800 to 0xDBFF
|
|
||||||
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", true),
|
|
||||||
new Data("High surrogate followed by a symbol that is not a surrogate",
|
|
||||||
"\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", 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", true),
|
|
||||||
new Data("Low surrogate followed by another low surrogate",
|
|
||||||
"\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", 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", 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"),
|
|
||||||
new Data(0x010FFF, "\uDBFF\uDFFF", "\u00F4\u008F\u00BF\u00BF"),
|
|
||||||
};
|
|
||||||
|
|
||||||
@Rule
|
|
||||||
public ExpectedException exception = ExpectedException.none();
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void encodeAndDecode() throws UTF8Exception {
|
|
||||||
for (Data data : DATA) {
|
|
||||||
String reason = data.description != null? data.description : "U+" + Integer.toHexString(data.codePoint).toUpperCase();
|
|
||||||
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);
|
|
||||||
UTF8.decode("\uFFFF");
|
|
||||||
|
|
||||||
exception.expect(UTF8Exception.class);
|
|
||||||
UTF8.decode("\u00E9\u0000\u0000");
|
|
||||||
|
|
||||||
exception.expect(UTF8Exception.class);
|
|
||||||
UTF8.decode("\u00C2\uFFFF");
|
|
||||||
|
|
||||||
exception.expect(UTF8Exception.class);
|
|
||||||
UTF8.decode("\u00F0\u009D");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class Data {
|
|
||||||
public int codePoint = -1;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
82
src/test/resources/package-lock.json
generated
82
src/test/resources/package-lock.json
generated
@@ -11,16 +11,6 @@
|
|||||||
"negotiator": "0.6.2"
|
"negotiator": "0.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"after": {
|
|
||||||
"version": "0.8.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
|
|
||||||
"integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
|
|
||||||
},
|
|
||||||
"arraybuffer.slice": {
|
|
||||||
"version": "0.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
|
|
||||||
"integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog=="
|
|
||||||
},
|
|
||||||
"base64-arraybuffer": {
|
"base64-arraybuffer": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz",
|
||||||
@@ -31,15 +21,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
|
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
|
||||||
},
|
},
|
||||||
"blob": {
|
|
||||||
"version": "0.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz",
|
|
||||||
"integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig=="
|
|
||||||
},
|
|
||||||
"cookie": {
|
"cookie": {
|
||||||
"version": "0.3.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
|
||||||
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
|
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
|
||||||
|
},
|
||||||
|
"cors": {
|
||||||
|
"version": "2.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||||
|
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||||
|
"requires": {
|
||||||
|
"object-assign": "^4",
|
||||||
|
"vary": "^1"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
@@ -50,43 +44,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engine.io": {
|
"engine.io": {
|
||||||
"version": "3.4.2",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.0.5.tgz",
|
||||||
"integrity": "sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg==",
|
"integrity": "sha512-Ri+whTNr2PKklxQkfbGjwEo+kCBUM4Qxk4wtLqLrhH+b1up2NFL9g9pjYWiCV/oazwB0rArnvF/ZmZN2ab5Hpg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"accepts": "~1.3.4",
|
"accepts": "~1.3.4",
|
||||||
"base64id": "2.0.0",
|
"base64id": "2.0.0",
|
||||||
"cookie": "0.3.1",
|
"cookie": "~0.4.1",
|
||||||
|
"cors": "~2.8.5",
|
||||||
"debug": "~4.1.0",
|
"debug": "~4.1.0",
|
||||||
"engine.io-parser": "~2.2.0",
|
"engine.io-parser": "~4.0.0",
|
||||||
"ws": "^7.1.2"
|
"ws": "^7.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"engine.io-parser": {
|
"engine.io-parser": {
|
||||||
"version": "2.2.1",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz",
|
||||||
"integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==",
|
"integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"after": "0.8.2",
|
"base64-arraybuffer": "0.1.4"
|
||||||
"arraybuffer.slice": "~0.0.7",
|
|
||||||
"base64-arraybuffer": "0.1.4",
|
|
||||||
"blob": "0.0.5",
|
|
||||||
"has-binary2": "~1.0.2"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"has-binary2": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==",
|
|
||||||
"requires": {
|
|
||||||
"isarray": "2.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"isarray": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
|
|
||||||
"integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
|
|
||||||
},
|
|
||||||
"mime-db": {
|
"mime-db": {
|
||||||
"version": "1.44.0",
|
"version": "1.44.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
|
||||||
@@ -101,15 +79,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
},
|
},
|
||||||
"negotiator": {
|
"negotiator": {
|
||||||
"version": "0.6.2",
|
"version": "0.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
|
||||||
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
|
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
|
||||||
},
|
},
|
||||||
|
"object-assign": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
|
||||||
|
},
|
||||||
|
"vary": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
|
||||||
|
},
|
||||||
"ws": {
|
"ws": {
|
||||||
"version": "7.4.1",
|
"version": "7.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"engine.io": "^3.4.2"
|
"engine.io": "^4.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user