diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..98d31cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,162 @@ +# Created by https://www.gitignore.io + +*.log + +### Gradle ### +.gradle +build/ +classes/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm + +*.iml + +## Directory-based project format: +.idea/ +# if you remove the above rule, at least ignore the following: + +# User-specific stuff: +# .idea/workspace.xml +# .idea/tasks.xml +# .idea/dictionaries + +# Sensitive or high-churn files: +# .idea/dataSources.ids +# .idea/dataSources.xml +# .idea/sqlDataSources.xml +# .idea/dynamic.xml +# .idea/uiDesigner.xml + +# Gradle: +# .idea/gradle.xml +# .idea/libraries + +# Mongo Explorer plugin: +# .idea/mongoSettings.xml + +## File-based project format: +*.ipr +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties + + +### Eclipse ### +*.pydevproject +.metadata +.gradle +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath + +# Eclipse Core +.project + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# PDT-specific +.buildpath + +# sbteclipse plugin +.target + +# TeXlipse plugin +.texlipse + + +### Linux ### +*~ + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + + +### OSX ### +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### Windows ### +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..3dfcde7 --- /dev/null +++ b/build.gradle @@ -0,0 +1,92 @@ +apply plugin: 'java' +apply plugin: 'maven' +apply plugin: 'signing' +apply plugin: 'jacoco' +apply plugin: 'gitflow-version' + +sourceCompatibility = 1.7 +group = 'ch.dissem.jabit' + +repositories { + mavenCentral() +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.11' +} + +test { + testLogging { + exceptionFormat = 'full' + } +} + +task javadocJar(type: Jar) { + classifier = 'javadoc' + from javadoc +} + +task sourcesJar(type: Jar) { + classifier = 'sources' + from sourceSets.main.allSource +} + +artifacts { + archives javadocJar, sourcesJar +} + +signing { + required { isRelease && project.getProperties().get("signing.keyId")?.length() > 0 } + sign configurations.archives +} + +uploadArchives { + repositories { + mavenDeployer { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + + repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + + snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + + pom.project { + name 'msgpack' + packaging 'jar' + url 'https://github.com/Dissem/msgpack' + + scm { + connection 'scm:git:https://github.com/Dissem/msgpack.git' + developerConnection 'scm:git:git@github.com:Dissem/msgpack.git' + url 'https://github.com/Dissem/msgpack.git' + } + + licenses { + license { + name 'The Apache License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + + developers { + developer { + name 'Christian Basler' + email 'chrigu.meyer@gmail.com' + } + } + } + } + } +} + +jacocoTestReport { + reports { + xml.enabled = true + html.enabled = true + } +} + +check.dependsOn jacocoTestReport diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..6ffa237 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..118da26 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Jan 17 07:22:12 CET 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 diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..e08debe --- /dev/null +++ b/gradlew @@ -0,0 +1,169 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an value, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..84c2605 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'msgpack' + diff --git a/src/main/java/ch/dissem/msgpack/Reader.java b/src/main/java/ch/dissem/msgpack/Reader.java new file mode 100644 index 0000000..51d1518 --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/Reader.java @@ -0,0 +1,44 @@ +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 List> unpackers = new LinkedList>(); + + public Reader() { + unpackers.add(new MPNil.Unpacker()); + unpackers.add(new MPBoolean.Unpacker()); + unpackers.add(new MPInteger.Unpacker()); + unpackers.add(new MPFloat.Unpacker()); + unpackers.add(new MPDouble.Unpacker()); + unpackers.add(new MPString.Unpacker()); + unpackers.add(new MPBinary.Unpacker()); + unpackers.add(new MPMap.Unpacker(this)); + unpackers.add(new MPArray.Unpacker(this)); + } + + /** + * Register your own extensions + */ + public void register(MPType.Unpacker unpacker) { + unpackers.add(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 new file mode 100644 index 0000000..80bdad8 --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/MPArray.java @@ -0,0 +1,101 @@ +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.*; + +public class MPArray implements MPType> { + private List array; + + public MPArray(List array) { + this.array = array; + } + + 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 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 String toString() { + StringBuilder result = new StringBuilder(); + result.append('['); + Iterator iterator = array.iterator(); + while (iterator.hasNext()) { + T item = iterator.next(); + result.append(item.toString()); + if (iterator.hasNext()) { + result.append(", "); + } + } + 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 new file mode 100644 index 0000000..2414c1e --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/MPBinary.java @@ -0,0 +1,74 @@ +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; + +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 null; // TODO base64 + } + + 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() << 8 | 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 new file mode 100644 index 0000000..f54b4a8 --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/MPBoolean.java @@ -0,0 +1,64 @@ +package ch.dissem.msgpack.types; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Objects; + +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); + } + + 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/MPDouble.java b/src/main/java/ch/dissem/msgpack/types/MPDouble.java new file mode 100644 index 0000000..89575cf --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/MPDouble.java @@ -0,0 +1,59 @@ +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; + +public class MPDouble implements MPType { + private double value; + + public MPDouble(double value) { + this.value = value; + } + + @Override + public Double getValue() { + return value; + } + + public void pack(OutputStream out) throws IOException { + out.write(0xCB); + out.write(ByteBuffer.allocate(8).putDouble(value).array()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MPDouble mpDouble = (MPDouble) o; + return Double.compare(mpDouble.value, value) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public String toString() { + return String.valueOf(value); + } + + public static class Unpacker implements MPType.Unpacker { + public boolean is(int firstByte) { + return firstByte == 0xCB; + } + + public MPDouble unpack(int firstByte, InputStream in) throws IOException { + if (firstByte == 0xCB) { + return new MPDouble(bytes(in, 8).getDouble()); + } else { + throw new IllegalArgumentException(String.format("Unexpected first byte 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 new file mode 100644 index 0000000..30a52a3 --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/MPFloat.java @@ -0,0 +1,59 @@ +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; + +public class MPFloat implements MPType { + private float value; + + public MPFloat(float value) { + this.value = value; + } + + @Override + public Float getValue() { + return value; + } + + public void pack(OutputStream out) throws IOException { + out.write(0xCA); + out.write(ByteBuffer.allocate(4).putFloat(value).array()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MPFloat mpFloat = (MPFloat) o; + return Float.compare(mpFloat.value, value) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public String toString() { + return String.valueOf(value); + } + + public static class Unpacker implements MPType.Unpacker { + public boolean is(int firstByte) { + return firstByte == 0xCA; + } + + public MPFloat unpack(int firstByte, InputStream in) throws IOException { + if (firstByte == 0xCA) { + return new MPFloat(bytes(in, 4).getFloat()); + } else { + 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 new file mode 100644 index 0000000..a58e8b7 --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/MPInteger.java @@ -0,0 +1,124 @@ +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; + +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 >= 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); + } + + 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: + return new MPInteger(in.read() << 24 | in.read() << 16 | in.read() << 8 | in.read()); + 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 new file mode 100644 index 0000000..a105149 --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/MPMap.java @@ -0,0 +1,105 @@ +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.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +public class MPMap implements MPType> { + private Map map; + + 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 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() { + StringBuilder result = new StringBuilder(); + result.append('{'); + Iterator> iterator = map.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry item = iterator.next(); + result.append(item.getKey().toString()); + result.append(": "); + result.append(item.getValue().toString()); + if (iterator.hasNext()) { + result.append(", "); + } + } + 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 == 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 new file mode 100644 index 0000000..27122e9 --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/MPNil.java @@ -0,0 +1,46 @@ +package ch.dissem.msgpack.types; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +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 "nil"; + } + + 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 new file mode 100644 index 0000000..bbee5cc --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/MPString.java @@ -0,0 +1,78 @@ +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; + +public class MPString implements MPType { + private String value; + + public MPString(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void pack(OutputStream out) throws IOException { + int size = value.length(); + if (size < 16) { + out.write(0b10100000 + size); + } else if (size < 256) { + out.write(0xD9); + out.write((byte) size); + } else if (size < 65536) { + out.write(0xDA); + out.write(ByteBuffer.allocate(2).putShort((short) size).array()); + } else { + out.write(0xDB); + out.write(ByteBuffer.allocate(4).putInt(size).array()); + } + out.write(value.getBytes("UTF-8")); + } + + @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 String toString() { + return '"' + value + '"'; // FIXME: escape value + } + + public static class Unpacker implements MPType.Unpacker { + public boolean is(int firstByte) { + return firstByte == 0xD9 || firstByte == 0xDA || firstByte == 0xDB || (firstByte & 0b11100000) == 0b10100000; + } + + public MPString unpack(int firstByte, InputStream in) throws IOException { + int size; + if ((firstByte & 0b11100000) == 0b10100000) { + size = firstByte & 0b00011111; + } else if (firstByte == 0xD9) { + size = in.read() << 8 | in.read(); + } else if (firstByte == 0xDA) { + size = in.read() << 8 | in.read(); + } else if (firstByte == 0xDB) { + 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(), "UTF-8")); + } + } +} diff --git a/src/main/java/ch/dissem/msgpack/types/MPType.java b/src/main/java/ch/dissem/msgpack/types/MPType.java new file mode 100644 index 0000000..d85b379 --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/MPType.java @@ -0,0 +1,20 @@ +package ch.dissem.msgpack.types; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Representation for some msgpack encoded data. + */ +public interface MPType { + interface Unpacker { + boolean is(int firstByte); + + M unpack(int firstByte, InputStream in) throws IOException; + } + + T getValue(); + + void pack(OutputStream out) throws IOException; +} diff --git a/src/main/java/ch/dissem/msgpack/types/Utils.java b/src/main/java/ch/dissem/msgpack/types/Utils.java new file mode 100644 index 0000000..de7ff00 --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/Utils.java @@ -0,0 +1,20 @@ +package ch.dissem.msgpack.types; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +class Utils { + 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); + } +} diff --git a/src/test/java/ch/dissem/msgpack/ReaderTest.java b/src/test/java/ch/dissem/msgpack/ReaderTest.java new file mode 100644 index 0000000..cb7975d --- /dev/null +++ b/src/test/java/ch/dissem/msgpack/ReaderTest.java @@ -0,0 +1,74 @@ +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.HashMap; +import java.util.LinkedHashMap; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + +public class ReaderTest { + private Reader reader = new Reader(); + + @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 { + MPMap> object = new MPMap<>(new LinkedHashMap>()); + object.getValue().put(new MPString("compact"), new MPBoolean(true)); + object.getValue().put(new MPString("schema"), new MPInteger(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<>( +// new MPBinary(new byte[]{1, 3, 3, 7}), + new MPBoolean(false), + new MPDouble(Math.PI), + new MPFloat(1.5f), + new MPInteger(42), + new MPMap<>(new HashMap()), + new MPNil(), + new MPString("yay!") // TODO: emoji + ); + 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)); + } + + 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/resources/demo.json b/src/test/resources/demo.json new file mode 100644 index 0000000..b091f84 --- /dev/null +++ b/src/test/resources/demo.json @@ -0,0 +1 @@ +{"compact": true, "schema": 0} \ No newline at end of file diff --git a/src/test/resources/demo.mp b/src/test/resources/demo.mp new file mode 100644 index 0000000..87ce865 Binary files /dev/null and b/src/test/resources/demo.mp differ