diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..992d272 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = false +max_line_length = 120 +tab_width = 4 +ij_javascript_space_before_function_left_parenth = false + +[{*.json,.eslintrc}] +indent_size = 2 diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..28c5bfe --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,41 @@ +{ + "env": { + "commonjs": true, + "es2017": true, + "node": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 2018 + }, + "rules": { + "indent": [ + "error", + 4, + { + "SwitchCase": 1 + } + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "double" + ], + "semi": [ + "error", + "always" + ], + "no-var": "error", + "prefer-const": "error", + "curly": "error", + "prefer-template": "error", + "brace-style": "error", + "padded-blocks": [ + "error", + "never" + ] + } +} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..f0ae348 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,26 @@ +name: Java CI + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Cache Maven packages + uses: actions/cache@v2 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + - name: Build with Maven + run: mvn --batch-mode --update-snapshots verify + - run: mkdir staging && cp target/*.jar staging + - uses: actions/upload-artifact@v2 + with: + name: Package + path: staging \ No newline at end of file diff --git a/.gitignore b/.gitignore index b473ad9..623a45d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ +/node +/out /node_modules/ /test/*.class /package-lock.json /.nyc_output/ /coverage/ +/.idea +*.iml +/target \ No newline at end of file diff --git a/package.json b/package.json index 3f7d9c8..14147b6 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,13 @@ "name": "java-deserialization", "version": "0.1.0", "description": "Parse Java object serialization format using pure JavaScript", - "main": "src/index.js", + "main": "src/main/node/index.js", "scripts": { - "test": "nyc --reporter=html --reporter=text-summary mocha test/*.js", - "gentest": "cd test && javac *.java && java GenerateTestCases > generated.js && npm test" + "built": "echo 'build here'", + "lint": "echo 'no linting setup yet'", + "test": "nyc --reporter=html --reporter=text-summary mocha src/test/node/*.js", + "gentest": "java -classpath target/classes io.github.gagern.nodeJavaDeserialization.GenerateTestCases > src/test/node/generated.js && npm test", + "package": "echo 'make publishable artifact here'" }, "repository": { "type": "git", @@ -24,11 +27,19 @@ }, "homepage": "https://github.com/gagern/nodeJavaDeserialization#readme", "devDependencies": { - "chai": "^4.1.2", - "mocha": "^4.1.0", - "nyc": "^11.4.1" + "@types/node": "^10.17.55", + "chai": "^4.3.4", + "eslint": "^7.22.0", + "eslint-config-airbnb-base": "^14.2.1", + "eslint-plugin-import": "^2.22.1", + "mocha": "^8.3.2", + "nyc": "^15.1.0", + "typescript": "^4.2.3" }, "dependencies": { - "long": "^3.2.0" + "long": "^4.0.0" + }, + "engines": { + "node": ">=18.0.0" } } diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..225b7f6 --- /dev/null +++ b/pom.xml @@ -0,0 +1,125 @@ + + + 4.0.0 + + + io.github.gagern + 1.0.0-SNAPSHOT + nodeJavaDeserialization + jar + + + 1.13.4 + v20.12.1 + 10.5.0 + + + 1.8 + 1.8 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 1.8 + 1.8 + + + + com.github.eirslett + frontend-maven-plugin + ${frontend-maven-plugin.version} + + + install node and npm + + install-node-and-npm + + + ${node.version} + ${npm.version} + + + + npm install + + npm + + generate-resources + + install + + + + npm lint + + npm + + generate-resources + + run lint + + + + npm gentest + + npm + + test-compile + + run gentest + + + + npm test + + npm + + test + + run test + + + + + + + + + + + + + + + + + + + + + + + + + npm package + + npm + + package + + run package + + + + + + + diff --git a/test/GenerateTestCases.java b/src/main/java/io/github/gagern/nodeJavaDeserialization/GenerateTestCases.java similarity index 92% rename from test/GenerateTestCases.java rename to src/main/java/io/github/gagern/nodeJavaDeserialization/GenerateTestCases.java index 2efc662..e4af550 100644 --- a/test/GenerateTestCases.java +++ b/src/main/java/io/github/gagern/nodeJavaDeserialization/GenerateTestCases.java @@ -1,3 +1,5 @@ +package io.github.gagern.nodeJavaDeserialization; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; @@ -10,12 +12,16 @@ class GenerateTestCases { public static void main(String[] args) throws Exception { System.out.print - ("'use strict';\n" + + ("//This is a generated file from GeneratedTestCases.java\n" + + "'use strict';\n" + "\n" + "const chai = require('chai');\n" + "const expect = chai.expect;\n" + "const zlib = require('zlib');\n" + - "const javaDeserialization = require('../');\n" + + "const javaDeserialization = require('../../main/node');\n" + + "\n" + + "// Register a classdata parser for the io.github.gagern.nodeJavaDeserialization.CompletelyCustomFormat test.\n" + + "javaDeserialization.registerClassDataParser('io.github.gagern.nodeJavaDeserialization.CompletelyCustomFormat', '0000000000000001', cls => ({}));\n" + "\n" + "function testCase(b64data, checks) {\n" + " return function() {\n" + diff --git a/test/SerializationTestCase.java b/src/main/java/io/github/gagern/nodeJavaDeserialization/SerializationTestCase.java similarity index 85% rename from test/SerializationTestCase.java rename to src/main/java/io/github/gagern/nodeJavaDeserialization/SerializationTestCase.java index 2991509..09efd38 100644 --- a/test/SerializationTestCase.java +++ b/src/main/java/io/github/gagern/nodeJavaDeserialization/SerializationTestCase.java @@ -1,3 +1,5 @@ +package io.github.gagern.nodeJavaDeserialization; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/test/TestCases.java b/src/main/java/io/github/gagern/nodeJavaDeserialization/TestCases.java similarity index 84% rename from test/TestCases.java rename to src/main/java/io/github/gagern/nodeJavaDeserialization/TestCases.java index c230baa..0a437ff 100644 --- a/test/TestCases.java +++ b/src/main/java/io/github/gagern/nodeJavaDeserialization/TestCases.java @@ -1,3 +1,5 @@ +package io.github.gagern.nodeJavaDeserialization; + import java.io.Serializable; import java.util.HashMap; @@ -41,6 +43,7 @@ class CustomFormat implements Serializable { private static final long serialVersionUID = 0x1; private int foo = 12345; + private String bar = "Hello, World!"; private void writeObject(java.io.ObjectOutputStream out) throws java.io.IOException @@ -59,6 +62,32 @@ private void readObjectNoData() throws java.io.ObjectStreamException { } } +class CompletelyCustomFormat implements Serializable { + private static final long serialVersionUID = 0x1; + + // These are here to cause an error if there is an attempt to parse + // the defaultWriteObject format. + private int foo; + private String bar; + + private void writeObject(java.io.ObjectOutputStream out) + throws java.io.IOException + { + out.writeObject("Hello, World!"); + out.writeInt(12345); + + // These numbers should result in "test" visible in the base64 output ;) + byte[] data = { -75, -21, 45, 0, -75, -21, 45, 0, -75, -21, 45 }; + out.write(data); + } + + private void readObject(java.io.ObjectInputStream in) + throws java.io.IOException, ClassNotFoundException { } + + private void readObjectNoData() + throws java.io.ObjectStreamException { } +} + class External implements Serializable, java.io.Externalizable { // These numbers should result in "test" visible in the base64 output ;) @@ -169,23 +198,23 @@ public void nulls() throws Exception { @SerializationTestCase public void inheritedField() throws Exception { writeObject(new DerivedClassWithAnotherField()); - checkStrictEqual("itm.class.name", "'DerivedClassWithAnotherField'"); - checkStrictEqual("itm.class.super.name", "'BaseClassWithField'"); + checkStrictEqual("itm.class.name", "'io.github.gagern.nodeJavaDeserialization.DerivedClassWithAnotherField'"); + checkStrictEqual("itm.class.super.name", "'io.github.gagern.nodeJavaDeserialization.BaseClassWithField'"); checkStrictEqual("itm.class.super.super", "null"); - checkStrictEqual("itm.extends.DerivedClassWithAnotherField.bar", "234"); - checkStrictEqual("itm.extends.DerivedClassWithAnotherField.foo", "undefined"); - checkStrictEqual("itm.extends.BaseClassWithField.foo", "123"); + checkStrictEqual("itm.extends['io.github.gagern.nodeJavaDeserialization.DerivedClassWithAnotherField'].bar", "234"); + checkStrictEqual("itm.extends['io.github.gagern.nodeJavaDeserialization.DerivedClassWithAnotherField'].foo", "undefined"); + checkStrictEqual("itm.extends['io.github.gagern.nodeJavaDeserialization.BaseClassWithField'].foo", "123"); checkStrictEqual("itm.bar", "234"); checkStrictEqual("itm.foo", "123"); } @SerializationTestCase public void duplicateField() throws Exception { writeObject(new DerivedClassWithSameField()); - checkStrictEqual("itm.class.name", "'DerivedClassWithSameField'"); - checkStrictEqual("itm.class.super.name", "'BaseClassWithField'"); + checkStrictEqual("itm.class.name", "'io.github.gagern.nodeJavaDeserialization.DerivedClassWithSameField'"); + checkStrictEqual("itm.class.super.name", "'io.github.gagern.nodeJavaDeserialization.BaseClassWithField'"); checkStrictEqual("itm.class.super.super", "null"); - checkStrictEqual("itm.extends.DerivedClassWithSameField.foo", "345"); - checkStrictEqual("itm.extends.BaseClassWithField.foo", "123"); + checkStrictEqual("itm.extends['io.github.gagern.nodeJavaDeserialization.DerivedClassWithSameField'].foo", "345"); + checkStrictEqual("itm.extends['io.github.gagern.nodeJavaDeserialization.BaseClassWithField'].foo", "123"); checkStrictEqual("itm.foo", "345"); } @@ -233,7 +262,7 @@ public void enums() throws Exception { checkInstanceof("one", "String"); checkLooseEqual("one", "'ONE'"); checkNotStrictEqual("one", "'ONE'"); - checkStrictEqual("one.class.name", "'SomeEnum'"); + checkStrictEqual("one.class.name", "'io.github.gagern.nodeJavaDeserialization.SomeEnum'"); checkThat("one.class.isEnum"); checkStrictEqual("one.class.super.name", "'java.lang.Enum'"); checkStrictEqual("one.class.super.super", "null"); @@ -264,6 +293,18 @@ public void exception() throws Exception { "'b5eb2d00b5eb2d00b5eb2d'"); checkStrictEqual("itm['@'][1]", "'and more'"); checkStrictEqual("itm.foo", "12345"); + checkStrictEqual("itm.bar", "'Hello, World!'"); + } + + @SerializationTestCase public void completelyCustomFormat() throws Exception { + writeObject(new CompletelyCustomFormat()); + checkArray("itm['@']"); + checkLength("itm['@']", 2); + checkStrictEqual("itm['@'][0]", "'Hello, World!'"); + // This is the primitive data lump (TC_BLOCKDATA). + checkThat("Buffer.isBuffer(itm['@'][1])"); + checkStrictEqual("itm['@'][1].toString('hex')", + "'00003039b5eb2d00b5eb2d00b5eb2d'"); } @SerializationTestCase public void externalizable() throws Exception { @@ -371,7 +412,7 @@ public void enumMap() throws Exception { checkStrictEqual("itm.obj.THREE", "'baz'"); checkStrictEqual("itm.obj.ONE.value", "123"); checkKeys("itm.obj", "'ONE', 'THREE'"); - checkStrictEqual("itm.keyType.name", "'SomeEnum'"); + checkStrictEqual("itm.keyType.name", "'io.github.gagern.nodeJavaDeserialization.SomeEnum'"); checkThat("itm.keyType.isEnum"); checkInstanceof("itm.map", "Map"); checkStrictEqual("itm.map.get(three)", "'baz'"); @@ -411,5 +452,4 @@ public void hashSet() throws Exception { checkStrictEqual("itm.set.size", "2"); checkThat("itm.set.has('foo')"); } - } diff --git a/src/index.js b/src/main/node/index.js similarity index 84% rename from src/index.js rename to src/main/node/index.js index cf53263..f9d6d5d 100644 --- a/src/index.js +++ b/src/main/node/index.js @@ -1,16 +1,16 @@ /* * Copyright (c) 2015,2018 Martin von Gagern - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -22,10 +22,13 @@ "use strict"; -var Parser = require("./parser.js"); +const Parser = require("./parser.js"); require("./util.js"); module.exports.parse = function parse(buf) { - var parser = new Parser(buf); + const parser = new Parser(buf); return parser.contents; -} +}; + +module.exports.registerClassDataParser = Parser.registerClassDataParser; +module.exports.registerPostProcessor = Parser.registerPostProcessor; diff --git a/src/main/node/parser.js b/src/main/node/parser.js new file mode 100644 index 0000000..c7f2628 --- /dev/null +++ b/src/main/node/parser.js @@ -0,0 +1,443 @@ +/** + * Copyright (c) 2015,2018 Martin von Gagern + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// See http://docs.oracle.com/javase/7/docs/platform/serialization/spec/protocol.html for reference + +"use strict"; + +const assert = require("assert"); +const Long = require("long"); + +const names = [ + "Null", "Reference", "ClassDesc", "Object", "String", "Array", "Class", "BlockData", "EndBlockData", + "Reset", "BlockDataLong", "Exception", "LongString", "ProxyClassDesc", "Enum", +]; + +const endBlock = {}; + +/** @type {Object.} */ +const classDataParsers = {}; + +/** @type {Object.} */ +const classPostProcessors = {}; + +/** @type {Object.} */ +const typeHandlers = { + "Null": function() { + return null; + }, + "Reference": function() { + return this.handles[this.readInt32()]; + }, + "ClassDesc": function() { + const res = {}; + res.name = this.utf(); + res.serialVersionUID = this.readHex(8); + this.newHandle(res); + res.flags = this.readUInt8(); + res.isEnum = !!(res.flags & 0x10); + const count = this.readUInt16(); + res.fields = []; + for (let i = 0; i < count; ++i) { + res.fields.push(this.fieldDesc()); + } + res.annotations = this.annotations(); + res.super = this.classDesc(); + return res; + }, + "Object": function() { + const res = Object.defineProperties({}, { + "class": { + configurable: true, + value: this.classDesc(), + }, + "extends": { + configurable: true, + value: {}, + }, + }); + this.newHandle(res); + this.recursiveClassData(res.class, res); + return res; + }, + "String": function() { + return this.newHandle(this.utf()); + }, + "Array": function() { + const classDesc = this.classDesc(); + const res = Object.defineProperties([], { + "class": { + configurable: true, + value: classDesc, + }, + "extends": { + configurable: true, + value: {}, + }, + }); + this.newHandle(res); + const len = this.readInt32(); + const handler = this.primHandler(classDesc.name.charAt(1)); + res.length = len; + for (let i = 0; i < len; ++i) { + res[i] = handler.call(this); + } + return res; + }, + "Class": function() { + return this.newHandle(this.classDesc()); + }, + "BlockData": function() { + const len = this.readUInt8(); + const res = this.buf.slice(this.pos, this.pos + len); + this.pos += len; + return res; + }, + "EndBlockData": function() { + return endBlock; + }, + "BlockDataLong": function() { + const len = this.readUInt32(); + const res = this.buf.slice(this.pos, this.pos + len); + this.pos += len; + return res; + }, + "LongString": function() { + return this.newHandle(this.utfLong()); + }, + "Enum": function() { + const clazz = this.classDesc(); + const deferredHandle = this.newDeferredHandle(); + const constant = this.content(); + // We need to use the object wrapper here to define the additional properties. + // noinspection JSPrimitiveTypeWrapperUsage + const obj = new String(constant); // eslint-disable-line no-new-wrappers + const res = Object.defineProperties(obj, { + "class": { + configurable: true, + value: clazz, + }, + "extends": { + configurable: true, + value: {}, + }, + }); + deferredHandle(res); + return res; + }, +}; + +/** @type {Object.} */ +const primHandlers = { + "B": function() { + return this.readInt8(); + }, + "C": function() { + return String.fromCharCode(this.readUInt16()); + }, + "D": function() { + return this.buf.readDoubleBE(this.step(8)); + }, + "F": function() { + return this.buf.readFloatBE(this.step(4)); + }, + "I": function() { + return this.readInt32(); + }, + "J": function() { + const high = this.readUInt32(); + const low = this.readUInt32(); + return Long.fromBits(low, high); + }, + "S": function() { + return this.readInt16(); + }, + "Z": function() { + return !!this.readInt8(); + }, + "L": function() { + return this.content(); + }, + "[": function() { + return this.content(); + }, +}; + +class Parser { + /** + * @param {Buffer} buf + */ + constructor(buf) { + this.buf = buf; + this.pos = 0; + this.nextHandle = 0x7e0000; + this.handles = []; + this.contents = []; + + this.magic(); + this.version(); + + while (this.pos < this.buf.length) { + this.contents.push(this.content()); + } + } + + /** + * @param {string} className + * @param {string} serialVersionUID + * @param {function(this:Parser, Object): Object} parser + */ + static registerClassDataParser(className, serialVersionUID, parser) { + assert.strictEqual(serialVersionUID.length, 16, + "serialVersionUID must be 16 hex digits"); + + classDataParsers[`${className}@${serialVersionUID}`] = parser; + } + + /** + * @param {string} className + * @param {string} serialVersionUID + * @param {function(this:Parser, Object, Object, Array): Object} parser + */ + static registerPostProcessor(className, serialVersionUID, parser) { + assert.strictEqual(serialVersionUID.length, 16, + "serialVersionUID must be 16 hex digits"); + + classPostProcessors[`${className}@${serialVersionUID}`] = parser; + } + + step(len) { + const pos = this.pos; + this.pos += len; + + if (this.pos > this.buf.length) { + const err = new Error("Premature end of input"); + err.buf = this.buf; + err.pos = this.pos; + throw err; + } + + return pos; + } + + chunk(len, encoding) { + const pos = this.step(len); + return this.buf.toString(encoding, pos, this.pos); + } + + readUInt8() { + return this.buf.readUInt8(this.step(1)); + } + + readInt8() { + return this.buf.readInt8(this.step(1)); + } + + readUInt16() { + return this.buf.readUInt16BE(this.step(2)); + } + + readInt16() { + return this.buf.readInt16BE(this.step(2)); + } + + readUInt32() { + return this.buf.readUInt32BE(this.step(4)); + } + + readInt32() { + return this.buf.readInt32BE(this.step(4)); + } + + readHex(len) { + return this.chunk(len, "hex"); + } + + utf() { + return this.chunk(this.readUInt16(), "utf8"); + } + + utfLong() { + if (this.readUInt32() !== 0) { + throw new Error("Can't handle more than 2^32 bytes in a string"); + } + + return this.chunk(this.readUInt32(), "utf8"); + } + + magic() { + this.magic = this.readUInt16(); + if (this.magic !== 0xaced) { + throw Error("STREAM_MAGIC not found"); + } + } + + version() { + this.version = this.readUInt16(); + if (this.version !== 5) { + throw Error("Only understand protocol version 5"); + } + } + + content(allowed) { + const tc = this.readUInt8() - 0x70; + if (tc < 0 || tc > names.length) { + throw Error(`Don't know about type 0x${(tc + 0x70).toString(16)}`); + } + + const name = names[tc]; + if (allowed && allowed.indexOf(name) === -1) { + throw Error(`${name} not allowed here`); + } + + const handler = typeHandlers[name]; + if (!handler) { + throw Error(`Don't know how to handle ${name}`); + } + + return handler.call(this); + } + + annotations(allowed) { + const annotations = []; + for (;;) { + const annotation = this.content(allowed); + if (annotation === endBlock) { + break; + } + + annotations.push(annotation); + } + + return annotations; + } + + classDesc() { + return this.content(["ClassDesc", "ProxyClassDesc", "Null", "Reference"]); + } + + fieldDesc() { + const res = {}; + res.type = String.fromCharCode(this.readUInt8()); + res.name = this.utf(); + + if ("[L".indexOf(res.type) !== -1) { + res.className = this.content(); + } + + return res; + } + + recursiveClassData(cls, obj) { + if (cls.super) { + this.recursiveClassData(cls.super, obj); + } + + const fields = obj.extends[cls.name] = this.classdata(cls, obj); + for (const name in fields) { + obj[name] = fields[name]; + } + } + + classdata(cls) { + // For bcompat, this defaults to the values handler - same as without a write method. + const classDataParser = classDataParsers[`${cls.name}@${cls.serialVersionUID}`] || this.values; + const postProcessor = classPostProcessors[`${cls.name}@${cls.serialVersionUID}`]; + + switch (cls.flags & 0x0f) { + case 0x02: // SC_SERIALIZABLE without SC_WRITE_METHOD + return this.values(cls); + case 0x03: { // SC_SERIALIZABLE with SC_WRITE_METHOD + let res = classDataParser.call(this, cls); + const data = res["@"] = this.annotations(); + if (postProcessor) { + res = postProcessor.call(this, cls, res, data); + } + + return res; + } + case 0x04: // SC_EXTERNALIZABLE without SC_BLOCKDATA + throw Error("Can't parse version 1 external content"); + case 0x0c: // SC_EXTERNALIZABLE with SC_BLOCKDATA + return { "@": this.annotations() }; + default: + throw Error(`Don't know how to deserialize class with flags 0x${cls.flags.toString(16)}`); + } + } + + /** + * @param {string} type + * @return {function(this:Parser): *} + */ + primHandler(type) { + const handler = primHandlers[type]; + if (!handler) { + throw Error(`Don't know how to read field of type '${type}'`); + } + + return handler; + } + + /** + * @param cls + * @return {Object} + */ + values(cls) { + const vals = {}; + + const fields = cls.fields; + for (let i = 0; i < fields.length; ++i) { + const field = fields[i]; + const handler = this.primHandler(field.type); + vals[field.name] = handler.call(this); + } + + return vals; + } + + /** + * @template T + * @param {T} obj + * @return {T} + */ + newHandle(obj) { + this.handles[this.nextHandle++] = obj; + return obj; + } + + /** + * @return {function(*): void} + */ + newDeferredHandle() { + const idx = this.nextHandle++; + const handles = this.handles; + handles[idx] = null; + return function(obj) { + handles[idx] = obj; + }; + } +} + +// Backwards compat shim. +Parser.register = Parser.registerPostProcessor; + +module.exports = Parser; diff --git a/src/util.js b/src/main/node/util.js similarity index 88% rename from src/util.js rename to src/main/node/util.js index e393c77..e96d890 100644 --- a/src/util.js +++ b/src/main/node/util.js @@ -36,8 +36,8 @@ function mapParser(cls, fields, data) { var map = new Map(); var obj = {}; for (var i = 0; i < size; ++i) { - var key = data[2*i + 1]; - var value = data[2*i + 2]; + var key = data[(2 * i) + 1]; + var value = data[(2 * i) + 2]; map.set(key, value); if (typeof key === "string") { obj[key] = value; @@ -53,8 +53,8 @@ function enumMapParser(cls, fields, data) { var map = new Map(); var obj = {}; for (var i = 0; i < size; ++i) { - var key = data[2*i + 1]; - var value = data[2*i + 2]; + var key = data[(2 * i) + 1]; + var value = data[(2 * i) + 2]; map.set(key, value); obj[key] = value; } @@ -69,14 +69,14 @@ function hashSetParser(cls, fields, data) { var size = data[0].readInt32BE(8); if (data.length !== size + 1) throw new Error("Expected " + size + " elements " + - "but parsed " + (data.length - 1)); + "but parsed " + (data.length - 1)); fields.set = new Set(data.slice(1)); return fields; } -Parser.register("java.util.ArrayList", "7881d21d99c7619d", listParser); +Parser.register("java.util.ArrayList", "7881d21d99c7619d", listParser); Parser.register("java.util.ArrayDeque", "207cda2e240da08b", listParser); -Parser.register("java.util.Hashtable", "13bb0f25214ae4b8", mapParser); +Parser.register("java.util.Hashtable", "13bb0f25214ae4b8", mapParser); Parser.register("java.util.HashMap", "0507dac1c31660d1", mapParser); Parser.register("java.util.EnumMap", "065d7df7be907ca1", enumMapParser); Parser.register("java.util.HashSet", "ba44859596b8b734", hashSetParser); diff --git a/src/parser.js b/src/parser.js deleted file mode 100644 index 01482e4..0000000 --- a/src/parser.js +++ /dev/null @@ -1,373 +0,0 @@ -/* - * Copyright (c) 2015,2018 Martin von Gagern - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -// See http://docs.oracle.com/javase/7/docs/platform/serialization/spec/protocol.html for reference - -"use strict"; - -var assert = require("assert"); -var Long = require("long"); - -var names = [ - "Null", "Reference", "ClassDesc", "Object", "String", "Array", "Class", "BlockData", "EndBlockData", - "Reset", "BlockDataLong", "Exception", "LongString", "ProxyClassDesc", "Enum" -]; - -var endBlock = {}; - -function Parser(buf) { - this.buf = buf; - this.pos = 0; - this.nextHandle = 0x7e0000; - this.handles = []; - this.contents = []; - this.magic(); - this.version(); - while (this.pos < this.buf.length) { - this.contents.push(this.content()); - } -} - -Parser.prototype.step = function(len) { - var pos = this.pos; - this.pos += len; - if (this.pos > this.buf.length) { - var err = new Error("Premature end of input"); - err.buf = this.buf; - err.pos = this.pos; - throw err; - } - return pos; -} - -Parser.prototype.chunk = function(len, encoding) { - var pos = this.step(len); - return this.buf.toString(encoding, pos, this.pos); -} - -Parser.prototype.readUInt8 = function() { - return this.buf.readUInt8(this.step(1)); -} - -Parser.prototype.readInt8 = function() { - return this.buf.readInt8(this.step(1)); -} - -Parser.prototype.readUInt16 = function() { - return this.buf.readUInt16BE(this.step(2)); -} - -Parser.prototype.readInt16 = function() { - return this.buf.readInt16BE(this.step(2)); -} - -Parser.prototype.readUInt32 = function() { - return this.buf.readUInt32BE(this.step(4)); -} - -Parser.prototype.readInt32 = function() { - return this.buf.readInt32BE(this.step(4)); -} - -Parser.prototype.readHex = function(len) { - return this.chunk(len, "hex"); -} - -Parser.prototype.utf = function() { - return this.chunk(this.readUInt16(), "utf8"); -} - -Parser.prototype.utfLong = function() { - if (this.readUInt32() !== 0) - throw new Error("Can't handle more than 2^32 bytes in a string"); - return this.chunk(this.readUInt32(), "utf8"); -} - -Parser.prototype.magic = function() { - this.magic = this.readUInt16(); - if (this.magic !== 0xaced) - throw Error("STREAM_MAGIC not found"); -} - -Parser.prototype.version = function() { - this.version = this.readUInt16(); - if (this.version !== 5) - throw Error("Only understand protocol version 5"); -} - -Parser.prototype.content = function(allowed) { - var tc = this.readUInt8() - 0x70; - if (tc < 0 || tc > names.length) - throw Error("Don't know about type 0x" + (tc + 0x70).toString(16)); - var name = names[tc]; - if (allowed && allowed.indexOf(name) === -1) - throw Error(name + " not allowed here"); - var handler = this["parse" + name]; - if (!handler) - throw Error("Don't know how to handle " + name); - var elt = handler.call(this); - return elt; -} - -Parser.prototype.annotations = function(allowed) { - var annotations = []; - while (true) { - var annotation = this.content(allowed); - if (annotation === endBlock) - break; - annotations.push(annotation); - } - return annotations; -} - -Parser.prototype.classDesc = function() { - return this.content(["ClassDesc", "ProxyClassDesc", "Null", "Reference"]); -} - -Parser.prototype.parseClassDesc = function() { - var res = {}; - res.name = this.utf(); - res.serialVersionUID = this.readHex(8); - this.newHandle(res); - res.flags = this.readUInt8(); - res.isEnum = !!(res.flags & 0x10); - var count = this.readUInt16(); - res.fields = []; - for (var i = 0; i < count; ++i) - res.fields.push(this.fieldDesc()); - res.annotations = this.annotations(); - res.super = this.classDesc(); - return res; -} - -Parser.prototype.fieldDesc = function() { - var res = {}; - res.type = String.fromCharCode(this.readUInt8()); - res.name = this.utf(); - if ("[L".indexOf(res.type) !== -1) - res.className = this.content(); - return res; -} - -Parser.prototype.parseClass = function() { - return this.newHandle(this.classDesc()); -} - -Parser.prototype.parseObject = function() { - var res = Object.defineProperties({}, { - "class": { - configurable: true, - value: this.classDesc() - }, - "extends": { - configurable: true, - value: {} - } - }); - this.newHandle(res); - this.recursiveClassData(res.class, res); - return res; -} - -Parser.prototype.recursiveClassData = function(cls, obj) { - if (cls.super) - this.recursiveClassData(cls.super, obj); - var fields = obj.extends[cls.name] = this.classdata(cls, obj); - for (var name in fields) - obj[name] = fields[name]; -} - -Parser.prototype.classdata = function(cls) { - var res, data; - var postproc = this[cls.name + "@" + cls.serialVersionUID]; - switch (cls.flags & 0x0f) { - case 0x02: // SC_SERIALIZABLE without SC_WRITE_METHOD - return this.values(cls); - case 0x03: // SC_SERIALIZABLE with SC_WRITE_METHOD - res = this.values(cls); - data = res["@"] = this.annotations(); - if (postproc) - res = postproc.call(this, cls, res, data); - return res; - case 0x04: // SC_EXTERNALIZABLE without SC_BLOCKDATA - throw Error("Can't parse version 1 external content"); - case 0x0c: // SC_EXTERNALIZABLE with SC_BLOCKDATA - return {"@": this.annotations()}; - default: - throw Error("Don't know how to deserialize class with flags 0x" + cls.flags.toString(16)); - } -} - -Parser.prototype.parseArray = function() { - var classDesc = this.classDesc(); - var res = Object.defineProperties([], { - "class": { - configurable: true, - value: classDesc - }, - "extends": { - configurable: true, - value: {} - } - }); - this.newHandle(res); - var len = this.readInt32(); - var handler = this.primHandler(classDesc.name.charAt(1)); - res.length = len; - for (var i = 0; i < len; ++i) - res[i] = handler.call(this); - return res; -} - -Parser.prototype.parseEnum = function() { - var clazz = this.classDesc(); - var deferredHandle = this.newDeferredHandle(); - var constant = this.content(); - var res = Object.defineProperties(new String(constant), { - "class": { - configurable: true, - value: clazz - }, - "extends": { - configurable: true, - value: {} - } - }); - deferredHandle(res); - return res; -} - -Parser.prototype.parseBlockData = function() { - var len = this.readUInt8(); - var res = this.buf.slice(this.pos, this.pos + len); - this.pos += len; - return res; -} - -Parser.prototype.parseBlockDataLong = function() { - var len = this.readUInt32(); - var res = this.buf.slice(this.pos, this.pos + len); - this.pos += len; - return res; -} - -Parser.prototype.parseString = function() { - return this.newHandle(this.utf()); -} - -Parser.prototype.parseLongString = function() { - return this.newHandle(this.utfLong()); -} - -Parser.prototype.primHandler = function(type) { - var handler = this["prim" + type]; - if (!handler) - throw Error("Don't know how to read field of type '" + type + "'"); - return handler; -} - -Parser.prototype.values = function(cls) { - var vals = {}; - var fields = cls.fields; - for (var i = 0; i < fields.length; ++i) { - var field = fields[i]; - var handler = this.primHandler(field.type); - vals[field.name] = handler.call(this); - } - return vals; -} - -Parser.prototype.newHandle = function(obj) { - this.handles[this.nextHandle++] = obj; - return obj; -} - -Parser.prototype.newDeferredHandle = function() { - var idx = this.nextHandle++; - var handles = this.handles; - handles[idx] = null; - return function(obj) { - handles[idx] = obj; - }; -} - -Parser.prototype.parseReference = function() { - return this.handles[this.readInt32()]; -} - -Parser.prototype.parseNull = function() { - return null; -} - -Parser.prototype.parseEndBlockData = function() { - return endBlock; -} - -Parser.prototype.primB = function() { - return this.readInt8(); -} - -Parser.prototype.primC = function() { - return String.fromCharCode(this.readUInt16()); -} - -Parser.prototype.primD = function() { - return this.buf.readDoubleBE(this.step(8)); -} - -Parser.prototype.primF = function() { - return this.buf.readFloatBE(this.step(4)); -} - -Parser.prototype.primI = function() { - return this.readInt32(); -} - -Parser.prototype.primJ = function() { - var high = this.readUInt32(); - var low = this.readUInt32(); - return Long.fromBits(low, high); -} - -Parser.prototype.primS = function() { - return this.readInt16(); -} - -Parser.prototype.primZ = function() { - return !!this.readInt8(); -} - -Parser.prototype.primL = function() { - return this.content(); -} - -Parser.prototype["prim["] = function() { - return this.content(); -} - -Parser.register = function(className, serialVersionUID, parser) { - assert.strictEqual(serialVersionUID.length, 16, - "serialVersionUID must be 16 hex digits"); - Parser.prototype[className + "@" + serialVersionUID] = parser; -} - -module.exports = Parser; diff --git a/test/failures.js b/src/test/node/failures.js similarity index 98% rename from test/failures.js rename to src/test/node/failures.js index 73a52b2..d9f3a2d 100644 --- a/test/failures.js +++ b/src/test/node/failures.js @@ -1,7 +1,7 @@ "use strict"; const expect = require('chai').expect; -const javaDeserialization = require('../'); +const javaDeserialization = require('../../main/node'); const STREAM_MAGIC = "aced"; const STREAM_VERSION = "0005"; @@ -147,17 +147,17 @@ describe("Failure scenarios", function() { expect(parsing(template1({flags: 0}))) .to.throw("Don't know how to deserialize class with flags 0x0"); }); - + it("version 1 external", function() { expect(parsing(template1({flags: SC_EXTERNALIZABLE}))) .to.throw("Can't parse version 1 external content"); }); - + it("unknown primitive", function() { expect(parsing(template1({fieldType: "Q"}))) .to.throw("Don't know how to read field of type 'Q'"); }); - + it("bad classDesc", function() { expect(parsing(template1({classDesc: TC_OBJECT}))) .to.throw("Object not allowed here"); diff --git a/src/test/node/generated.js b/src/test/node/generated.js new file mode 100644 index 0000000..e1fd721 --- /dev/null +++ b/src/test/node/generated.js @@ -0,0 +1,327 @@ +//This is a generated file from GeneratedTestCases.java +'use strict'; + +const chai = require('chai'); +const expect = chai.expect; +const zlib = require('zlib'); +const javaDeserialization = require('../../main/node'); + +// Register a classdata parser for the io.github.gagern.nodeJavaDeserialization.CompletelyCustomFormat test. +javaDeserialization.registerClassDataParser('io.github.gagern.nodeJavaDeserialization.CompletelyCustomFormat', '0000000000000001', cls => ({})); + +function testCase(b64data, checks) { + return function() { + let bytes = Buffer.from(b64data, 'base64'); + if (b64data.substring(0, 4) === 'H4sI') + bytes = zlib.gunzipSync(bytes); + const res = javaDeserialization.parse(bytes); + const begin = res[0]; + const end = res[res.length - 1]; + expect(begin[0]).to.equal('Begin'); + expect(begin[1]).to.equal(begin); + expect(end[0]).to.equal(end); + expect(end[1]).to.equal('End'); + expect(res.length, + 'Number of serialized objects must match args list' + ).to.equal(checks.length + 2); + return checks.apply(null, res.slice(1, -1)); + }; +} + +describe('Deserialization of', function() { + + it('Exception as regular object', testCase( + 'H4sIAAAAAAAAAIVSXWvUQBS9m83WNqAufmGrriBaUWQXQYWSIthllWKsoBWEBWU2ud1OnUzizMSNCqKIr+KrgvoHfBX8AX48FEQEH330TZ99seDc1P2Qgs7DTHJz7plzTu6rH1DJFGxvByvsFqsLJrv1i50VDI3/5OPVl1V9RDgAeQoAjoHKHHa5vAn3oKQVTA1bLmXS8BhbeYip4Yl8cX3snHfi4TfqtexD4ADxaW3/6Sl/79sNiMVllfRYR+CX9ycPz/TerJbBDaASskyjgZ2FzgYhGwOkH8DmCA3j4gJqzboWt20Ed9koLrt+GzxtWHhjUbHQImrtvyD9Dy2BMUpjKXfoLE2VJcRooFobqK63ZYaLRsC18fOUAvEMjJ1nnSSJbZy10Tg3cjtnj87Orh2Y6SdLYe75RwObePds+tTXRw648+AJLnEhizuoAtgSYSgY2WsKpjUJ2RTA+BIXuMBi/PPuxWiWk2hQKS69a6DcFNbQuN3rdLstWKDVMl1oIYf1ZiKEHQayfvCKjJOIL3GKnJz/2nro+Ovvj6sOlAJwha0Q+4T9ncf+TzCsT87B/dVrP2sFTSk0sGsk4SHM5qz7g1Iwn1GK3SYd+YPP+55+YM/LUJoHV/M7WBiEnkt7TqJ25xkdtBx6mLReWzL6DS3112r+AgAA', + function(itm) { + expect(itm.class.name, "itm.class.name").to.equal('java.lang.RuntimeException'); + expect(itm.detailMessage, "itm.detailMessage").to.equal('Kaboom'); + })); + + it('string', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABdAAIc29tZXRleHR1cQB+AAAAAAACcQB+AAR0AANFbmQ=', + function(itm) { + expect(typeof itm, "typeof itm").to.equal('string'); + expect(itm, "itm").to.equal('sometext'); + })); + + it('canaries only', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABdXEAfgAAAAAAAnEAfgADdAADRW5k', + function() { + })); + + it('long string', testCase( + 'H4sIAAAAAAAAAO3JuwnCABRA0Wc0veAUNlnATrATbAWr+CEYQvCTiIVkBjdwAWdxE3eQgGOcU12472+k7SUmm2WZ3/KsyusiW23Lw66ZPT/r1/g6rZKI+ykikibS+aE41ufoYvCIXv8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/tpzdNFL+hg1MVzU+x8AC//OVwACAA==', + function(itm) { + expect(typeof itm, "typeof itm").to.equal('string'); + expect(itm, "itm").to.have.lengthOf(131072); + expect(itm[0], "itm[0]").to.equal('x'); + expect(itm[(1 << 17) - 1], "itm[(1 << 17) - 1]").to.equal('x'); + })); + + it('null', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABcHVxAH4AAAAAAAJxAH4AA3QAA0VuZA==', + function(itm) { + expect(typeof itm, "typeof itm").to.equal('object'); + expect(itm, "itm").to.equal(null); + })); + + it('duplicate object', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IAO2lvLmdpdGh1Yi5nYWdlcm4ubm9kZUphdmFEZXNlcmlhbGl6YXRpb24uQmFzZUNsYXNzV2l0aEZpZWxkAAAAAAAAEjQCAAFJAANmb294cAAAAHt0AAVkZWxpbXEAfgAEdXEAfgAAAAAAAnEAfgAGdAADRW5k', + function(obj1, delim, obj2) { + expect(obj1, "obj1").to.equal(obj2); + })); + + it('primitive fields', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IAOGlvLmdpdGh1Yi5nYWdlcm4ubm9kZUphdmFEZXNlcmlhbGl6YXRpb24uUHJpbWl0aXZlRmllbGRzAAASNFZ4mrwCAAhaAAJib0IAAmJ5QwABY0QAAWRGAAFmSQABaUoAAWxTAAFzeHAB6xI0QCiuFHrhR65CmQAA////hf////////zr/jh1cQB+AAAAAAACcQB+AAV0AANFbmQ=', + function(itm) { + expect(itm.i, "itm.i").to.equal(-123); + expect(itm.s, "itm.s").to.equal(-456); + expect(String(itm.l), "String(itm.l)").to.equal('-789'); + expect(itm.l.toNumber(), "itm.l.toNumber()").to.equal(-789); + expect(itm.l.equals(-789), "itm.l.equals(-789)").to.be.true; + expect(itm.by, "itm.by").to.equal(-21); + expect(itm.d, "itm.d").to.equal(12.34); + expect(itm.f, "itm.f").to.equal(76.5); + expect(itm.bo, "itm.bo").to.equal(true); + expect(itm.c, "itm.c").to.equal('\u1234'); + expect(itm, "itm").to.have.all.keys(['i', 's', 'l', 'by', 'd', 'f', 'bo', 'c']); + expect(itm.class.serialVersionUID, "itm.class.serialVersionUID").to.equal('0000123456789abc'); + })); + + it('boxed primitives', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cP///4VzcgAPamF2YS5sYW5nLlNob3J0aE03EzRg2lICAAFTAAV2YWx1ZXhxAH4ABP44c3IADmphdmEubGFuZy5Mb25nO4vkkMyPI98CAAFKAAV2YWx1ZXhxAH4ABP////////zrc3IADmphdmEubGFuZy5CeXRlnE5ghO5Q9RwCAAFCAAV2YWx1ZXhxAH4ABOtzcgAQamF2YS5sYW5nLkRvdWJsZYCzwkopa/sEAgABRAAFdmFsdWV4cQB+AARAKK4UeuFHrnNyAA9qYXZhLmxhbmcuRmxvYXTa7cmi2zzw7AIAAUYABXZhbHVleHEAfgAEQpkAAHNyABFqYXZhLmxhbmcuQm9vbGVhbs0gcoDVnPruAgABWgAFdmFsdWV4cAFzcgATamF2YS5sYW5nLkNoYXJhY3RlcjSLR9lrGiZ4AgABQwAFdmFsdWV4cBI0dXEAfgAAAAAAAnEAfgAUdAADRW5k', + function(i, s, l, by, d, f, bo, c) { + expect(i.value, "i.value").to.equal(-123); + expect(s.value, "s.value").to.equal(-456); + expect(l.value.equals(-789), "l.value.equals(-789)").to.be.true; + expect(by.value, "by.value").to.equal(-21); + expect(d.value, "d.value").to.equal(12.34); + expect(f.value, "f.value").to.equal(76.5); + expect(bo.value, "bo.value").to.equal(true); + expect(c.value, "c.value").to.equal('\u1234'); + expect(i.class.name, "i.class.name").to.equal('java.lang.Integer'); + expect(s.class.name, "s.class.name").to.equal('java.lang.Short'); + expect(l.class.name, "l.class.name").to.equal('java.lang.Long'); + expect(by.class.name, "by.class.name").to.equal('java.lang.Byte'); + expect(d.class.name, "d.class.name").to.equal('java.lang.Double'); + expect(f.class.name, "f.class.name").to.equal('java.lang.Float'); + expect(bo.class.name, "bo.class.name").to.equal('java.lang.Boolean'); + expect(c.class.name, "c.class.name").to.equal('java.lang.Character'); + })); + + it('inherited field', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IARWlvLmdpdGh1Yi5nYWdlcm4ubm9kZUphdmFEZXNlcmlhbGl6YXRpb24uRGVyaXZlZENsYXNzV2l0aEFub3RoZXJGaWVsZAAAAAAAACNFAgABSQADYmFyeHIAO2lvLmdpdGh1Yi5nYWdlcm4ubm9kZUphdmFEZXNlcmlhbGl6YXRpb24uQmFzZUNsYXNzV2l0aEZpZWxkAAAAAAAAEjQCAAFJAANmb294cAAAAHsAAADqdXEAfgAAAAAAAnEAfgAGdAADRW5k', + function(itm) { + expect(itm.class.name, "itm.class.name").to.equal('io.github.gagern.nodeJavaDeserialization.DerivedClassWithAnotherField'); + expect(itm.class.super.name, "itm.class.super.name").to.equal('io.github.gagern.nodeJavaDeserialization.BaseClassWithField'); + expect(itm.class.super.super, "itm.class.super.super").to.equal(null); + expect(itm.extends['io.github.gagern.nodeJavaDeserialization.DerivedClassWithAnotherField'].bar, "itm.extends['io.github.gagern.nodeJavaDeserialization.DerivedClassWithAnotherField'].bar").to.equal(234); + expect(itm.extends['io.github.gagern.nodeJavaDeserialization.DerivedClassWithAnotherField'].foo, "itm.extends['io.github.gagern.nodeJavaDeserialization.DerivedClassWithAnotherField'].foo").to.equal(undefined); + expect(itm.extends['io.github.gagern.nodeJavaDeserialization.BaseClassWithField'].foo, "itm.extends['io.github.gagern.nodeJavaDeserialization.BaseClassWithField'].foo").to.equal(123); + expect(itm.bar, "itm.bar").to.equal(234); + expect(itm.foo, "itm.foo").to.equal(123); + })); + + it('duplicate field', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IAQmlvLmdpdGh1Yi5nYWdlcm4ubm9kZUphdmFEZXNlcmlhbGl6YXRpb24uRGVyaXZlZENsYXNzV2l0aFNhbWVGaWVsZAAAAAAAADRWAgABSQADZm9veHIAO2lvLmdpdGh1Yi5nYWdlcm4ubm9kZUphdmFEZXNlcmlhbGl6YXRpb24uQmFzZUNsYXNzV2l0aEZpZWxkAAAAAAAAEjQCAAFJAANmb294cAAAAHsAAAFZdXEAfgAAAAAAAnEAfgAGdAADRW5k', + function(itm) { + expect(itm.class.name, "itm.class.name").to.equal('io.github.gagern.nodeJavaDeserialization.DerivedClassWithSameField'); + expect(itm.class.super.name, "itm.class.super.name").to.equal('io.github.gagern.nodeJavaDeserialization.BaseClassWithField'); + expect(itm.class.super.super, "itm.class.super.super").to.equal(null); + expect(itm.extends['io.github.gagern.nodeJavaDeserialization.DerivedClassWithSameField'].foo, "itm.extends['io.github.gagern.nodeJavaDeserialization.DerivedClassWithSameField'].foo").to.equal(345); + expect(itm.extends['io.github.gagern.nodeJavaDeserialization.BaseClassWithField'].foo, "itm.extends['io.github.gagern.nodeJavaDeserialization.BaseClassWithField'].foo").to.equal(123); + expect(itm.foo, "itm.foo").to.equal(345); + })); + + it('primitive array', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABdXIAAltJTbpgJnbqsqUCAAB4cAAAAAMAAAAMAAAAIgAAADh1cQB+AAAAAAACcQB+AAV0AANFbmQ=', + function(itm) { + expect(itm, "itm").to.be.an('Array'); + expect(itm, "itm").to.have.lengthOf(3); + expect(itm[0], "itm[0]").to.equal(12); + expect(itm[1], "itm[1]").to.equal(34); + expect(itm[2], "itm[2]").to.equal(56); + expect(itm.class.name, "itm.class.name").to.equal('[I'); + })); + + it('nested array', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABdXIAFFtbTGphdmEubGFuZy5TdHJpbmc7Mk0JrYQy5FcCAAB4cAAAAAJ1cgATW0xqYXZhLmxhbmcuU3RyaW5nO63SVufpHXtHAgAAeHAAAAACdAABYXQAAWJ1cQB+AAUAAAABdAABY3VxAH4AAAAAAAJxAH4AC3QAA0VuZA==', + function(itm) { + expect(itm, "itm").to.be.an('Array'); + expect(itm, "itm").to.have.lengthOf(2); + expect(itm[0], "itm[0]").to.be.an('Array'); + expect(itm[0], "itm[0]").to.have.lengthOf(2); + expect(itm[1], "itm[1]").to.have.lengthOf(1); + expect(itm[0][0], "itm[0][0]").to.equal('a'); + expect(itm[0][1], "itm[0][1]").to.equal('b'); + expect(itm[1][0], "itm[1][0]").to.equal('c'); + })); + + it('array fields', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IANGlvLmdpdGh1Yi5nYWdlcm4ubm9kZUphdmFEZXNlcmlhbGl6YXRpb24uQXJyYXlGaWVsZHMAAAAAAAAAAQIAA1sAAmlhdAACW0lbAANpYWF0AANbW0lbAAJzYXQAE1tMamF2YS9sYW5nL1N0cmluZzt4cHVyAAJbSU26YCZ26rKlAgAAeHAAAAADAAAADAAAACIAAAA4dXIAA1tbSRf35E8Zj4k8AgAAeHAAAAACdXEAfgAIAAAAAgAAAAsAAAAMdXEAfgAIAAAAAwAAABUAAAAWAAAAF3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAJ0AANmb290AANiYXJ1cQB+AAAAAAACcQB+ABJ0AANFbmQ=', + function(itm) { + expect(itm.ia, "itm.ia").to.be.an('Array'); + expect(itm.iaa, "itm.iaa").to.be.an('Array'); + expect(itm.sa, "itm.sa").to.be.an('Array'); + expect(itm.iaa[1][2], "itm.iaa[1][2]").to.equal(23); + expect(itm.sa[1], "itm.sa[1]").to.equal('bar'); + })); + + it('enum', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABfnIAMWlvLmdpdGh1Yi5nYWdlcm4ubm9kZUphdmFEZXNlcmlhbGl6YXRpb24uU29tZUVudW0AAAAAAAAAABIAAHhyAA5qYXZhLmxhbmcuRW51bQAAAAAAAAAAEgAAeHB0AANPTkV+cQB+AAN0AAVUSFJFRXEAfgAHdXEAfgAAAAAAAnEAfgAJdAADRW5k', + function(one, three, three2) { + expect(typeof one, "typeof one").to.equal('object'); + expect(one, "one").to.be.an.instanceof(String); + expect(one == 'ONE', "one == 'ONE'").to.be.true; + expect(one, "one").to.not.equal('ONE'); + expect(one.class.name, "one.class.name").to.equal('io.github.gagern.nodeJavaDeserialization.SomeEnum'); + expect(one.class.isEnum, "one.class.isEnum").to.be.true; + expect(one.class.super.name, "one.class.super.name").to.equal('java.lang.Enum'); + expect(one.class.super.super, "one.class.super.super").to.equal(null); + expect(three == 'THREE', "three == 'THREE'").to.be.true; + expect(typeof three2, "typeof three2").to.equal('object'); + expect(three2, "three2").to.be.an.instanceof(String); + expect(three2 == 'THREE', "three2 == 'THREE'").to.be.true; + expect(three2, "three2").to.equal(three); + })); + + it('custom format', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IANWlvLmdpdGh1Yi5nYWdlcm4ubm9kZUphdmFEZXNlcmlhbGl6YXRpb24uQ3VzdG9tRm9ybWF0AAAAAAAAAAEDAAJJAANmb29MAANiYXJ0ABJMamF2YS9sYW5nL1N0cmluZzt4cAAAMDl0AA1IZWxsbywgV29ybGQhdwu16y0AtestALXrLXQACGFuZCBtb3JleHVxAH4AAAAAAAJxAH4ACHQAA0VuZA==', + function(itm) { + expect(itm['@'], "itm['@']").to.be.an('Array'); + expect(itm['@'], "itm['@']").to.have.lengthOf(2); + expect(Buffer.isBuffer(itm['@'][0]), "Buffer.isBuffer(itm['@'][0])").to.be.true; + expect(itm['@'][0].toString('hex'), "itm['@'][0].toString('hex')").to.equal('b5eb2d00b5eb2d00b5eb2d'); + expect(itm['@'][1], "itm['@'][1]").to.equal('and more'); + expect(itm.foo, "itm.foo").to.equal(12345); + expect(itm.bar, "itm.bar").to.equal('Hello, World!'); + })); + + it('completely custom format', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IAP2lvLmdpdGh1Yi5nYWdlcm4ubm9kZUphdmFEZXNlcmlhbGl6YXRpb24uQ29tcGxldGVseUN1c3RvbUZvcm1hdAAAAAAAAAABAwACSQADZm9vTAADYmFydAASTGphdmEvbGFuZy9TdHJpbmc7eHB0AA1IZWxsbywgV29ybGQhdw8AADA5testALXrLQC16y14dXEAfgAAAAAAAnEAfgAHdAADRW5k', + function(itm) { + expect(itm['@'], "itm['@']").to.be.an('Array'); + expect(itm['@'], "itm['@']").to.have.lengthOf(2); + expect(itm['@'][0], "itm['@'][0]").to.equal('Hello, World!'); + expect(Buffer.isBuffer(itm['@'][1]), "Buffer.isBuffer(itm['@'][1])").to.be.true; + expect(itm['@'][1].toString('hex'), "itm['@'][1].toString('hex')").to.equal('00003039b5eb2d00b5eb2d00b5eb2d'); + })); + + it('externalizable', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IAMWlvLmdpdGh1Yi5nYWdlcm4ubm9kZUphdmFEZXNlcmlhbGl6YXRpb24uRXh0ZXJuYWyNBpwj8+bz8wwAAHhwdw8AAAALtestALXrLQC16y10AAhhbmQgbW9yZXh1cQB+AAAAAAACcQB+AAZ0AANFbmQ=', + function(itm) { + expect(itm['@'], "itm['@']").to.be.an('Array'); + expect(itm['@'], "itm['@']").to.have.lengthOf(2); + expect(Buffer.isBuffer(itm['@'][0]), "Buffer.isBuffer(itm['@'][0])").to.be.true; + expect(itm['@'][0].toString('hex'), "itm['@'][0].toString('hex')").to.equal('0000000bb5eb2d00b5eb2d00b5eb2d'); + expect(itm['@'][1], "itm['@'][1]").to.equal('and more'); + expect(itm, "itm").to.have.all.keys(['@']); + })); + + it('long externalizable', testCase( + 'H4sIAAAAAAAAAFvzloG1tIhBONonK7EsUS8nMS9dzz8pKzW5xHrCuYj5AsWaOUwMDBUFDAwMTCUMrE6p6Zl5hQx1DIzFRQyGmfl66ZklGaVJeumJ6alFeXp5+SmpXkBzXFKLU4syE3MyqxJLMvPz9FwrSoDSiTm9bHOUPz/7/JkHZGQV0EgWIGZgYGRiZmFlY+fg5OLm4eXjFxAUEhYRFROXkJSSlpGVk1dQVFJWUVVT19DU0tbR1dM3MDQyNjE1M7ewtLK2sbWzd3B0cnZxdXP38PTy9vH18w8IDAoOCQ0Lj4iMio6JjYtPSExKTklNS8/IzMrOyc3LLygsKi4pLSuvqKyqrqmtq29obGpuaW1r7+js6u7p7eufMHHS5ClTp02fMXPW7Dlz581fsHDR4iVLly1fsXLV6jVr163fsHHT5i1bt23fsXPX7j179+0/cPDQ4SNHjx0/cfLU6TNnz52/cPHS5StXr12/cfPW7Tt3791/8PDR4ydPnz1/8fLV6zdv373/8PHT5y9fv33/8fPX7z9///0f6f4vYeBIzEtRyM0vSq0oBSUuEGACMdhKGJhd81IAbs5j0qUCAAA=', + function(itm) { + expect(itm['@'], "itm['@']").to.be.an('Array'); + expect(itm['@'], "itm['@']").to.have.lengthOf(2); + expect(Buffer.isBuffer(itm['@'][0]), "Buffer.isBuffer(itm['@'][0])").to.be.true; + expect(itm['@'][0], "itm['@'][0]").to.have.lengthOf(516); + expect(itm['@'][0].toString('hex', 0, 4), "itm['@'][0].toString('hex', 0, 4)").to.equal('00000200'); + expect(itm['@'][0].toString('hex', 4, 8), "itm['@'][0].toString('hex', 4, 8)").to.equal('00010203'); + expect(itm['@'][1], "itm['@'][1]").to.equal('and more'); + expect(itm, "itm").to.have.all.keys(['@']); + })); + + it('HashMap', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAADHcIAAAAEAAAAAJ0AANiYXJ0AANiYXp0AANmb29zcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAe3h1cQB+AAAAAAACcQB+AAt0AANFbmQ=', + function(itm) { + expect(typeof itm.obj, "typeof itm.obj").to.equal('object'); + expect(typeof itm['@'], "typeof itm['@']").to.equal('object'); + expect(itm.obj.bar, "itm.obj.bar").to.equal('baz'); + expect(itm.obj.foo.value, "itm.obj.foo.value").to.equal(123); + expect(itm.obj, "itm.obj").to.have.all.keys(['foo', 'bar']); + expect(itm.map, "itm.map").to.be.an.instanceof(Map); + expect(itm.map.get('bar'), "itm.map.get('bar')").to.equal('baz'); + expect(itm.map.get('foo').value, "itm.map.get('foo').value").to.equal(123); + expect(itm.map.size, "itm.map.size").to.equal(2); + })); + + it('HashMap', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAADHcIAAAAEAAAAAJ0AANiYXp0AANiYXJzcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAe3QAA2Zvb3hxAH4ACXVxAH4AAAAAAAJxAH4AC3QAA0VuZA==', + function(itm, i123) { + expect(typeof itm.obj, "typeof itm.obj").to.equal('object'); + expect(typeof itm['@'], "typeof itm['@']").to.equal('object'); + expect(itm['@'], "itm['@']").to.be.an('Array'); + expect(itm.obj, "itm.obj").to.have.all.keys(['baz']); + expect(itm.obj.baz, "itm.obj.baz").to.equal('bar'); + expect(itm.map, "itm.map").to.be.an.instanceof(Map); + expect(itm.map.get('baz'), "itm.map.get('baz')").to.equal('bar'); + expect(itm.map.get(i123), "itm.map.get(i123)").to.equal('foo'); + expect(itm.map.size, "itm.map.size").to.equal(2); + })); + + it('empty HashMap', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4dXEAfgAAAAAAAnEAfgAFdAADRW5k', + function(itm) { + expect(typeof itm.obj, "typeof itm.obj").to.equal('object'); + expect(itm.obj, "itm.obj").to.be.an('object').that.is.empty; + expect(itm.map, "itm.map").to.be.an.instanceof(Map); + expect(itm.map.size, "itm.map.size").to.equal(0); + })); + + it('Hashtable', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IAE2phdmEudXRpbC5IYXNodGFibGUTuw8lIUrkuAMAAkYACmxvYWRGYWN0b3JJAAl0aHJlc2hvbGR4cD9AAAAAAAAIdwgAAAALAAAAAnQAA2JhcnQAA2JhenQAA2Zvb3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAB7eHVxAH4AAAAAAAJxAH4AC3QAA0VuZA==', + function(itm) { + expect(typeof itm.obj, "typeof itm.obj").to.equal('object'); + expect(typeof itm['@'], "typeof itm['@']").to.equal('object'); + expect(itm.obj.bar, "itm.obj.bar").to.equal('baz'); + expect(itm.obj.foo.value, "itm.obj.foo.value").to.equal(123); + expect(itm.obj, "itm.obj").to.have.all.keys(['foo', 'bar']); + expect(itm.map, "itm.map").to.be.an.instanceof(Map); + expect(itm.map.get('bar'), "itm.map.get('bar')").to.equal('baz'); + expect(itm.map.get('foo').value, "itm.map.get('foo').value").to.equal(123); + expect(itm.map.size, "itm.map.size").to.equal(2); + })); + + it('EnumMap', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IAEWphdmEudXRpbC5FbnVtTWFwBl19976QfKEDAAFMAAdrZXlUeXBldAARTGphdmEvbGFuZy9DbGFzczt4cHZyADFpby5naXRodWIuZ2FnZXJuLm5vZGVKYXZhRGVzZXJpYWxpemF0aW9uLlNvbWVFbnVtAAAAAAAAAAASAAB4cgAOamF2YS5sYW5nLkVudW0AAAAAAAAAABIAAHhwdwQAAAACfnEAfgAGdAADT05Fc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cAAAAHt+cQB+AAZ0AAVUSFJFRXQAA2JhenhxAH4ACXEAfgAOdXEAfgAAAAAAAnEAfgARdAADRW5k', + function(itm, one, three) { + expect(typeof itm.obj, "typeof itm.obj").to.equal('object'); + expect(typeof itm['@'], "typeof itm['@']").to.equal('object'); + expect(itm.obj.THREE, "itm.obj.THREE").to.equal('baz'); + expect(itm.obj.ONE.value, "itm.obj.ONE.value").to.equal(123); + expect(itm.obj, "itm.obj").to.have.all.keys(['ONE', 'THREE']); + expect(itm.keyType.name, "itm.keyType.name").to.equal('io.github.gagern.nodeJavaDeserialization.SomeEnum'); + expect(itm.keyType.isEnum, "itm.keyType.isEnum").to.be.true; + expect(itm.map, "itm.map").to.be.an.instanceof(Map); + expect(itm.map.get(three), "itm.map.get(three)").to.equal('baz'); + expect(itm.map.get(one).value, "itm.map.get(one).value").to.equal(123); + expect(itm.map.size, "itm.map.size").to.equal(2); + })); + + it('ArrayList', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IAE2phdmEudXRpbC5BcnJheUxpc3R4gdIdmcdhnQMAAUkABHNpemV4cAAAAAJ3BAAAAAJ0AANmb29zcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAe3h1cQB+AAAAAAACcQB+AAl0AANFbmQ=', + function(itm) { + expect(itm.list, "itm.list").to.be.an('Array'); + expect(itm.list, "itm.list").to.have.lengthOf(2); + expect(itm.list[0], "itm.list[0]").to.equal('foo'); + expect(itm.list[1].value, "itm.list[1].value").to.equal(123); + })); + + it('ArrayDeque', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IAFGphdmEudXRpbC5BcnJheURlcXVlIHzaLiQNoIsDAAB4cHcEAAAAAnQAA2Zvb3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAB7eHVxAH4AAAAAAAJxAH4ACXQAA0VuZA==', + function(itm) { + expect(itm.list, "itm.list").to.be.an('Array'); + expect(itm.list, "itm.list").to.have.lengthOf(2); + expect(itm.list[0], "itm.list[0]").to.equal('foo'); + expect(itm.list[1].value, "itm.list[1].value").to.equal(123); + })); + + it('HashSet', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IAEWphdmEudXRpbC5IYXNoU2V0ukSFlZa4tzQDAAB4cHcMAAAAED9AAAAAAAACdAADZm9vc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cAAAAHt4dXEAfgAAAAAAAnEAfgAJdAADRW5k', + function(itm) { + expect(itm.set, "itm.set").to.be.an.instanceof(Set); + expect(itm.set.size, "itm.set.size").to.equal(2); + expect(itm.set.has('foo'), "itm.set.has('foo')").to.be.true; + })); + +}); diff --git a/test/generated.js b/src/test/node/generatedOriginal.js similarity index 94% rename from test/generated.js rename to src/test/node/generatedOriginal.js index 52ec290..bd61cd2 100644 --- a/test/generated.js +++ b/src/test/node/generatedOriginal.js @@ -3,7 +3,10 @@ const chai = require('chai'); const expect = chai.expect; const zlib = require('zlib'); -const javaDeserialization = require('../'); +const javaDeserialization = require('../../main/node/index'); + +// Register a classdata parser for the CompletelyCustomFormat test. +javaDeserialization.registerClassDataParser('CompletelyCustomFormat', '0000000000000001', cls => ({})); function testCase(b64data, checks) { return function() { @@ -26,6 +29,23 @@ function testCase(b64data, checks) { describe('Deserialization of', function() { + it('ArrayDeque', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IAFGphdmEudXRpbC5BcnJheURlcXVlIHzaLiQNoIsDAAB4cHcEAAAAAnQAA2Zvb3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAB7eHVxAH4AAAAAAAJxAH4ACXQAA0VuZA==', + function(itm) { + expect(itm.list, "itm.list").to.be.an('Array'); + expect(itm.list, "itm.list").to.have.lengthOf(2); + expect(itm.list[0], "itm.list[0]").to.equal('foo'); + expect(itm.list[1].value, "itm.list[1].value").to.equal(123); + })); + + it('HashSet', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IAEWphdmEudXRpbC5IYXNoU2V0ukSFlZa4tzQDAAB4cHcMAAAAED9AAAAAAAACdAADZm9vc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cAAAAHt4dXEAfgAAAAAAAnEAfgAJdAADRW5k', + function(itm) { + expect(itm.set, "itm.set").to.be.an.instanceof(Set); + expect(itm.set.size, "itm.set.size").to.equal(2); + expect(itm.set.has('foo'), "itm.set.has('foo')").to.be.true; + })); + it('canaries only', testCase( 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABdXEAfgAAAAAAAnEAfgADdAADRW5k', function() { @@ -182,7 +202,7 @@ describe('Deserialization of', function() { })); it('custom format', testCase( - 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IADEN1c3RvbUZvcm1hdAAAAAAAAAABAwABSQADZm9veHAAADA5dwu16y0AtestALXrLXQACGFuZCBtb3JleHVxAH4AAAAAAAJxAH4ABnQAA0VuZA==', + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IADEN1c3RvbUZvcm1hdAAAAAAAAAABAwACSQADZm9vTAADYmFydAASTGphdmEvbGFuZy9TdHJpbmc7eHAAADA5dAANSGVsbG8sIFdvcmxkIXcLtestALXrLQC16y10AAhhbmQgbW9yZXh1cQB+AAAAAAACcQB+AAh0AANFbmQ=', function(itm) { expect(itm['@'], "itm['@']").to.be.an('Array'); expect(itm['@'], "itm['@']").to.have.lengthOf(2); @@ -190,6 +210,17 @@ describe('Deserialization of', function() { expect(itm['@'][0].toString('hex'), "itm['@'][0].toString('hex')").to.equal('b5eb2d00b5eb2d00b5eb2d'); expect(itm['@'][1], "itm['@'][1]").to.equal('and more'); expect(itm.foo, "itm.foo").to.equal(12345); + expect(itm.bar, "itm.bar").to.equal('Hello, World!'); + })); + + it('completely custom format', testCase( + 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IAFkNvbXBsZXRlbHlDdXN0b21Gb3JtYXQAAAAAAAAAAQMAAkkAA2Zvb0wAA2JhcnQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwdAANSGVsbG8sIFdvcmxkIXcPAAAwObXrLQC16y0AtesteHVxAH4AAAAAAAJxAH4AB3QAA0VuZA==', + function(itm) { + expect(itm['@'], "itm['@']").to.be.an('Array'); + expect(itm['@'], "itm['@']").to.have.lengthOf(2); + expect(itm['@'][0], "itm['@'][0]").to.equal('Hello, World!'); + expect(Buffer.isBuffer(itm['@'][1]), "Buffer.isBuffer(itm['@'][1])").to.be.true; + expect(itm['@'][1].toString('hex'), "itm['@'][1].toString('hex')").to.equal('00003039b5eb2d00b5eb2d00b5eb2d'); })); it('externalizable', testCase( @@ -292,21 +323,4 @@ describe('Deserialization of', function() { expect(itm.list[1].value, "itm.list[1].value").to.equal(123); })); - it('ArrayDeque', testCase( - 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IAFGphdmEudXRpbC5BcnJheURlcXVlIHzaLiQNoIsDAAB4cHcEAAAAAnQAA2Zvb3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAB7eHVxAH4AAAAAAAJxAH4ACXQAA0VuZA==', - function(itm) { - expect(itm.list, "itm.list").to.be.an('Array'); - expect(itm.list, "itm.list").to.have.lengthOf(2); - expect(itm.list[0], "itm.list[0]").to.equal('foo'); - expect(itm.list[1].value, "itm.list[1].value").to.equal(123); - })); - - it('HashSet', testCase( - 'rO0ABXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAAJ0AAVCZWdpbnEAfgABc3IAEWphdmEudXRpbC5IYXNoU2V0ukSFlZa4tzQDAAB4cHcMAAAAED9AAAAAAAACdAADZm9vc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cAAAAHt4dXEAfgAAAAAAAnEAfgAJdAADRW5k', - function(itm) { - expect(itm.set, "itm.set").to.be.an.instanceof(Set); - expect(itm.set.size, "itm.set.size").to.equal(2); - expect(itm.set.has('foo'), "itm.set.has('foo')").to.be.true; - })); - }); diff --git a/test/other.js b/src/test/node/other.js similarity index 94% rename from test/other.js rename to src/test/node/other.js index d8e9763..cef7666 100644 --- a/test/other.js +++ b/src/test/node/other.js @@ -2,7 +2,7 @@ const chai = require('chai'); const expect = chai.expect; -const javaDeserialization = require('../'); +const javaDeserialization = require('../../main/node'); const parse = javaDeserialization.parse; describe('Special cases', function() { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9433204 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "include": ["src/**/*"], + "compilerOptions": { + "lib": ["es2018"], + "module": "commonjs", + "target": "es2018", + "allowJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "dist" + } +} \ No newline at end of file