Merge branch 'release/2.0.0'

This commit is contained in:
Christian Basler 2017-09-21 16:29:19 +02:00
commit bc970b47ae
28 changed files with 1273 additions and 1621 deletions

11
CHANGELOG.md Normal file
View File

@ -0,0 +1,11 @@
Changelog
=========
2.0.0
-----
Migrated to Kotlin. If you didn't implement your own `Unpacker`, nothing should change except for the added null safety.
Otherwise, because `is` is a reserved word in Kotlin, the method was renamed to `doesUnpack` for ease of use.
1.0.0
-----
Initial version

View File

@ -4,14 +4,14 @@ Simple MessagePack
[![Javadoc](https://javadoc-emblem.rhcloud.com/doc/ch.dissem.msgpack/msgpack/badge.svg)](http://www.javadoc.io/doc/ch.dissem.msgpack/msgpack) [![Javadoc](https://javadoc-emblem.rhcloud.com/doc/ch.dissem.msgpack/msgpack/badge.svg)](http://www.javadoc.io/doc/ch.dissem.msgpack/msgpack)
[![Apache 2](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](https://raw.githubusercontent.com/Dissem/Jabit/master/LICENSE) [![Apache 2](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](https://raw.githubusercontent.com/Dissem/Jabit/master/LICENSE)
This is a simple Java library for handling MessagePack data. It doesn't do any object mapping, but maps to special This is a simple Kotlin/Java library for handling MessagePack data. It doesn't do any object mapping, but maps to
objects representing MessagePack types. To build, use command `./gradlew build`. special objects representing MessagePack types. To build, use command `./gradlew build`.
For most cases you might be better off using `org.msgpack:msgpack`, but I found that I needed something that generically For most cases you might be better off using `org.msgpack:msgpack`, but I found that I needed something that generically
represents the internal structure of the data. represents the internal structure of the data.
_Simple MessagePack_ uses Semantic Versioning, meaning as long as the major version doesn't change, nothing should break if you _Simple MessagePack_ uses Semantic Versioning, meaning as long as the major version doesn't change, nothing should break
update. Be aware though that this doesn't necessarily applies for SNAPSHOT builds and the development branch. if you update. Be aware though that this doesn't necessarily applies for SNAPSHOT builds and the development branch.
#### Master #### Master

View File

@ -50,7 +50,7 @@ class GitFlowVersion implements Plugin<Project> {
project.ext.isRelease = isRelease(project) project.ext.isRelease = isRelease(project)
project.version = getVersion(project) project.version = getVersion(project)
project.task('version') << { project.task('version').doLast {
println "Version deduced from git: '${project.version}'" println "Version deduced from git: '${project.version}'"
} }
} }

View File

@ -1,6 +1,6 @@
#Tue Jan 17 07:22:12 CET 2017 #Tue Sep 19 21:21:56 CEST 2017
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-bin.zip

2
gradlew vendored
View File

@ -154,7 +154,7 @@ if $cygwin ; then
esac esac
fi fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an value, following the shell quoting and substitution rules # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() { function splitJvmOpts() {
JVM_OPTS=("$@") JVM_OPTS=("$@")
} }

View File

@ -1,65 +0,0 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack;
import ch.dissem.msgpack.types.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList;
import java.util.List;
/**
* Reads MPType object from an {@link InputStream}.
*/
public class Reader {
private LinkedList<MPType.Unpacker<?>> unpackers = new LinkedList<>();
private static final Reader instance = new Reader();
private Reader() {
unpackers.add(new MPNil.Unpacker());
unpackers.add(new MPBoolean.Unpacker());
unpackers.add(new MPInteger.Unpacker());
unpackers.add(new MPFloat.Unpacker());
unpackers.add(new MPString.Unpacker());
unpackers.add(new MPBinary.Unpacker());
unpackers.add(new MPMap.Unpacker(this));
unpackers.add(new MPArray.Unpacker(this));
}
public static Reader getInstance() {
return instance;
}
/**
* Register your own extensions
*/
public void register(MPType.Unpacker<?> unpacker) {
unpackers.addFirst(unpacker);
}
public MPType read(InputStream in) throws IOException {
int firstByte = in.read();
for (MPType.Unpacker<?> unpacker : unpackers) {
if (unpacker.is(firstByte)) {
return unpacker.unpack(firstByte, in);
}
}
throw new IOException(String.format("Unsupported input, no reader for 0x%02x", firstByte));
}
}

View File

@ -1,256 +0,0 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack.types;
import ch.dissem.msgpack.Reader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.*;
/**
* Representation of a msgpack encoded array. Uses a list to represent data internally, and implements the {@link List}
* interface for your convenience.
*
* @param <T>
*/
public class MPArray<T extends MPType> implements MPType<List<T>>, List<T> {
private List<T> array;
public MPArray() {
this.array = new LinkedList<>();
}
public MPArray(List<T> array) {
this.array = array;
}
@SafeVarargs
public MPArray(T... objects) {
this.array = Arrays.asList(objects);
}
public List<T> getValue() {
return array;
}
public void pack(OutputStream out) throws IOException {
int size = array.size();
if (size < 16) {
out.write(0b10010000 + size);
} else if (size < 65536) {
out.write(0xDC);
out.write(ByteBuffer.allocate(2).putShort((short) size).array());
} else {
out.write(0xDD);
out.write(ByteBuffer.allocate(4).putInt(size).array());
}
for (MPType o : array) {
o.pack(out);
}
}
@Override
public int size() {
return array.size();
}
@Override
public boolean isEmpty() {
return array.isEmpty();
}
@Override
public boolean contains(Object o) {
return array.contains(o);
}
@Override
public Iterator<T> iterator() {
return array.iterator();
}
@Override
public Object[] toArray() {
return array.toArray();
}
@Override
@SuppressWarnings("SuspiciousToArrayCall")
public <T1> T1[] toArray(T1[] t1s) {
return array.toArray(t1s);
}
@Override
public boolean add(T t) {
return array.add(t);
}
@Override
public boolean remove(Object o) {
return array.remove(o);
}
@Override
public boolean containsAll(Collection<?> collection) {
return array.containsAll(collection);
}
@Override
public boolean addAll(Collection<? extends T> collection) {
return array.addAll(collection);
}
@Override
public boolean addAll(int i, Collection<? extends T> collection) {
return array.addAll(i, collection);
}
@Override
public boolean removeAll(Collection<?> collection) {
return array.removeAll(collection);
}
@Override
public boolean retainAll(Collection<?> collection) {
return array.retainAll(collection);
}
@Override
public void clear() {
array.clear();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MPArray<?> mpArray = (MPArray<?>) o;
return Objects.equals(array, mpArray.array);
}
@Override
public int hashCode() {
return Objects.hash(array);
}
@Override
public T get(int i) {
return array.get(i);
}
@Override
public T set(int i, T t) {
return array.set(i, t);
}
@Override
public void add(int i, T t) {
array.add(i, t);
}
@Override
public T remove(int i) {
return array.remove(i);
}
@Override
public int indexOf(Object o) {
return array.indexOf(o);
}
@Override
public int lastIndexOf(Object o) {
return array.lastIndexOf(o);
}
@Override
public ListIterator<T> listIterator() {
return array.listIterator();
}
@Override
public ListIterator<T> listIterator(int i) {
return array.listIterator(i);
}
@Override
public List<T> subList(int fromIndex, int toIndex) {
return array.subList(fromIndex, toIndex);
}
@Override
public String toString() {
return toJson();
}
@Override
public String toJson() {
return toJson("");
}
String toJson(String indent) {
StringBuilder result = new StringBuilder();
result.append("[\n");
Iterator<T> iterator = array.iterator();
String indent2 = indent + " ";
while (iterator.hasNext()) {
T item = iterator.next();
result.append(indent2);
result.append(Utils.toJson(item, indent2));
if (iterator.hasNext()) {
result.append(',');
}
result.append('\n');
}
result.append("]");
return result.toString();
}
public static class Unpacker implements MPType.Unpacker<MPArray> {
private final Reader reader;
public Unpacker(Reader reader) {
this.reader = reader;
}
public boolean is(int firstByte) {
return firstByte == 0xDC || firstByte == 0xDD || (firstByte & 0b11110000) == 0b10010000;
}
public MPArray<MPType<?>> unpack(int firstByte, InputStream in) throws IOException {
int size;
if ((firstByte & 0b11110000) == 0b10010000) {
size = firstByte & 0b00001111;
} else if (firstByte == 0xDC) {
size = in.read() << 8 | in.read();
} else if (firstByte == 0xDD) {
size = in.read() << 24 | in.read() << 16 | in.read() << 8 | in.read();
} else {
throw new IllegalArgumentException(String.format("Unexpected first byte 0x%02x", firstByte));
}
List<MPType<?>> list = new LinkedList<>();
for (int i = 0; i < size; i++) {
MPType value = reader.read(in);
list.add(value);
}
return new MPArray<>(list);
}
}
}

View File

@ -1,98 +0,0 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack.types;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import static ch.dissem.msgpack.types.Utils.bytes;
/**
* Representation of msgpack encoded binary data a.k.a. byte array.
*/
public class MPBinary implements MPType<byte[]> {
private byte[] value;
public MPBinary(byte[] value) {
this.value = value;
}
public byte[] getValue() {
return value;
}
public void pack(OutputStream out) throws IOException {
int size = value.length;
if (size < 256) {
out.write(0xC4);
out.write((byte) size);
} else if (size < 65536) {
out.write(0xC5);
out.write(ByteBuffer.allocate(2).putShort((short) size).array());
} else {
out.write(0xC6);
out.write(ByteBuffer.allocate(4).putInt(size).array());
}
out.write(value);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MPBinary mpBinary = (MPBinary) o;
return Arrays.equals(value, mpBinary.value);
}
@Override
public int hashCode() {
return Arrays.hashCode(value);
}
@Override
public String toString() {
return toJson();
}
@Override
public String toJson() {
return Utils.base64(value);
}
public static class Unpacker implements MPType.Unpacker<MPBinary> {
public boolean is(int firstByte) {
return firstByte == 0xC4 || firstByte == 0xC5 || firstByte == 0xC6;
}
public MPBinary unpack(int firstByte, InputStream in) throws IOException {
int size;
if (firstByte == 0xC4) {
size = in.read();
} else if (firstByte == 0xC5) {
size = in.read() << 8 | in.read();
} else if (firstByte == 0xC6) {
size = in.read() << 24 | in.read() << 16 | in.read() << 8 | in.read();
} else {
throw new IllegalArgumentException(String.format("Unexpected first byte 0x%02x", firstByte));
}
return new MPBinary(bytes(in, size).array());
}
}
}

View File

@ -1,88 +0,0 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack.types;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Objects;
/**
* Representation of a msgpack encoded boolean.
*/
public class MPBoolean implements MPType<Boolean> {
private final static int FALSE = 0xC2;
private final static int TRUE = 0xC3;
private final boolean value;
public MPBoolean(boolean value) {
this.value = value;
}
public Boolean getValue() {
return value;
}
public void pack(OutputStream out) throws IOException {
if (value) {
out.write(TRUE);
} else {
out.write(FALSE);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MPBoolean mpBoolean = (MPBoolean) o;
return value == mpBoolean.value;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return String.valueOf(value);
}
@Override
public String toJson() {
return String.valueOf(value);
}
public static class Unpacker implements MPType.Unpacker<MPBoolean> {
public boolean is(int firstByte) {
return firstByte == TRUE || firstByte == FALSE;
}
public MPBoolean unpack(int firstByte, InputStream in) {
if (firstByte == TRUE) {
return new MPBoolean(true);
} else if (firstByte == FALSE) {
return new MPBoolean(false);
} else {
throw new IllegalArgumentException(String.format("0xC2 or 0xC3 expected but was 0x%02x", firstByte));
}
}
}
}

View File

@ -1,110 +0,0 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack.types;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Objects;
import static ch.dissem.msgpack.types.Utils.bytes;
/**
* Representation of a msgpack encoded float32 or float64 number.
*/
public class MPFloat implements MPType<Double> {
public enum Precision {FLOAT32, FLOAT64}
private final double value;
private final Precision precision;
public MPFloat(float value) {
this.value = value;
this.precision = Precision.FLOAT32;
}
public MPFloat(double value) {
this.value = value;
this.precision = Precision.FLOAT64;
}
@Override
public Double getValue() {
return value;
}
public Precision getPrecision() {
return precision;
}
public void pack(OutputStream out) throws IOException {
switch (precision) {
case FLOAT32:
out.write(0xCA);
out.write(ByteBuffer.allocate(4).putFloat((float) value).array());
break;
case FLOAT64:
out.write(0xCB);
out.write(ByteBuffer.allocate(8).putDouble(value).array());
break;
default:
throw new IllegalArgumentException("Unknown precision: " + precision);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MPFloat mpFloat = (MPFloat) o;
return Double.compare(mpFloat.value, value) == 0;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return String.valueOf(value);
}
@Override
public String toJson() {
return String.valueOf(value);
}
public static class Unpacker implements MPType.Unpacker<MPFloat> {
public boolean is(int firstByte) {
return firstByte == 0xCA || firstByte == 0xCB;
}
public MPFloat unpack(int firstByte, InputStream in) throws IOException {
switch (firstByte) {
case 0xCA:
return new MPFloat(bytes(in, 4).getFloat());
case 0xCB:
return new MPFloat(bytes(in, 8).getDouble());
default:
throw new IllegalArgumentException(String.format("Unexpected first byte 0x%02x", firstByte));
}
}
}
}

View File

@ -1,160 +0,0 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack.types;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Objects;
import static ch.dissem.msgpack.types.Utils.bytes;
/**
* Representation of a msgpack encoded integer. The encoding is automatically selected according to the value's size.
* Uses long due to the fact that the msgpack integer implementation may contain up to 64 bit numbers, corresponding
* to Java long values. Also note that uint64 values may be too large for signed long (thanks Java for not supporting
* unsigned values) and end in a negative value.
*/
public class MPInteger implements MPType<Long> {
private long value;
public MPInteger(long value) {
this.value = value;
}
@Override
public Long getValue() {
return value;
}
public void pack(OutputStream out) throws IOException {
if ((value > ((byte) 0b11100000) && value < 0x80)) {
out.write((int) value);
} else if (value > 0) {
if (value <= 0xFF) {
out.write(0xCC);
out.write((int) value);
} else if (value <= 0xFFFF) {
out.write(0xCD);
out.write(ByteBuffer.allocate(2).putShort((short) value).array());
} else if (value <= 0xFFFFFFFFL) {
out.write(0xCE);
out.write(ByteBuffer.allocate(4).putInt((int) value).array());
} else {
out.write(0xCF);
out.write(ByteBuffer.allocate(8).putLong(value).array());
}
} else {
if (value >= -32) {
out.write(new byte[]{
(byte) value
});
} else if (value >= Byte.MIN_VALUE) {
out.write(0xD0);
out.write(ByteBuffer.allocate(1).put((byte) value).array());
} else if (value >= Short.MIN_VALUE) {
out.write(0xD1);
out.write(ByteBuffer.allocate(2).putShort((short) value).array());
} else if (value >= Integer.MIN_VALUE) {
out.write(0xD2);
out.write(ByteBuffer.allocate(4).putInt((int) value).array());
} else {
out.write(0xD3);
out.write(ByteBuffer.allocate(8).putLong(value).array());
}
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MPInteger mpInteger = (MPInteger) o;
return value == mpInteger.value;
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return String.valueOf(value);
}
@Override
public String toJson() {
return String.valueOf(value);
}
public static class Unpacker implements MPType.Unpacker<MPInteger> {
public boolean is(int firstByte) {
switch (firstByte) {
case 0xCC:
case 0xCD:
case 0xCE:
case 0xCF:
case 0xD0:
case 0xD1:
case 0xD2:
case 0xD3:
return true;
default:
return (firstByte & 0b10000000) == 0 || (firstByte & 0b11100000) == 0b11100000;
}
}
public MPInteger unpack(int firstByte, InputStream in) throws IOException {
if ((firstByte & 0b10000000) == 0 || (firstByte & 0b11100000) == 0b11100000) {
return new MPInteger((byte) firstByte);
} else {
switch (firstByte) {
case 0xCC:
return new MPInteger(in.read());
case 0xCD:
return new MPInteger(in.read() << 8 | in.read());
case 0xCE: {
long value = 0;
for (int i = 0; i < 4; i++) {
value = value << 8 | in.read();
}
return new MPInteger(value);
}
case 0xCF: {
long value = 0;
for (int i = 0; i < 8; i++) {
value = value << 8 | in.read();
}
return new MPInteger(value);
}
case 0xD0:
return new MPInteger(bytes(in, 1).get());
case 0xD1:
return new MPInteger(bytes(in, 2).getShort());
case 0xD2:
return new MPInteger(bytes(in, 4).getInt());
case 0xD3:
return new MPInteger(bytes(in, 8).getLong());
default:
throw new IllegalArgumentException(String.format("Unexpected first byte 0x%02x", firstByte));
}
}
}
}
}

View File

@ -1,185 +0,0 @@
package ch.dissem.msgpack.types;
import ch.dissem.msgpack.Reader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.*;
/**
* Representation of a msgpack encoded map. It is recommended to use a {@link LinkedHashMap} to ensure the order
* of entries. For convenience, it also implements the {@link Map} interface.
*
* @param <K>
* @param <V>
*/
public class MPMap<K extends MPType, V extends MPType> implements MPType<Map<K, V>>, Map<K, V> {
private Map<K, V> map;
public MPMap() {
this.map = new LinkedHashMap<>();
}
public MPMap(Map<K, V> map) {
this.map = map;
}
@Override
public Map<K, V> getValue() {
return map;
}
public void pack(OutputStream out) throws IOException {
int size = map.size();
if (size < 16) {
out.write(0x80 + size);
} else if (size < 65536) {
out.write(0xDE);
out.write(ByteBuffer.allocate(2).putShort((short) size).array());
} else {
out.write(0xDF);
out.write(ByteBuffer.allocate(4).putInt(size).array());
}
for (Map.Entry<K, V> e : map.entrySet()) {
e.getKey().pack(out);
e.getValue().pack(out);
}
}
@Override
public int size() {
return map.size();
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public boolean containsKey(Object o) {
return map.containsKey(o);
}
@Override
public boolean containsValue(Object o) {
return map.containsValue(o);
}
@Override
public V get(Object o) {
return map.get(o);
}
@Override
public V put(K k, V v) {
return map.put(k, v);
}
@Override
public V remove(Object o) {
return map.remove(o);
}
@Override
public void putAll(Map<? extends K, ? extends V> map) {
this.map.putAll(map);
}
@Override
public void clear() {
map.clear();
}
@Override
public Set<K> keySet() {
return map.keySet();
}
@Override
public Collection<V> values() {
return map.values();
}
@Override
public Set<Entry<K, V>> entrySet() {
return map.entrySet();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MPMap<?, ?> mpMap = (MPMap<?, ?>) o;
return Objects.equals(map, mpMap.map);
}
@Override
public int hashCode() {
return Objects.hash(map);
}
@Override
public String toString() {
return toJson();
}
String toJson(String indent) {
StringBuilder result = new StringBuilder();
result.append("{\n");
Iterator<Map.Entry<K, V>> iterator = map.entrySet().iterator();
String indent2 = indent + " ";
while (iterator.hasNext()) {
Map.Entry<K, V> item = iterator.next();
result.append(indent2);
result.append(Utils.toJson(item.getKey(), indent2));
result.append(": ");
result.append(Utils.toJson(item.getValue(), indent2));
if (iterator.hasNext()) {
result.append(',');
}
result.append('\n');
}
result.append(indent).append("}");
return result.toString();
}
@Override
public String toJson() {
return toJson("");
}
public static class Unpacker implements MPType.Unpacker<MPMap> {
private final Reader reader;
public Unpacker(Reader reader) {
this.reader = reader;
}
public boolean is(int firstByte) {
return firstByte == 0xDE || firstByte == 0xDF || (firstByte & 0xF0) == 0x80;
}
public MPMap<MPType<?>, MPType<?>> unpack(int firstByte, InputStream in) throws IOException {
int size;
if ((firstByte & 0xF0) == 0x80) {
size = firstByte & 0x0F;
} else if (firstByte == 0xDE) {
size = in.read() << 8 | in.read();
} else if (firstByte == 0xDF) {
size = in.read() << 24 | in.read() << 16 | in.read() << 8 | in.read();
} else {
throw new IllegalArgumentException(String.format("Unexpected first byte 0x%02x", firstByte));
}
Map<MPType<?>, MPType<?>> map = new LinkedHashMap<>();
for (int i = 0; i < size; i++) {
MPType key = reader.read(in);
MPType value = reader.read(in);
map.put(key, value);
}
return new MPMap<>(map);
}
}
}

View File

@ -1,70 +0,0 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack.types;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Representation of msgpack encoded nil / null.
*/
public class MPNil implements MPType<Void> {
private final static int NIL = 0xC0;
public Void getValue() {
return null;
}
public void pack(OutputStream out) throws IOException {
out.write(NIL);
}
@Override
public int hashCode() {
return 0;
}
@Override
public boolean equals(Object o) {
return o instanceof MPNil;
}
@Override
public String toString() {
return "null";
}
@Override
public String toJson() {
return "null";
}
public static class Unpacker implements MPType.Unpacker<MPNil> {
public boolean is(int firstByte) {
return firstByte == NIL;
}
public MPNil unpack(int firstByte, InputStream in) {
if (firstByte != NIL) {
throw new IllegalArgumentException(String.format("0xC0 expected but was 0x%02x", firstByte));
}
return new MPNil();
}
}
}

View File

@ -1,185 +0,0 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack.types;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Objects;
import static ch.dissem.msgpack.types.Utils.bytes;
/**
* Representation of a msgpack encoded string. The encoding is automatically selected according to the string's length.
* <p>
* The default encoding is UTF-8.
* </p>
*/
public class MPString implements MPType<String>, CharSequence {
private static final int FIXSTR_PREFIX = 0b10100000;
private static final int FIXSTR_PREFIX_FILTER = 0b11100000;
private static final int STR8_PREFIX = 0xD9;
private static final int STR8_LIMIT = 256;
private static final int STR16_PREFIX = 0xDA;
private static final int STR16_LIMIT = 65536;
private static final int STR32_PREFIX = 0xDB;
private static final int FIXSTR_FILTER = 0b00011111;
private static Charset encoding = Charset.forName("UTF-8");
private final String value;
/**
* Use this method if for some messed up reason you really need to use something else than UTF-8.
* Ask yourself: why should I? Is this really necessary?
* <p>
* It will set the encoding for all {@link MPString}s, but if you have inconsistent encoding in your
* format you're lost anyway.
* </p>
*/
public static void setEncoding(Charset encoding) {
MPString.encoding = encoding;
}
public MPString(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void pack(OutputStream out) throws IOException {
byte[] bytes = value.getBytes(encoding);
int size = bytes.length;
if (size < 32) {
out.write(FIXSTR_PREFIX + size);
} else if (size < STR8_LIMIT) {
out.write(STR8_PREFIX);
out.write(size);
} else if (size < STR16_LIMIT) {
out.write(STR16_PREFIX);
out.write(ByteBuffer.allocate(2).putShort((short) size).array());
} else {
out.write(STR32_PREFIX);
out.write(ByteBuffer.allocate(4).putInt(size).array());
}
out.write(bytes);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MPString mpString = (MPString) o;
return Objects.equals(value, mpString.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public int length() {
return value.length();
}
@Override
public char charAt(int i) {
return value.charAt(i);
}
@Override
public CharSequence subSequence(int beginIndex, int endIndex) {
return value.subSequence(beginIndex, endIndex);
}
@Override
public String toString() {
return value;
}
@Override
public String toJson() {
StringBuilder result = new StringBuilder(value.length() + 4);
result.append('"');
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
switch (c) {
case '\\':
case '"':
case '/':
result.append('\\').append(c);
break;
case '\b':
result.append("\\b");
break;
case '\t':
result.append("\\t");
break;
case '\n':
result.append("\\n");
break;
case '\f':
result.append("\\f");
break;
case '\r':
result.append("\\r");
break;
default:
if (c < ' ') {
result.append("\\u");
String hex = Integer.toHexString(c);
for (int j = 0; j + hex.length() < 4; j++) {
result.append('0');
}
result.append(hex);
} else {
result.append(c);
}
}
}
result.append('"');
return result.toString();
}
public static class Unpacker implements MPType.Unpacker<MPString> {
public boolean is(int firstByte) {
return firstByte == STR8_PREFIX || firstByte == STR16_PREFIX || firstByte == STR32_PREFIX
|| (firstByte & FIXSTR_PREFIX_FILTER) == FIXSTR_PREFIX;
}
public MPString unpack(int firstByte, InputStream in) throws IOException {
int size;
if ((firstByte & FIXSTR_PREFIX_FILTER) == FIXSTR_PREFIX) {
size = firstByte & FIXSTR_FILTER;
} else if (firstByte == STR8_PREFIX) {
size = in.read();
} else if (firstByte == STR16_PREFIX) {
size = in.read() << 8 | in.read();
} else if (firstByte == STR32_PREFIX) {
size = in.read() << 24 | in.read() << 16 | in.read() << 8 | in.read();
} else {
throw new IllegalArgumentException(String.format("Unexpected first byte 0x%02x", firstByte));
}
return new MPString(new String(bytes(in, size).array(), encoding));
}
}
}

View File

@ -1,115 +0,0 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack.types;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
public class Utils {
private static final char[] BASE64_CODES = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".toCharArray();
private static final MPNil NIL = new MPNil();
/**
* Returns a {@link ByteBuffer} containing the next <code>count</code> bytes from the {@link InputStream}.
*/
static ByteBuffer bytes(InputStream in, int count) throws IOException {
byte[] result = new byte[count];
int off = 0;
while (off < count) {
int read = in.read(result, off, count - off);
if (read < 0) {
throw new IOException("Unexpected end of stream, wanted to read " + count + " bytes but only got " + off);
}
off += read;
}
return ByteBuffer.wrap(result);
}
/**
* Helper method to decide which types support extra indention (for pretty printing JSON)
*/
static String toJson(MPType<?> type, String indent) {
if (type instanceof MPMap) {
return ((MPMap) type).toJson(indent);
}
if (type instanceof MPArray) {
return ((MPArray) type).toJson(indent);
}
return type.toJson();
}
/**
* Slightly improved code from https://en.wikipedia.org/wiki/Base64
*/
static String base64(byte[] data) {
StringBuilder result = new StringBuilder((data.length * 4) / 3 + 3);
int b;
for (int i = 0; i < data.length; i += 3) {
b = (data[i] & 0xFC) >> 2;
result.append(BASE64_CODES[b]);
b = (data[i] & 0x03) << 4;
if (i + 1 < data.length) {
b |= (data[i + 1] & 0xF0) >> 4;
result.append(BASE64_CODES[b]);
b = (data[i + 1] & 0x0F) << 2;
if (i + 2 < data.length) {
b |= (data[i + 2] & 0xC0) >> 6;
result.append(BASE64_CODES[b]);
b = data[i + 2] & 0x3F;
result.append(BASE64_CODES[b]);
} else {
result.append(BASE64_CODES[b]);
result.append('=');
}
} else {
result.append(BASE64_CODES[b]);
result.append("==");
}
}
return result.toString();
}
public static MPString mp(String value) {
return new MPString(value);
}
public static MPBoolean mp(boolean value) {
return new MPBoolean(value);
}
public static MPFloat mp(double value) {
return new MPFloat(value);
}
public static MPFloat mp(float value) {
return new MPFloat(value);
}
public static MPInteger mp(long value) {
return new MPInteger(value);
}
public static MPBinary mp(byte... data) {
return new MPBinary(data);
}
public static MPNil nil() {
return NIL;
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack
import ch.dissem.msgpack.types.*
import java.io.IOException
import java.io.InputStream
/**
* Reads MPType object from an [InputStream].
*/
object Reader {
private val unpackers = mutableListOf<MPType.Unpacker<*>>()
init {
unpackers.add(MPNil.Unpacker())
unpackers.add(MPBoolean.Unpacker())
unpackers.add(MPInteger.Unpacker())
unpackers.add(MPFloat.Unpacker())
unpackers.add(MPString.Unpacker())
unpackers.add(MPBinary.Unpacker())
unpackers.add(MPMap.Unpacker(this))
unpackers.add(MPArray.Unpacker(this))
}
/**
* Register your own extensions. The last registered unpacker always takes precedence.
*/
fun register(unpacker: MPType.Unpacker<*>) {
unpackers.add(0, unpacker)
}
@Throws(IOException::class)
fun read(input: InputStream): MPType<*> {
val firstByte = input.read()
unpackers
.firstOrNull { it.doesUnpack(firstByte) }
?.let {
return it.unpack(firstByte, input)
}
throw IOException(String.format("Unsupported input, no reader for 0x%02x", firstByte))
}
}

View File

@ -0,0 +1,145 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack.types
import ch.dissem.msgpack.Reader
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.ByteBuffer
/**
* Representation of a msgpack encoded array. Uses a list to represent data internally, and implements the [List]
* interface for your convenience.
* @param <E> content type
*/
data class MPArray<E : MPType<*>>(override val value: MutableList<E> = mutableListOf()) : MPType<MutableList<E>>, MutableList<E> {
@SafeVarargs
constructor(vararg objects: E) : this(mutableListOf(*objects))
@Throws(IOException::class)
override fun pack(out: OutputStream) {
val size = value.size
when {
size < 16 -> out.write(144 + size)
size < 65536 -> {
out.write(0xDC)
out.write(ByteBuffer.allocate(2).putShort(size.toShort()).array())
}
else -> {
out.write(0xDD)
out.write(ByteBuffer.allocate(4).putInt(size).array())
}
}
for (o in value) {
o.pack(out)
}
}
override val size: Int
get() = value.size
override fun isEmpty(): Boolean {
return value.isEmpty()
}
override fun contains(element: E) = value.contains(element)
override fun iterator() = value.iterator()
override fun add(element: E) = value.add(element)
override fun remove(element: E) = value.remove(element)
override fun containsAll(elements: Collection<E>) = value.containsAll(elements)
override fun addAll(elements: Collection<E>) = value.addAll(elements)
override fun addAll(index: Int, elements: Collection<E>) = value.addAll(index, elements)
override fun removeAll(elements: Collection<E>) = value.removeAll(elements)
override fun retainAll(elements: Collection<E>) = value.retainAll(elements)
override fun clear() = value.clear()
override fun get(index: Int) = value[index]
override operator fun set(index: Int, element: E) = value.set(index, element)
override fun add(index: Int, element: E) = value.add(index, element)
override fun removeAt(index: Int) = value.removeAt(index)
override fun indexOf(element: E) = value.indexOf(element)
override fun lastIndexOf(element: E) = value.lastIndexOf(element)
override fun listIterator() = value.listIterator()
override fun listIterator(index: Int) = value.listIterator(index)
override fun subList(fromIndex: Int, toIndex: Int) = value.subList(fromIndex, toIndex)
override fun toString() = toJson()
override fun toJson() = toJson("")
internal fun toJson(indent: String): String {
val result = StringBuilder()
result.append("[\n")
val iterator = value.iterator()
val indent2 = indent + " "
while (iterator.hasNext()) {
val item = iterator.next()
result.append(indent2)
result.append(Utils.toJson(item, indent2))
if (iterator.hasNext()) {
result.append(',')
}
result.append('\n')
}
result.append("]")
return result.toString()
}
class Unpacker(private val reader: Reader) : MPType.Unpacker<MPArray<*>> {
override fun doesUnpack(firstByte: Int): Boolean {
return firstByte == 0xDC || firstByte == 0xDD || firstByte and 240 == 144
}
@Throws(IOException::class)
override fun unpack(firstByte: Int, input: InputStream): MPArray<MPType<*>> {
val size: Int = when {
firstByte and 240 == 144 -> firstByte and 15
firstByte == 0xDC -> input.read() shl 8 or input.read()
firstByte == 0xDD -> input.read() shl 24 or (input.read() shl 16) or (input.read() shl 8) or input.read()
else -> throw IllegalArgumentException(String.format("Unexpected first byte 0x%02x", firstByte))
}
val list = mutableListOf<MPType<*>>()
for (i in 0 until size) {
val value = reader.read(input)
list.add(value)
}
return MPArray(list)
}
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack.types
import ch.dissem.msgpack.types.Utils.bytes
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.ByteBuffer
import java.util.*
/**
* Representation of msgpack encoded binary data a.k.a. byte array.
*/
data class MPBinary(override val value: ByteArray) : MPType<ByteArray> {
@Throws(IOException::class)
override fun pack(out: OutputStream) {
val size = value.size
when {
size < 256 -> {
out.write(0xC4)
out.write(size.toByte().toInt())
}
size < 65536 -> {
out.write(0xC5)
out.write(ByteBuffer.allocate(2).putShort(size.toShort()).array())
}
else -> {
out.write(0xC6)
out.write(ByteBuffer.allocate(4).putInt(size).array())
}
}
out.write(value)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is MPBinary) return false
return Arrays.equals(value, other.value)
}
override fun hashCode(): Int {
return Arrays.hashCode(value)
}
override fun toString(): String {
return toJson()
}
override fun toJson(): String {
return Utils.base64(value)
}
class Unpacker : MPType.Unpacker<MPBinary> {
override fun doesUnpack(firstByte: Int): Boolean {
return firstByte == 0xC4 || firstByte == 0xC5 || firstByte == 0xC6
}
@Throws(IOException::class)
override fun unpack(firstByte: Int, input: InputStream): MPBinary {
val size: Int = when (firstByte) {
0xC4 -> input.read()
0xC5 -> input.read() shl 8 or input.read()
0xC6 -> input.read() shl 24 or (input.read() shl 16) or (input.read() shl 8) or input.read()
else -> throw IllegalArgumentException(String.format("Unexpected first byte 0x%02x", firstByte))
}
return MPBinary(bytes(input, size).array())
}
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack.types
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
/**
* Representation of a msgpack encoded boolean.
*/
data class MPBoolean(override val value: Boolean) : MPType<Boolean> {
@Throws(IOException::class)
override fun pack(out: OutputStream) {
out.write(if (value) {
TRUE
} else {
FALSE
})
}
override fun toString(): String {
return value.toString()
}
override fun toJson(): String {
return value.toString()
}
class Unpacker : MPType.Unpacker<MPBoolean> {
override fun doesUnpack(firstByte: Int): Boolean {
return firstByte == TRUE || firstByte == FALSE
}
override fun unpack(firstByte: Int, input: InputStream) = when (firstByte) {
TRUE -> MPBoolean(true)
FALSE -> MPBoolean(false)
else -> throw IllegalArgumentException(String.format("0xC2 or 0xC3 expected but was 0x%02x", firstByte))
}
}
companion object {
private const val FALSE = 0xC2
private const val TRUE = 0xC3
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack.types
import ch.dissem.msgpack.types.Utils.bytes
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.ByteBuffer
/**
* Representation of a msgpack encoded float32 or float64 number.
*/
data class MPFloat(override val value: Double, val precision: Precision) : MPType<Double> {
enum class Precision {
FLOAT32, FLOAT64
}
constructor(value: Float) : this(value.toDouble(), Precision.FLOAT32)
constructor(value: Double) : this(value, Precision.FLOAT64)
@Throws(IOException::class)
override fun pack(out: OutputStream) {
when (precision) {
MPFloat.Precision.FLOAT32 -> {
out.write(0xCA)
out.write(ByteBuffer.allocate(4).putFloat(value.toFloat()).array())
}
MPFloat.Precision.FLOAT64 -> {
out.write(0xCB)
out.write(ByteBuffer.allocate(8).putDouble(value).array())
}
}
}
override fun toString(): String {
return value.toString()
}
override fun toJson(): String {
return value.toString()
}
class Unpacker : MPType.Unpacker<MPFloat> {
override fun doesUnpack(firstByte: Int): Boolean {
return firstByte == 0xCA || firstByte == 0xCB
}
@Throws(IOException::class)
override fun unpack(firstByte: Int, input: InputStream) = when (firstByte) {
0xCA -> MPFloat(bytes(input, 4).float)
0xCB -> MPFloat(bytes(input, 8).double)
else -> throw IllegalArgumentException(String.format("Unexpected first byte 0x%02x", firstByte))
}
}
}

View File

@ -0,0 +1,116 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack.types
import ch.dissem.msgpack.types.Utils.bytes
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.ByteBuffer
/**
* Representation of a msgpack encoded integer. The encoding is automatically selected according to the value's size.
* Uses long due to the fact that the msgpack integer implementation may contain up to 64 bit numbers, corresponding
* to Java long values. Also note that uint64 values may be too large for signed long (thanks Java for not supporting
* unsigned values) and end in a negative value.
*/
data class MPInteger(override val value: Long) : MPType<Long> {
constructor(value: Byte) : this(value.toLong())
constructor(value: Short) : this(value.toLong())
constructor(value: Int) : this(value.toLong())
@Throws(IOException::class)
override fun pack(out: OutputStream) {
when (value) {
in -32..127 -> out.write(value.toInt())
in 0..0xFF -> {
out.write(0xCC)
out.write(value.toInt())
}
in 0..0xFFFF -> {
out.write(0xCD)
out.write(ByteBuffer.allocate(2).putShort(value.toShort()).array())
}
in 0..0xFFFFFFFFL -> {
out.write(0xCE)
out.write(ByteBuffer.allocate(4).putInt(value.toInt()).array())
}
in 0..Long.MAX_VALUE -> {
out.write(0xCF)
out.write(ByteBuffer.allocate(8).putLong(value).array())
}
in Byte.MIN_VALUE..0 -> {
out.write(0xD0)
out.write(ByteBuffer.allocate(1).put(value.toByte()).array())
}
in Short.MIN_VALUE..0 -> {
out.write(0xD1)
out.write(ByteBuffer.allocate(2).putShort(value.toShort()).array())
}
in Int.MIN_VALUE..0 -> {
out.write(0xD2)
out.write(ByteBuffer.allocate(4).putInt(value.toInt()).array())
}
in Long.MIN_VALUE..0 -> {
out.write(0xD3)
out.write(ByteBuffer.allocate(8).putLong(value).array())
}
}
}
override fun toString() = value.toString()
override fun toJson() = value.toString()
class Unpacker : MPType.Unpacker<MPInteger> {
override fun doesUnpack(firstByte: Int): Boolean {
return when (firstByte) {
0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xD2, 0xD3 -> true
else -> firstByte and 128 == 0 || firstByte and 224 == 224
}
}
@Throws(IOException::class)
override fun unpack(firstByte: Int, input: InputStream): MPInteger {
if (firstByte and 128 == 0 || firstByte and 224 == 224) {
// The cast needs to happen for the MPInteger to have the correct sign
return MPInteger(firstByte.toByte())
} else {
return when (firstByte) {
0xCC -> MPInteger(readLong(input, 1))
0xCD -> MPInteger(readLong(input, 2))
0xCE -> MPInteger(readLong(input, 4))
0xCF -> MPInteger(readLong(input, 8))
0xD0 -> MPInteger(bytes(input, 1).get())
0xD1 -> MPInteger(bytes(input, 2).short)
0xD2 -> MPInteger(bytes(input, 4).int)
0xD3 -> MPInteger(bytes(input, 8).long)
else -> throw IllegalArgumentException(String.format("Unexpected first byte 0x%02x", firstByte))
}
}
}
private fun readLong(input: InputStream, length: Int): Long {
var value: Long = 0
for (i in 0 until length) {
value = value shl 8 or input.read().toLong()
}
return value
}
}
}

View File

@ -0,0 +1,119 @@
package ch.dissem.msgpack.types
import ch.dissem.msgpack.Reader
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.ByteBuffer
import java.util.*
import kotlin.collections.LinkedHashMap
/**
* Representation of a msgpack encoded map. It is recommended to use a [LinkedHashMap] to ensure the order
* of entries. For convenience, it also implements the [Map] interface.
*
* @param <K> key type
* @param <V> value type
*/
data class MPMap<K : MPType<*>, V : MPType<*>>(override val value: MutableMap<K, V> = LinkedHashMap<K, V>()) : MPType<Map<K, V>>, MutableMap<K, V> {
@Throws(IOException::class)
override fun pack(out: OutputStream) {
val size = value.size
if (size < 16) {
out.write(0x80 + size)
} else if (size < 65536) {
out.write(0xDE)
out.write(ByteBuffer.allocate(2).putShort(size.toShort()).array())
} else {
out.write(0xDF)
out.write(ByteBuffer.allocate(4).putInt(size).array())
}
for ((key, value1) in value) {
key.pack(out)
value1.pack(out)
}
}
override val size: Int
get() = value.size
override fun isEmpty() = value.isEmpty()
override fun containsKey(key: K) = value.containsKey(key)
override fun containsValue(value: V) = this.value.containsValue(value)
override fun get(key: K) = value[key]
override fun putIfAbsent(key: K, value: V) = this.value.putIfAbsent(key, value)
override fun put(key: K, value: V): V? = this.value.put(key, value)
override fun remove(key: K): V? = value.remove(key)
override fun putAll(from: Map<out K, V>) = value.putAll(from)
override fun clear() = value.clear()
override val keys: MutableSet<K>
get() = value.keys
override val values: MutableCollection<V>
get() = value.values
override val entries: MutableSet<MutableMap.MutableEntry<K, V>>
get() = value.entries
override fun toString() = toJson()
internal fun toJson(indent: String): String {
val result = StringBuilder()
result.append("{\n")
val iterator = value.entries.iterator()
val indent2 = indent + " "
while (iterator.hasNext()) {
val item = iterator.next()
result.append(indent2)
result.append(Utils.toJson(item.key, indent2))
result.append(": ")
result.append(Utils.toJson(item.value, indent2))
if (iterator.hasNext()) {
result.append(',')
}
result.append('\n')
}
result.append(indent).append("}")
return result.toString()
}
override fun toJson(): String {
return toJson("")
}
class Unpacker(private val reader: Reader) : MPType.Unpacker<MPMap<*, *>> {
override fun doesUnpack(firstByte: Int): Boolean {
return firstByte == 0xDE || firstByte == 0xDF || firstByte and 0xF0 == 0x80
}
@Throws(IOException::class)
override fun unpack(firstByte: Int, input: InputStream): MPMap<MPType<*>, MPType<*>> {
val size: Int
when {
firstByte and 0xF0 == 0x80 -> size = firstByte and 0x0F
firstByte == 0xDE -> size = input.read() shl 8 or input.read()
firstByte == 0xDF -> size = input.read() shl 24 or (input.read() shl 16) or (input.read() shl 8) or input.read()
else -> throw IllegalArgumentException(String.format("Unexpected first byte 0x%02x", firstByte))
}
val map = LinkedHashMap<MPType<*>, MPType<*>>()
for (i in 0..size - 1) {
val key = reader.read(input)
val value = reader.read(input)
map.put(key, value)
}
return MPMap(map)
}
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack.types
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
/**
* Representation of msgpack encoded nil / null.
*/
object MPNil : MPType<Void?> {
override val value: Void? = null
@Throws(IOException::class)
override fun pack(out: OutputStream) = out.write(NIL)
override fun toString() = "null"
override fun toJson() = "null"
class Unpacker : MPType.Unpacker<MPNil> {
override fun doesUnpack(firstByte: Int) = firstByte == NIL
override fun unpack(firstByte: Int, input: InputStream): MPNil {
return when (firstByte) {
NIL -> MPNil
else -> throw IllegalArgumentException(String.format("0xC0 expected but was 0x%02x", firstByte))
}
}
}
private val NIL = 0xC0
}

View File

@ -0,0 +1,135 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack.types
import ch.dissem.msgpack.types.Utils.bytes
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.ByteBuffer
import java.nio.charset.Charset
/**
* Representation of a msgpack encoded string. The encoding is automatically selected according to the string's length.
*
* The default encoding is UTF-8.
*/
data class MPString(override val value: String) : MPType<String>, CharSequence {
@Throws(IOException::class)
override fun pack(out: OutputStream) {
val bytes = value.toByteArray(encoding)
val size = bytes.size
when {
size < 32 -> out.write(FIXSTR_PREFIX + size)
size < STR8_LIMIT -> {
out.write(STR8_PREFIX)
out.write(size)
}
size < STR16_LIMIT -> {
out.write(STR16_PREFIX)
out.write(ByteBuffer.allocate(2).putShort(size.toShort()).array())
}
else -> {
out.write(STR32_PREFIX)
out.write(ByteBuffer.allocate(4).putInt(size).array())
}
}
out.write(bytes)
}
override val length: Int = value.length
override fun get(index: Int): Char {
return value[index]
}
override fun subSequence(startIndex: Int, endIndex: Int): CharSequence {
return value.subSequence(startIndex, endIndex)
}
override fun toString(): String {
return value
}
override fun toJson(): String {
val result = StringBuilder(value.length + 4)
result.append('"')
value.forEach {
when (it) {
'\\', '"', '/' -> result.append('\\').append(it)
'\b' -> result.append("\\b")
'\t' -> result.append("\\t")
'\n' -> result.append("\\n")
'\r' -> result.append("\\r")
else -> if (it < ' ') {
result.append("\\u")
val hex = Integer.toHexString(it.toInt())
var j = 0
while (j + hex.length < 4) {
result.append('0')
j++
}
result.append(hex)
} else {
result.append(it)
}
}
}
result.append('"')
return result.toString()
}
class Unpacker : MPType.Unpacker<MPString> {
override fun doesUnpack(firstByte: Int): Boolean {
return firstByte == STR8_PREFIX || firstByte == STR16_PREFIX || firstByte == STR32_PREFIX
|| firstByte and FIXSTR_PREFIX_FILTER == FIXSTR_PREFIX
}
@Throws(IOException::class)
override fun unpack(firstByte: Int, input: InputStream): MPString {
val size: Int = when {
firstByte and FIXSTR_PREFIX_FILTER == FIXSTR_PREFIX -> firstByte and FIXSTR_FILTER
firstByte == STR8_PREFIX -> input.read()
firstByte == STR16_PREFIX -> input.read() shl 8 or input.read()
firstByte == STR32_PREFIX -> input.read() shl 24 or (input.read() shl 16) or (input.read() shl 8) or input.read()
else -> throw IllegalArgumentException(String.format("Unexpected first byte 0x%02x", firstByte))
}
return MPString(String(bytes(input, size).array(), encoding))
}
}
companion object {
private val FIXSTR_PREFIX = 160
private val FIXSTR_PREFIX_FILTER = 224
private val STR8_PREFIX = 0xD9
private val STR8_LIMIT = 256
private val STR16_PREFIX = 0xDA
private val STR16_LIMIT = 65536
private val STR32_PREFIX = 0xDB
private val FIXSTR_FILTER = 31
/**
* Use this if for some messed up reason you really need to use something else than UTF-8.
* Ask yourself: why should I? Is this really necessary?
*
* It will set the encoding for all [MPString]s, but if you have inconsistent encoding in your
* format you're lost anyway.
*/
var encoding = Charset.forName("UTF-8")
}
}

View File

@ -14,25 +14,27 @@
* limitations under the License. * limitations under the License.
*/ */
package ch.dissem.msgpack.types; package ch.dissem.msgpack.types
import java.io.IOException; import java.io.IOException
import java.io.InputStream; import java.io.InputStream
import java.io.OutputStream; import java.io.OutputStream
/** /**
* Representation of some msgpack encoded data. * Representation of some msgpack encoded data.
*/ */
public interface MPType<T> { interface MPType<out T> {
interface Unpacker<M extends MPType> { interface Unpacker<out M : MPType<*>> {
boolean is(int firstByte); fun doesUnpack(firstByte: Int): Boolean
M unpack(int firstByte, InputStream in) throws IOException; @Throws(IOException::class)
fun unpack(firstByte: Int, input: InputStream): M
} }
T getValue(); val value: T
void pack(OutputStream out) throws IOException; @Throws(IOException::class)
fun pack(out: OutputStream)
String toJson(); fun toJson(): String
} }

View File

@ -0,0 +1,133 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack.types
import java.io.IOException
import java.io.InputStream
import java.nio.ByteBuffer
object Utils {
private val BASE64_CODES = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".toCharArray()
/**
* Returns a [ByteBuffer] containing the next `count` bytes from the [InputStream].
*/
@Throws(IOException::class)
internal fun bytes(`in`: InputStream, count: Int): ByteBuffer {
val result = ByteArray(count)
var off = 0
while (off < count) {
val read = `in`.read(result, off, count - off)
if (read < 0) {
throw IOException("Unexpected end of stream, wanted to read $count bytes but only got $off")
}
off += read
}
return ByteBuffer.wrap(result)
}
/**
* Helper method to decide which types support extra indention (for pretty printing JSON)
*/
internal fun toJson(type: MPType<*>, indent: String): String {
if (type is MPMap<*, *>) {
return type.toJson(indent)
}
if (type is MPArray<*>) {
return type.toJson(indent)
}
return type.toJson()
}
/**
* Slightly improved code from https://en.wikipedia.org/wiki/Base64
*/
internal fun base64(data: ByteArray): String {
val result = StringBuilder(data.size * 4 / 3 + 3)
var b: Int
var i = 0
while (i < data.size) {
b = data[i].toInt() and 0xFC shr 2
result.append(BASE64_CODES[b])
b = data[i].toInt() and 0x03 shl 4
if (i + 1 < data.size) {
b = b or (data[i + 1].toInt() and 0xF0 shr 4)
result.append(BASE64_CODES[b])
b = data[i + 1].toInt() and 0x0F shl 2
if (i + 2 < data.size) {
b = b or (data[i + 2].toInt() and 0xC0 shr 6)
result.append(BASE64_CODES[b])
b = data[i + 2].toInt() and 0x3F
result.append(BASE64_CODES[b])
} else {
result.append(BASE64_CODES[b])
result.append('=')
}
} else {
result.append(BASE64_CODES[b])
result.append("==")
}
i += 3
}
return result.toString()
}
@JvmStatic
val String.mp
@JvmName("mp")
get() = MPString(this)
@JvmStatic
val Boolean.mp
@JvmName("mp")
get() = MPBoolean(this)
@JvmStatic
val Float.mp
@JvmName("mp")
get() = MPFloat(this)
@JvmStatic
val Double.mp
@JvmName("mp")
get() = MPFloat(this)
@JvmStatic
val Int.mp
@JvmName("mp")
get() = MPInteger(this.toLong())
@JvmStatic
val Long.mp
@JvmName("mp")
get() = MPInteger(this)
@JvmStatic
val ByteArray.mp
get() = MPBinary(this)
@JvmStatic
fun mp(vararg data: Byte): MPBinary {
return MPBinary(data)
}
@JvmStatic
fun nil(): MPNil {
return MPNil
}
}

View File

@ -1,270 +0,0 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack;
import ch.dissem.msgpack.types.*;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;
import static ch.dissem.msgpack.types.Utils.mp;
import static ch.dissem.msgpack.types.Utils.nil;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
public class ReaderTest {
private static final Random RANDOM = new Random();
private Reader reader = Reader.getInstance();
@Test
public void ensureDemoJsonIsParsedCorrectly() throws Exception {
MPType read = reader.read(stream("demo.mp"));
assertThat(read, instanceOf(MPMap.class));
assertThat(read.toString(), is(string("demo.json")));
}
@Test
public void ensureDemoJsonIsEncodedCorrectly() throws Exception {
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
MPMap<MPString, MPType<?>> object = new MPMap<>();
object.put(mp("compact"), mp(true));
object.put(mp("schema"), mp(0));
ByteArrayOutputStream out = new ByteArrayOutputStream();
object.pack(out);
assertThat(out.toByteArray(), is(bytes("demo.mp")));
}
@Test
@SuppressWarnings("unchecked")
public void ensureMPArrayIsEncodedAndDecodedCorrectly() throws Exception {
MPArray<MPType<?>> array = new MPArray<>(
mp(new byte[]{1, 3, 3, 7}),
mp(false),
mp(Math.PI),
mp(1.5f),
mp(42),
new MPMap<MPNil, MPNil>(),
nil(),
mp("yay! \uD83E\uDD13")
);
ByteArrayOutputStream out = new ByteArrayOutputStream();
array.pack(out);
MPType read = reader.read(new ByteArrayInputStream(out.toByteArray()));
assertThat(read, instanceOf(MPArray.class));
assertThat((MPArray<MPType<?>>) read, is(array));
assertThat(read.toJson(), is("[\n AQMDBw==,\n false,\n 3.141592653589793,\n 1.5,\n 42,\n {\n },\n null,\n \"yay! 🤓\"\n]"));
}
@Test
public void ensureFloatIsEncodedAndDecodedCorrectly() throws Exception {
MPFloat expected = new MPFloat(1.5f);
ByteArrayOutputStream out = new ByteArrayOutputStream();
expected.pack(out);
MPType read = reader.read(new ByteArrayInputStream(out.toByteArray()));
assertThat(read, instanceOf(MPFloat.class));
MPFloat actual = (MPFloat) read;
assertThat(actual, is(expected));
assertThat(actual.getPrecision(), is(MPFloat.Precision.FLOAT32));
}
@Test
public void ensureDoubleIsEncodedAndDecodedCorrectly() throws Exception {
MPFloat expected = new MPFloat(Math.PI);
ByteArrayOutputStream out = new ByteArrayOutputStream();
expected.pack(out);
MPType read = reader.read(new ByteArrayInputStream(out.toByteArray()));
assertThat(read, instanceOf(MPFloat.class));
MPFloat actual = (MPFloat) read;
assertThat(actual, is(expected));
assertThat(actual.getValue(), is(Math.PI));
assertThat(actual.getPrecision(), is(MPFloat.Precision.FLOAT64));
}
@Test
public void ensureLongsAreEncodedAndDecodedCorrectly() throws Exception {
// positive fixnum
ensureLongIsEncodedAndDecodedCorrectly(0, 1);
ensureLongIsEncodedAndDecodedCorrectly(127, 1);
// negative fixnum
ensureLongIsEncodedAndDecodedCorrectly(-1, 1);
ensureLongIsEncodedAndDecodedCorrectly(-32, 1);
// uint 8
ensureLongIsEncodedAndDecodedCorrectly(128, 2);
ensureLongIsEncodedAndDecodedCorrectly(255, 2);
// uint 16
ensureLongIsEncodedAndDecodedCorrectly(256, 3);
ensureLongIsEncodedAndDecodedCorrectly(65535, 3);
// uint 32
ensureLongIsEncodedAndDecodedCorrectly(65536, 5);
ensureLongIsEncodedAndDecodedCorrectly(4294967295L, 5);
// uint 64
ensureLongIsEncodedAndDecodedCorrectly(4294967296L, 9);
ensureLongIsEncodedAndDecodedCorrectly(Long.MAX_VALUE, 9);
// int 8
ensureLongIsEncodedAndDecodedCorrectly(-33, 2);
ensureLongIsEncodedAndDecodedCorrectly(-128, 2);
// int 16
ensureLongIsEncodedAndDecodedCorrectly(-129, 3);
ensureLongIsEncodedAndDecodedCorrectly(-32768, 3);
// int 32
ensureLongIsEncodedAndDecodedCorrectly(-32769, 5);
ensureLongIsEncodedAndDecodedCorrectly(Integer.MIN_VALUE, 5);
// int 64
ensureLongIsEncodedAndDecodedCorrectly(-2147483649L, 9);
ensureLongIsEncodedAndDecodedCorrectly(Long.MIN_VALUE, 9);
}
private void ensureLongIsEncodedAndDecodedCorrectly(long val, int bytes) throws Exception {
MPInteger value = mp(val);
ByteArrayOutputStream out = new ByteArrayOutputStream();
value.pack(out);
MPType read = reader.read(new ByteArrayInputStream(out.toByteArray()));
assertThat(out.size(), is(bytes));
assertThat(read, instanceOf(MPInteger.class));
assertThat((MPInteger) read, is(value));
}
@Test
public void ensureStringsAreEncodedAndDecodedCorrectly() throws Exception {
ensureStringIsEncodedAndDecodedCorrectly(0);
ensureStringIsEncodedAndDecodedCorrectly(31);
ensureStringIsEncodedAndDecodedCorrectly(32);
ensureStringIsEncodedAndDecodedCorrectly(255);
ensureStringIsEncodedAndDecodedCorrectly(256);
ensureStringIsEncodedAndDecodedCorrectly(65535);
ensureStringIsEncodedAndDecodedCorrectly(65536);
}
@Test
public void ensureJsonStringsAreEscapedCorrectly() throws Exception {
StringBuilder builder = new StringBuilder();
for (char c = '\u0001'; c < ' '; c++) {
builder.append(c);
}
MPString string = new MPString(builder.toString());
assertThat(string.toJson(), is("\"\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000b\\f\\r\\u000e\\u000f\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d\\u001e\\u001f\""));
}
private void ensureStringIsEncodedAndDecodedCorrectly(int length) throws Exception {
MPString value = new MPString(stringWithLength(length));
ByteArrayOutputStream out = new ByteArrayOutputStream();
value.pack(out);
MPType read = reader.read(new ByteArrayInputStream(out.toByteArray()));
assertThat(read, instanceOf(MPString.class));
assertThat((MPString) read, is(value));
}
@Test
public void ensureBinariesAreEncodedAndDecodedCorrectly() throws Exception {
ensureBinaryIsEncodedAndDecodedCorrectly(0);
ensureBinaryIsEncodedAndDecodedCorrectly(255);
ensureBinaryIsEncodedAndDecodedCorrectly(256);
ensureBinaryIsEncodedAndDecodedCorrectly(65535);
ensureBinaryIsEncodedAndDecodedCorrectly(65536);
}
private void ensureBinaryIsEncodedAndDecodedCorrectly(int length) throws Exception {
MPBinary value = new MPBinary(new byte[length]);
RANDOM.nextBytes(value.getValue());
ByteArrayOutputStream out = new ByteArrayOutputStream();
value.pack(out);
MPType read = reader.read(new ByteArrayInputStream(out.toByteArray()));
assertThat(read, instanceOf(MPBinary.class));
assertThat((MPBinary) read, is(value));
}
@Test
public void ensureArraysAreEncodedAndDecodedCorrectly() throws Exception {
ensureArrayIsEncodedAndDecodedCorrectly(0);
ensureArrayIsEncodedAndDecodedCorrectly(15);
ensureArrayIsEncodedAndDecodedCorrectly(16);
ensureArrayIsEncodedAndDecodedCorrectly(65535);
ensureArrayIsEncodedAndDecodedCorrectly(65536);
}
@SuppressWarnings("unchecked")
private void ensureArrayIsEncodedAndDecodedCorrectly(int length) throws Exception {
MPNil nil = new MPNil();
ArrayList<MPNil> list = new ArrayList<>(length);
for (int i = 0; i < length; i++) {
list.add(nil);
}
MPArray<MPNil> value = new MPArray<>(list);
ByteArrayOutputStream out = new ByteArrayOutputStream();
value.pack(out);
MPType read = reader.read(new ByteArrayInputStream(out.toByteArray()));
assertThat(read, instanceOf(MPArray.class));
assertThat((MPArray<MPNil>) read, is(value));
}
@Test
public void ensureMapsAreEncodedAndDecodedCorrectly() throws Exception {
ensureMapIsEncodedAndDecodedCorrectly(0);
ensureMapIsEncodedAndDecodedCorrectly(15);
ensureMapIsEncodedAndDecodedCorrectly(16);
ensureMapIsEncodedAndDecodedCorrectly(65535);
ensureMapIsEncodedAndDecodedCorrectly(65536);
}
@SuppressWarnings("unchecked")
private void ensureMapIsEncodedAndDecodedCorrectly(int size) throws Exception {
MPNil nil = new MPNil();
HashMap<MPInteger, MPNil> map = new HashMap<>(size);
for (int i = 0; i < size; i++) {
map.put(mp(i), nil);
}
MPMap<MPInteger, MPNil> value = new MPMap<>(map);
ByteArrayOutputStream out = new ByteArrayOutputStream();
value.pack(out);
MPType read = reader.read(new ByteArrayInputStream(out.toByteArray()));
assertThat(read, instanceOf(MPMap.class));
assertThat((MPMap<MPInteger, MPNil>) read, is(value));
}
private String stringWithLength(int length) {
StringBuilder result = new StringBuilder(length);
for (int i = 0; i < length; i++) {
result.append('a');
}
return result.toString();
}
private InputStream stream(String resource) {
return getClass().getClassLoader().getResourceAsStream(resource);
}
private byte[] bytes(String resource) throws IOException {
InputStream in = stream(resource);
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[100];
for (int size = in.read(buffer); size >= 0; size = in.read(buffer)) {
out.write(buffer, 0, size);
}
return out.toByteArray();
}
private String string(String resource) throws IOException {
return new String(bytes(resource), "UTF-8");
}
}

View File

@ -0,0 +1,268 @@
/*
* Copyright 2017 Christian Basler
*
* 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.
*/
package ch.dissem.msgpack
import ch.dissem.msgpack.types.*
import ch.dissem.msgpack.types.Utils.mp
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.CoreMatchers.instanceOf
import org.junit.Assert.assertThat
import org.junit.Test
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.util.*
class ReaderTest {
@Test
fun `ensure demo json is parsed correctly`() {
val read = Reader.read(stream("demo.mp"))
assertThat(read, instanceOf(MPMap::class.java))
assertThat(read.toString(), equalTo(string("demo.json")))
}
@Test
fun `ensure demo json is encoded correctly`() {
val obj = MPMap<MPString, MPType<*>>()
obj.put("compact".mp, true.mp)
obj.put("schema".mp, 0.mp)
val out = ByteArrayOutputStream()
obj.pack(out)
assertThat(out.toByteArray(), equalTo(bytes("demo.mp")))
}
@Test
fun `ensure mpArray is encoded and decoded correctly`() {
val array = MPArray(
byteArrayOf(1, 3, 3, 7).mp,
false.mp,
Math.PI.mp,
1.5f.mp,
42.mp,
MPMap<MPNil, MPNil>(),
MPNil,
"yay! \uD83E\uDD13".mp
)
val out = ByteArrayOutputStream()
array.pack(out)
val read = Reader.read(ByteArrayInputStream(out.toByteArray()))
assertThat(read, instanceOf(MPArray::class.java))
@Suppress("UNCHECKED_CAST")
assertThat(read as MPArray<MPType<*>>, equalTo(array))
assertThat(read.toJson(), equalTo("[\n AQMDBw==,\n false,\n 3.141592653589793,\n 1.5,\n 42,\n {\n },\n null,\n \"yay! 🤓\"\n]"))
}
@Test
fun `ensure float is encoded and decoded correctly`() {
val expected = MPFloat(1.5f)
val out = ByteArrayOutputStream()
expected.pack(out)
val read = Reader.read(ByteArrayInputStream(out.toByteArray()))
assertThat(read, instanceOf<Any>(MPFloat::class.java))
val actual = read as MPFloat
assertThat(actual, equalTo(expected))
assertThat(actual.precision, equalTo(MPFloat.Precision.FLOAT32))
}
@Test
fun `ensure double is encoded and decoded correctly`() {
val expected = MPFloat(Math.PI)
val out = ByteArrayOutputStream()
expected.pack(out)
val read = Reader.read(ByteArrayInputStream(out.toByteArray()))
assertThat(read, instanceOf<Any>(MPFloat::class.java))
val actual = read as MPFloat
assertThat(actual, equalTo(expected))
assertThat(actual.value, equalTo(Math.PI))
assertThat(actual.precision, equalTo(MPFloat.Precision.FLOAT64))
}
@Test
fun `ensure longs are encoded and decoded correctly`() {
// positive fixnum
ensureLongIsEncodedAndDecodedCorrectly(0, 1)
ensureLongIsEncodedAndDecodedCorrectly(127, 1)
// negative fixnum
ensureLongIsEncodedAndDecodedCorrectly(-1, 1)
ensureLongIsEncodedAndDecodedCorrectly(-32, 1)
// uint 8
ensureLongIsEncodedAndDecodedCorrectly(128, 2)
ensureLongIsEncodedAndDecodedCorrectly(255, 2)
// uint 16
ensureLongIsEncodedAndDecodedCorrectly(256, 3)
ensureLongIsEncodedAndDecodedCorrectly(65535, 3)
// uint 32
ensureLongIsEncodedAndDecodedCorrectly(65536, 5)
ensureLongIsEncodedAndDecodedCorrectly(4294967295L, 5)
// uint 64
ensureLongIsEncodedAndDecodedCorrectly(4294967296L, 9)
ensureLongIsEncodedAndDecodedCorrectly(java.lang.Long.MAX_VALUE, 9)
// int 8
ensureLongIsEncodedAndDecodedCorrectly(-33, 2)
ensureLongIsEncodedAndDecodedCorrectly(-128, 2)
// int 16
ensureLongIsEncodedAndDecodedCorrectly(-129, 3)
ensureLongIsEncodedAndDecodedCorrectly(-32768, 3)
// int 32
ensureLongIsEncodedAndDecodedCorrectly(-32769, 5)
ensureLongIsEncodedAndDecodedCorrectly(Integer.MIN_VALUE.toLong(), 5)
// int 64
ensureLongIsEncodedAndDecodedCorrectly(-2147483649L, 9)
ensureLongIsEncodedAndDecodedCorrectly(java.lang.Long.MIN_VALUE, 9)
}
private fun ensureLongIsEncodedAndDecodedCorrectly(`val`: Long, bytes: Int) {
val value = `val`.mp
val out = ByteArrayOutputStream()
value.pack(out)
val read = Reader.read(ByteArrayInputStream(out.toByteArray()))
assertThat(out.size(), equalTo(bytes))
assertThat(read, instanceOf<Any>(MPInteger::class.java))
assertThat(read as MPInteger, equalTo(value))
}
@Test
fun `ensure strings are encoded and decoded correctly`() {
ensureStringIsEncodedAndDecodedCorrectly(0)
ensureStringIsEncodedAndDecodedCorrectly(31)
ensureStringIsEncodedAndDecodedCorrectly(32)
ensureStringIsEncodedAndDecodedCorrectly(255)
ensureStringIsEncodedAndDecodedCorrectly(256)
ensureStringIsEncodedAndDecodedCorrectly(65535)
ensureStringIsEncodedAndDecodedCorrectly(65536)
}
@Test
fun `ensure json strings are escaped correctly`() {
val builder = StringBuilder()
var c = '\u0001'
while (c < ' ') {
builder.append(c)
c++
}
val string = MPString(builder.toString())
assertThat(string.toJson(), equalTo("\"\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000b\\u000c\\r\\u000e\\u000f\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d\\u001e\\u001f\""))
}
private fun ensureStringIsEncodedAndDecodedCorrectly(length: Int) {
val value = MPString(stringWithLength(length))
val out = ByteArrayOutputStream()
value.pack(out)
val read = Reader.read(ByteArrayInputStream(out.toByteArray()))
assertThat(read, instanceOf<Any>(MPString::class.java))
assertThat(read as MPString, equalTo(value))
}
@Test
fun `ensure binaries are encoded and decoded correctly`() {
ensureBinaryIsEncodedAndDecodedCorrectly(0)
ensureBinaryIsEncodedAndDecodedCorrectly(255)
ensureBinaryIsEncodedAndDecodedCorrectly(256)
ensureBinaryIsEncodedAndDecodedCorrectly(65535)
ensureBinaryIsEncodedAndDecodedCorrectly(65536)
}
private fun ensureBinaryIsEncodedAndDecodedCorrectly(length: Int) {
val value = MPBinary(ByteArray(length))
RANDOM.nextBytes(value.value)
val out = ByteArrayOutputStream()
value.pack(out)
val read = Reader.read(ByteArrayInputStream(out.toByteArray()))
assertThat(read, instanceOf<Any>(MPBinary::class.java))
assertThat(read as MPBinary, equalTo(value))
}
@Test
fun `ensure arrays are encoded and decoded correctly`() {
ensureArrayIsEncodedAndDecodedCorrectly(0)
ensureArrayIsEncodedAndDecodedCorrectly(15)
ensureArrayIsEncodedAndDecodedCorrectly(16)
ensureArrayIsEncodedAndDecodedCorrectly(65535)
ensureArrayIsEncodedAndDecodedCorrectly(65536)
}
private fun ensureArrayIsEncodedAndDecodedCorrectly(length: Int) {
val nil = MPNil
val list = ArrayList<MPNil>(length)
for (i in 0 until length) {
list.add(nil)
}
val value = MPArray(list)
val out = ByteArrayOutputStream()
value.pack(out)
val read = Reader.read(ByteArrayInputStream(out.toByteArray()))
assertThat(read, instanceOf(MPArray::class.java))
@Suppress("UNCHECKED_CAST")
assertThat(read as MPArray<MPNil>, equalTo(value))
}
@Test
fun `ensure maps are encoded and decoded correctly`() {
ensureMapIsEncodedAndDecodedCorrectly(0)
ensureMapIsEncodedAndDecodedCorrectly(15)
ensureMapIsEncodedAndDecodedCorrectly(16)
ensureMapIsEncodedAndDecodedCorrectly(65535)
ensureMapIsEncodedAndDecodedCorrectly(65536)
}
private fun ensureMapIsEncodedAndDecodedCorrectly(size: Int) {
val nil = MPNil
val map = HashMap<MPInteger, MPNil>(size)
for (i in 0..size - 1) {
map.put(i.mp, nil)
}
val value = MPMap(map)
val out = ByteArrayOutputStream()
value.pack(out)
val read = Reader.read(ByteArrayInputStream(out.toByteArray()))
assertThat(read, instanceOf<Any>(MPMap::class.java))
@Suppress("UNCHECKED_CAST")
assertThat(read as MPMap<MPInteger, MPNil>, equalTo(value))
}
private fun stringWithLength(length: Int): String {
val result = StringBuilder(length)
for (i in 0..length - 1) {
result.append('a')
}
return result.toString()
}
private fun stream(resource: String): InputStream {
return javaClass.classLoader.getResourceAsStream(resource)
}
private fun bytes(resource: String): ByteArray {
val `in` = stream(resource)
val out = ByteArrayOutputStream()
val buffer = ByteArray(100)
var size = `in`.read(buffer)
while (size >= 0) {
out.write(buffer, 0, size)
size = `in`.read(buffer)
}
return out.toByteArray()
}
private fun string(resource: String): String {
return String(bytes(resource))
}
companion object {
private val RANDOM = Random()
}
}