diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ffcaeb5 --- /dev/null +++ b/CHANGELOG.md @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index 96045a9..bb4f4d9 100644 --- a/README.md +++ b/README.md @@ -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) [![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 -objects representing MessagePack types. To build, use command `./gradlew build`. +This is a simple Kotlin/Java library for handling MessagePack data. It doesn't do any object mapping, but maps to +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 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 -update. Be aware though that this doesn't necessarily applies for SNAPSHOT builds and the development branch. +_Simple MessagePack_ uses Semantic Versioning, meaning as long as the major version doesn't change, nothing should break +if you update. Be aware though that this doesn't necessarily applies for SNAPSHOT builds and the development branch. #### Master diff --git a/buildSrc/src/main/groovy/ch/dissem/gradle/GitFlowVersion.groovy b/buildSrc/src/main/groovy/ch/dissem/gradle/GitFlowVersion.groovy index 869d57e..723861c 100644 --- a/buildSrc/src/main/groovy/ch/dissem/gradle/GitFlowVersion.groovy +++ b/buildSrc/src/main/groovy/ch/dissem/gradle/GitFlowVersion.groovy @@ -50,7 +50,7 @@ class GitFlowVersion implements Plugin { project.ext.isRelease = isRelease(project) project.version = getVersion(project) - project.task('version') << { + project.task('version').doLast { println "Version deduced from git: '${project.version}'" } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 118da26..f6c23c4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Jan 17 07:22:12 CET 2017 +#Tue Sep 19 21:21:56 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME 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 diff --git a/gradlew b/gradlew index e08debe..9aa616c 100755 --- a/gradlew +++ b/gradlew @@ -154,7 +154,7 @@ if $cygwin ; then esac 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() { JVM_OPTS=("$@") } diff --git a/src/main/java/ch/dissem/msgpack/Reader.java b/src/main/java/ch/dissem/msgpack/Reader.java deleted file mode 100644 index e24ebf8..0000000 --- a/src/main/java/ch/dissem/msgpack/Reader.java +++ /dev/null @@ -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> 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)); - } -} diff --git a/src/main/java/ch/dissem/msgpack/types/MPArray.java b/src/main/java/ch/dissem/msgpack/types/MPArray.java deleted file mode 100644 index 35d8328..0000000 --- a/src/main/java/ch/dissem/msgpack/types/MPArray.java +++ /dev/null @@ -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 - */ -public class MPArray implements MPType>, List { - private List array; - - public MPArray() { - this.array = new LinkedList<>(); - } - - public MPArray(List array) { - this.array = array; - } - - @SafeVarargs - public MPArray(T... objects) { - this.array = Arrays.asList(objects); - } - - public List 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 iterator() { - return array.iterator(); - } - - @Override - public Object[] toArray() { - return array.toArray(); - } - - @Override - @SuppressWarnings("SuspiciousToArrayCall") - public 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 collection) { - return array.addAll(collection); - } - - @Override - public boolean addAll(int i, Collection 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 listIterator() { - return array.listIterator(); - } - - @Override - public ListIterator listIterator(int i) { - return array.listIterator(i); - } - - @Override - public List 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 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 { - 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> 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> list = new LinkedList<>(); - for (int i = 0; i < size; i++) { - MPType value = reader.read(in); - list.add(value); - } - return new MPArray<>(list); - } - } -} diff --git a/src/main/java/ch/dissem/msgpack/types/MPBinary.java b/src/main/java/ch/dissem/msgpack/types/MPBinary.java deleted file mode 100644 index a9ae0d0..0000000 --- a/src/main/java/ch/dissem/msgpack/types/MPBinary.java +++ /dev/null @@ -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 { - 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 { - 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()); - } - } -} diff --git a/src/main/java/ch/dissem/msgpack/types/MPBoolean.java b/src/main/java/ch/dissem/msgpack/types/MPBoolean.java deleted file mode 100644 index d99745a..0000000 --- a/src/main/java/ch/dissem/msgpack/types/MPBoolean.java +++ /dev/null @@ -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 { - 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 { - - 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)); - } - } - } -} diff --git a/src/main/java/ch/dissem/msgpack/types/MPFloat.java b/src/main/java/ch/dissem/msgpack/types/MPFloat.java deleted file mode 100644 index 486a89f..0000000 --- a/src/main/java/ch/dissem/msgpack/types/MPFloat.java +++ /dev/null @@ -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 { - - 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 { - 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)); - } - } - } -} diff --git a/src/main/java/ch/dissem/msgpack/types/MPInteger.java b/src/main/java/ch/dissem/msgpack/types/MPInteger.java deleted file mode 100644 index 3a15a27..0000000 --- a/src/main/java/ch/dissem/msgpack/types/MPInteger.java +++ /dev/null @@ -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 { - 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 { - 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)); - } - } - } - } -} diff --git a/src/main/java/ch/dissem/msgpack/types/MPMap.java b/src/main/java/ch/dissem/msgpack/types/MPMap.java deleted file mode 100644 index f8e8d3d..0000000 --- a/src/main/java/ch/dissem/msgpack/types/MPMap.java +++ /dev/null @@ -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 - * @param - */ -public class MPMap implements MPType>, Map { - private Map map; - - public MPMap() { - this.map = new LinkedHashMap<>(); - } - - public MPMap(Map map) { - this.map = map; - } - - @Override - public Map 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 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 map) { - this.map.putAll(map); - } - - @Override - public void clear() { - map.clear(); - } - - @Override - public Set keySet() { - return map.keySet(); - } - - @Override - public Collection values() { - return map.values(); - } - - @Override - public Set> 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> iterator = map.entrySet().iterator(); - String indent2 = indent + " "; - while (iterator.hasNext()) { - Map.Entry 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 { - 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> 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> 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); - } - } -} diff --git a/src/main/java/ch/dissem/msgpack/types/MPNil.java b/src/main/java/ch/dissem/msgpack/types/MPNil.java deleted file mode 100644 index ae24831..0000000 --- a/src/main/java/ch/dissem/msgpack/types/MPNil.java +++ /dev/null @@ -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 { - 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 { - - 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(); - } - } -} diff --git a/src/main/java/ch/dissem/msgpack/types/MPString.java b/src/main/java/ch/dissem/msgpack/types/MPString.java deleted file mode 100644 index e0f50bb..0000000 --- a/src/main/java/ch/dissem/msgpack/types/MPString.java +++ /dev/null @@ -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. - *

- * The default encoding is UTF-8. - *

- */ -public class MPString implements MPType, 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? - *

- * It will set the encoding for all {@link MPString}s, but if you have inconsistent encoding in your - * format you're lost anyway. - *

- */ - 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 { - 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)); - } - } -} diff --git a/src/main/java/ch/dissem/msgpack/types/Utils.java b/src/main/java/ch/dissem/msgpack/types/Utils.java deleted file mode 100644 index 7bc32a3..0000000 --- a/src/main/java/ch/dissem/msgpack/types/Utils.java +++ /dev/null @@ -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 count 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; - } -} diff --git a/src/main/kotlin/ch/dissem/msgpack/Reader.kt b/src/main/kotlin/ch/dissem/msgpack/Reader.kt new file mode 100644 index 0000000..7798d77 --- /dev/null +++ b/src/main/kotlin/ch/dissem/msgpack/Reader.kt @@ -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>() + + 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)) + } +} diff --git a/src/main/kotlin/ch/dissem/msgpack/types/MPArray.kt b/src/main/kotlin/ch/dissem/msgpack/types/MPArray.kt new file mode 100644 index 0000000..534c72e --- /dev/null +++ b/src/main/kotlin/ch/dissem/msgpack/types/MPArray.kt @@ -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 content type + */ +data class MPArray>(override val value: MutableList = mutableListOf()) : MPType>, MutableList { + + @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) = value.containsAll(elements) + + override fun addAll(elements: Collection) = value.addAll(elements) + + override fun addAll(index: Int, elements: Collection) = value.addAll(index, elements) + + override fun removeAll(elements: Collection) = value.removeAll(elements) + + override fun retainAll(elements: Collection) = 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> { + + 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> { + 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>() + for (i in 0 until size) { + val value = reader.read(input) + list.add(value) + } + return MPArray(list) + } + } +} diff --git a/src/main/kotlin/ch/dissem/msgpack/types/MPBinary.kt b/src/main/kotlin/ch/dissem/msgpack/types/MPBinary.kt new file mode 100644 index 0000000..8ebb664 --- /dev/null +++ b/src/main/kotlin/ch/dissem/msgpack/types/MPBinary.kt @@ -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 { + + @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 { + 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()) + } + } +} diff --git a/src/main/kotlin/ch/dissem/msgpack/types/MPBoolean.kt b/src/main/kotlin/ch/dissem/msgpack/types/MPBoolean.kt new file mode 100644 index 0000000..09dc495 --- /dev/null +++ b/src/main/kotlin/ch/dissem/msgpack/types/MPBoolean.kt @@ -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 { + + @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 { + + 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 + } +} diff --git a/src/main/kotlin/ch/dissem/msgpack/types/MPFloat.kt b/src/main/kotlin/ch/dissem/msgpack/types/MPFloat.kt new file mode 100644 index 0000000..2225205 --- /dev/null +++ b/src/main/kotlin/ch/dissem/msgpack/types/MPFloat.kt @@ -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 { + + 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 { + 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)) + } + } +} diff --git a/src/main/kotlin/ch/dissem/msgpack/types/MPInteger.kt b/src/main/kotlin/ch/dissem/msgpack/types/MPInteger.kt new file mode 100644 index 0000000..21ccb88 --- /dev/null +++ b/src/main/kotlin/ch/dissem/msgpack/types/MPInteger.kt @@ -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 { + + 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 { + 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 + } + } +} diff --git a/src/main/kotlin/ch/dissem/msgpack/types/MPMap.kt b/src/main/kotlin/ch/dissem/msgpack/types/MPMap.kt new file mode 100644 index 0000000..87f855f --- /dev/null +++ b/src/main/kotlin/ch/dissem/msgpack/types/MPMap.kt @@ -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 key type + * @param value type + */ +data class MPMap, V : MPType<*>>(override val value: MutableMap = LinkedHashMap()) : MPType>, MutableMap { + + + @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) = value.putAll(from) + + override fun clear() = value.clear() + + override val keys: MutableSet + get() = value.keys + + override val values: MutableCollection + get() = value.values + + override val entries: MutableSet> + 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> { + + 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<*>> { + 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<*>>() + for (i in 0..size - 1) { + val key = reader.read(input) + val value = reader.read(input) + map.put(key, value) + } + return MPMap(map) + } + } +} diff --git a/src/main/kotlin/ch/dissem/msgpack/types/MPNil.kt b/src/main/kotlin/ch/dissem/msgpack/types/MPNil.kt new file mode 100644 index 0000000..73326f1 --- /dev/null +++ b/src/main/kotlin/ch/dissem/msgpack/types/MPNil.kt @@ -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 { + + 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 { + + 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 +} diff --git a/src/main/kotlin/ch/dissem/msgpack/types/MPString.kt b/src/main/kotlin/ch/dissem/msgpack/types/MPString.kt new file mode 100644 index 0000000..b357339 --- /dev/null +++ b/src/main/kotlin/ch/dissem/msgpack/types/MPString.kt @@ -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, 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 { + 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") + } +} diff --git a/src/main/java/ch/dissem/msgpack/types/MPType.java b/src/main/kotlin/ch/dissem/msgpack/types/MPType.kt similarity index 60% rename from src/main/java/ch/dissem/msgpack/types/MPType.java rename to src/main/kotlin/ch/dissem/msgpack/types/MPType.kt index 668467a..57a3a6f 100644 --- a/src/main/java/ch/dissem/msgpack/types/MPType.java +++ b/src/main/kotlin/ch/dissem/msgpack/types/MPType.kt @@ -14,25 +14,27 @@ * limitations under the License. */ -package ch.dissem.msgpack.types; +package ch.dissem.msgpack.types -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream /** * Representation of some msgpack encoded data. */ -public interface MPType { - interface Unpacker { - boolean is(int firstByte); +interface MPType { + interface Unpacker> { + 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 } diff --git a/src/main/kotlin/ch/dissem/msgpack/types/Utils.kt b/src/main/kotlin/ch/dissem/msgpack/types/Utils.kt new file mode 100644 index 0000000..f8abcbd --- /dev/null +++ b/src/main/kotlin/ch/dissem/msgpack/types/Utils.kt @@ -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 + } +} diff --git a/src/test/java/ch/dissem/msgpack/ReaderTest.java b/src/test/java/ch/dissem/msgpack/ReaderTest.java deleted file mode 100644 index cc7b12c..0000000 --- a/src/test/java/ch/dissem/msgpack/ReaderTest.java +++ /dev/null @@ -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> 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> array = new MPArray<>( - mp(new byte[]{1, 3, 3, 7}), - mp(false), - mp(Math.PI), - mp(1.5f), - mp(42), - new MPMap(), - 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>) 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 list = new ArrayList<>(length); - for (int i = 0; i < length; i++) { - list.add(nil); - } - MPArray 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) 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 map = new HashMap<>(size); - for (int i = 0; i < size; i++) { - map.put(mp(i), nil); - } - MPMap 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) 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"); - } -} diff --git a/src/test/kotlin/ch/dissem/msgpack/ReaderTest.kt b/src/test/kotlin/ch/dissem/msgpack/ReaderTest.kt new file mode 100644 index 0000000..000e015 --- /dev/null +++ b/src/test/kotlin/ch/dissem/msgpack/ReaderTest.kt @@ -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>() + 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, + "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>, 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(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(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(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(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(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(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, 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(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(MPMap::class.java)) + @Suppress("UNCHECKED_CAST") + assertThat(read as MPMap, 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() + } +}