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/.travis.yml b/.travis.yml new file mode 100644 index 0000000..aacb38f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: java +sudo: false # faster builds +jdk: + - oraclejdk8 + +before_install: + - pip install --user codecov + +after_success: + - codecov diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..485b2e7 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +Simple MessagePack +================== +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/ch.dissem.msgpack/msgpack/badge.svg)](https://maven-badges.herokuapp.com/maven-central/ch.dissem.msgpack/msgpack) +[![Javadoc](https://javadoc-emblem.rhcloud.com/doc/ch.dissem.msgpack/msgpack/badge.svg)](http://www.javadoc.io/doc/ch.dissem.msgpack/msgpack) +[![Apache 2](https://img.shields.io/badge/license-Apache_2.0-blue.svg)](https://raw.githubusercontent.com/Dissem/Jabit/master/LICENSE) + +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`. + +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. + + +#### Master +[![Build Status](https://travis-ci.org/Dissem/MsgPack.svg?branch=master)](https://travis-ci.org/Dissem/MsgPack) +[![Code Quality](https://img.shields.io/codacy/eb92c25247b4444383b163304e57a3ce/master.svg)](https://www.codacy.com/app/chrigu-meyer/MsgPack/dashboard?bid=4122049) +[![Test Coverage](https://codecov.io/github/Dissem/MsgPack/coverage.svg?branch=master)](https://codecov.io/github/Dissem/MsgPack?branch=master) + +#### Develop +[![Build Status](https://travis-ci.org/Dissem/MsgPack.svg?branch=develop)](https://travis-ci.org/Dissem/MsgPack?branch=develop) +[![Code Quality](https://img.shields.io/codacy/eb92c25247b4444383b163304e57a3ce/develop.svg)](https://www.codacy.com/app/chrigu-meyer/MsgPack/dashboard?bid=4118049) +[![Test Coverage](https://codecov.io/github/Dissem/MsgPack/coverage.svg?branch=develop)](https://codecov.io/github/Dissem/MsgPack?branch=develop) + +Limitations +-------------- + +* There is no fallback to BigInteger for large integer type numbers, so there might be an integer overflow when reading + too large numbers +* `MPFloat` uses the data type you're using to decide on precision (float 32 or 64) - not the actual value. E.g. 0.5 + could be saved perfectly as a float 42, but if you provide a double value, it will be stored as float 64, wasting + 4 bytes. +* If you want to use the 'ext format family', you'll need to implement and register your own `MPType` and + `MPType.Unpacker`. +* Be aware that custom `MPType.Unpacker` take precedence over the default unpackers, i.e. if you accidentally define + your unpacker to handle strings, for example, you won't be able to unpack any regular strings anymore. + +Setup +----- + +Add msgpack as Gradle dependency: +```Gradle +compile "ch.dissem.msgpack:msgpack:1.0.0" +``` + +Usage +----- + +### Serialize Data + +First, you'll need to create some msgpack objects to serialize: +```Java +MPMap> object = new MPMap<>(); +object.put(new MPString("compact"), new MPBoolean(true)); +object.put(new MPString("schema"), new MPInteger(0)); +``` +or the shorthand version for simple types: +```Java +import static ch.dissem.msgpack.types.Utils.mp; + +MPMap> object = new MPMap<>(); +object.put(mp("compact"), mp(true)); +object.put(mp("schema"), mp(0)); +``` +then just use `pack(OutputStream)`: +```Java +OutputStream out = ...; +object.pack(out); +``` + + +### Deserialize Data + +For deserializing data there is the reader object: +```Java +Reader reader = Reader.getInstance() +``` +just use `reader.read(InputStream)`. Unfortunately you'll need to make sure you got what you expected, the following +example might result in `ClassCastException` at weird places: +```Java +InputStream in = ...; +MPType read = reader.read(in); +MPMap map = (MPMap) read; +String value = map.get(mp("key")).getValue(); +``` \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..509edc2 --- /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.msgpack' + +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://dissem.ch/msgpack' + + scm { + connection 'scm:git:https://git.dissem.ch/chris/MessagePack.git' + developerConnection 'scm:git:git@git.dissem.ch:chris/MessagePack.git' + url 'https://git.dissem.ch/chris/MessagePack.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/buildSrc/src/main/groovy/ch/dissem/gradle/GitFlowVersion.groovy b/buildSrc/src/main/groovy/ch/dissem/gradle/GitFlowVersion.groovy new file mode 100644 index 0000000..869d57e --- /dev/null +++ b/buildSrc/src/main/groovy/ch/dissem/gradle/GitFlowVersion.groovy @@ -0,0 +1,57 @@ +package ch.dissem.gradle + +import org.gradle.api.Plugin +import org.gradle.api.Project + +/** + * Sets the version as follows: + *
    + *
  • If the branch is 'master', the version is set to the latest tag (which is expected to be set by Git flow)
  • + *
  • Otherwise, the version is set to the branch name, with '-SNAPSHOT' appended
  • + *
+ */ +class GitFlowVersion implements Plugin { + def getBranch(Project project) { + def stdout = new ByteArrayOutputStream() + project.exec { + commandLine 'git', 'rev-parse', '--abbrev-ref', 'HEAD' + standardOutput = stdout + } + return stdout.toString().trim() + } + + def getTag(Project project) { + def stdout = new ByteArrayOutputStream() + project.exec { + commandLine 'git', 'describe', '--abbrev=0' + standardOutput = stdout + } + return stdout.toString().trim() + } + + def isRelease(Project project) { + return "master" == getBranch(project); + } + + def getVersion(Project project) { + if (project.ext.isRelease) { + return getTag(project) + } else { + def branch = getBranch(project) + if ("develop" == branch) { + return "development-SNAPSHOT" + } + return branch.replaceAll("/", "-") + "-SNAPSHOT" + } + } + + @Override + void apply(Project project) { + project.ext.isRelease = isRelease(project) + project.version = getVersion(project) + + project.task('version') << { + println "Version deduced from git: '${project.version}'" + } + } +} diff --git a/buildSrc/src/main/resources/META-INF/gradle-plugins/gitflow-version.properties b/buildSrc/src/main/resources/META-INF/gradle-plugins/gitflow-version.properties new file mode 100644 index 0000000..1fb4f78 --- /dev/null +++ b/buildSrc/src/main/resources/META-INF/gradle-plugins/gitflow-version.properties @@ -0,0 +1 @@ +implementation-class=ch.dissem.gradle.GitFlowVersion diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..bb8beb4 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,11 @@ +# Don't change this file - override those properties in the +# gradle.properties file in your home directory instead + +signing.keyId= +signing.password= +#signing.secretKeyRingFile= + +ossrhUsername= +ossrhPassword= + +systemTestsEnabled=false \ No newline at end of file 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..e24ebf8 --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/Reader.java @@ -0,0 +1,65 @@ +/* + * 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 new file mode 100644 index 0000000..35d8328 --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/MPArray.java @@ -0,0 +1,256 @@ +/* + * 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 new file mode 100644 index 0000000..a9ae0d0 --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/MPBinary.java @@ -0,0 +1,98 @@ +/* + * 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 new file mode 100644 index 0000000..d99745a --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/MPBoolean.java @@ -0,0 +1,88 @@ +/* + * 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 new file mode 100644 index 0000000..486a89f --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/MPFloat.java @@ -0,0 +1,110 @@ +/* + * 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 new file mode 100644 index 0000000..3a15a27 --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/MPInteger.java @@ -0,0 +1,160 @@ +/* + * 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 new file mode 100644 index 0000000..f8e8d3d --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/MPMap.java @@ -0,0 +1,185 @@ +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 new file mode 100644 index 0000000..ae24831 --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/MPNil.java @@ -0,0 +1,70 @@ +/* + * 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 new file mode 100644 index 0000000..e0f50bb --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/MPString.java @@ -0,0 +1,185 @@ +/* + * 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/MPType.java b/src/main/java/ch/dissem/msgpack/types/MPType.java new file mode 100644 index 0000000..668467a --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/MPType.java @@ -0,0 +1,38 @@ +/* + * 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 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; + + String toJson(); +} 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..7bc32a3 --- /dev/null +++ b/src/main/java/ch/dissem/msgpack/types/Utils.java @@ -0,0 +1,115 @@ +/* + * 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/test/java/ch/dissem/msgpack/ReaderTest.java b/src/test/java/ch/dissem/msgpack/ReaderTest.java new file mode 100644 index 0000000..cc7b12c --- /dev/null +++ b/src/test/java/ch/dissem/msgpack/ReaderTest.java @@ -0,0 +1,270 @@ +/* + * 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/resources/demo.json b/src/test/resources/demo.json new file mode 100644 index 0000000..6118e1d --- /dev/null +++ b/src/test/resources/demo.json @@ -0,0 +1,4 @@ +{ + "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