From e087c4f71abaae413ec8c32a00b8fbf96b5c5592 Mon Sep 17 00:00:00 2001 From: Douglas Robertson Date: Fri, 29 Mar 2024 10:03:52 -0600 Subject: [PATCH] add support for deserialization (unpacking) --- README.md | 1 - resources/strings.xml | 3 + source/MessagePack.mc | 1 + source/MessagePackDeserialize.mc | 269 +++++++++++++++++++ source/MessagePackTest.mc | 425 ++++++++++++++++++++++++++++++- 5 files changed, 697 insertions(+), 2 deletions(-) create mode 100644 source/MessagePackDeserialize.mc diff --git a/README.md b/README.md index aeb7168..ff12732 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,6 @@ MessagePack.Deserialize.unpack(obj); - support for arrays bigger than 65535 elements - support maps (Dictionary) with more than 65535 keys - support for symbols (since values for symbols may change across builds) -- does not currently unpack (deserialize) anything ## Contributing Please see the CONTRIBUTING.md file for details on how contribute. diff --git a/resources/strings.xml b/resources/strings.xml index b860e5f..a210eb1 100644 --- a/resources/strings.xml +++ b/resources/strings.xml @@ -1,3 +1,6 @@ arrays/maps larger than 65535 elements are not supported + $1$ extra bytes after the deserialized object + invalid byte + specified format is not supported \ No newline at end of file diff --git a/source/MessagePack.mc b/source/MessagePack.mc index 1d58753..761610e 100644 --- a/source/MessagePack.mc +++ b/source/MessagePack.mc @@ -36,6 +36,7 @@ module MessagePack { const FORMAT_FIXARRAY = 0x90; const FORMAT_FIXSTR = 0xA0; const FORMAT_NIL = 0xC0; + const FORMAT_UNUSED = 0xC1; const FORMAT_FALSE = 0xC2; const FORMAT_TRUE = 0xC3; const FORMAT_BIN8 = 0xC4; diff --git a/source/MessagePackDeserialize.mc b/source/MessagePackDeserialize.mc new file mode 100644 index 0000000..781869a --- /dev/null +++ b/source/MessagePackDeserialize.mc @@ -0,0 +1,269 @@ +/* +MIT License + +Copyright (c) 2024 Douglas Robertson (douglas@edgeoftheearth.com) + +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. + +Author: Douglas Robertson (GitHub: douglasr; Garmin Connect: dbrobert) +*/ + +import Toybox.Application; +import Toybox.Lang; +import Toybox.StringUtil; +using Toybox.System; + +/* +MessagePack is an efficient binary serialization format. +And now it's available for Monkey C. +*/ +module MessagePack { + + (:messagePackDeserialize) + module Deserialize { + + // Deserialize (unpack) a packable object, recursively as needed. + // @param byteArray byte array of the object, in MessagePack format + // @return an object compatible with MessagePack + function unpack(byteArray as ByteArray) as MsgPackObject? { + var parseResults = unpackRecursive(byteArray, 0); + var index = parseResults[1] as Number; + if (index < byteArray.size()) { + throw new MalformedFormatException(Lang.format(Application.loadResource(Rez.Strings.exceptionExtraBytes) as String, [byteArray.size()-index])); + } + return parseResults[0] as MsgPackObject?; + } + + // Deserialize (unpack) a packable object, recursively as needed, starting at a given index in the byte array. + // Since array parameters (including ByteArray objects) are passed by by reference, it is more memory efficient + // to pass the entire byte array and the index to start parsing at rather than slicing the array into pieces. + // @param byteArray byte array of the object, in MessagePack format + // @param index the index within the byte array to start deserialization at + // @return an array/tuple of a MessagePack object and the end index (after parsing) + function unpackRecursive(byteArray as ByteArray, index as Number) as Array { + var parseResults; + var formatByte; + formatByte = byteArray[index]; + if (formatByte == FORMAT_UNUSED) { + throw new MalformedFormatException(Application.loadResource(Rez.Strings.exceptionInvalidByte) as String); + } else if (formatByte == FORMAT_NIL) { + parseResults = unpackNull(byteArray, index); + } else if ((formatByte >= FORMAT_FIXARRAY && formatByte < FORMAT_FIXARRAY+16) || formatByte == FORMAT_ARRAY16 || formatByte == FORMAT_ARRAY32) { + parseResults = unpackArray(byteArray, index); + } else if (formatByte == FORMAT_FALSE || formatByte == FORMAT_TRUE) { + parseResults = unpackBoolean(byteArray, index); + } else if ((formatByte >= FORMAT_FIXMAP && formatByte < FORMAT_FIXMAP+16) || formatByte == FORMAT_MAP16 || formatByte == FORMAT_MAP32) { + parseResults = unpackDictionary(byteArray, index); + } else if ((formatByte >= 0x00 && formatByte < 0x80) || (formatByte >= FORMAT_UINT8 && formatByte <= FORMAT_INT64) || formatByte == FORMAT_NEG_FIXINT) { + parseResults = unpackNumber(byteArray, index); + } else if ((formatByte >= FORMAT_FIXSTR && formatByte < FORMAT_FIXSTR+16) || formatByte == FORMAT_STR8 || formatByte == FORMAT_STR16) { + parseResults = unpackString(byteArray, index); + } else { + throw new MalformedFormatException(Application.loadResource(Rez.Strings.exceptionInvalidByte) as String); + } + return parseResults as Array; + } + + // Deserialize (unpack) an array. + // @param byteArray byte array of MessagePack object(s) + // @param index index within the byte array to start parsing + // @return an array/tuple of an array of MessagePack objects and the end index (after parsing) + function unpackArray(byteArray as ByteArray, index as Number) as Array { + var arraySize; + var headerSize; + var unpackedArray = []; + + if (byteArray[index] >= FORMAT_FIXARRAY && byteArray[index] < FORMAT_FIXARRAY+16) { + headerSize = 1; + arraySize = byteArray[index] - FORMAT_FIXARRAY; + } else if (byteArray[index] == FORMAT_ARRAY16) { + headerSize = 3; + arraySize = (byteArray[index+1] << 8) + byteArray[index+2]; + } else { + throw new MalformedFormatException(Application.loadResource(Rez.Strings.exceptionUnsupportedFormatByte) as String); + } + + index = index + headerSize; + for (var i=0; i < arraySize; i++) { + var parseResults = unpackRecursive(byteArray, index); + unpackedArray.add(parseResults[0] as MsgPackObject?); + index = parseResults[1]; + } + + return [unpackedArray, index] as Array; + } + + // Deserialize (unpack) a boolean. + // @param byteArray byte array of MessagePack object(s) + // @param index index within the byte array to start parsing + // @return an array/tuple of a boolean object and the end index (after parsing) + function unpackBoolean(byteArray as ByteArray, index as Number) as Array { + if (byteArray[index] == FORMAT_TRUE) { + return [true, index+1] as Array; + } else if (byteArray[index] == FORMAT_FALSE) { + return [false, index+1] as Array; + } + throw new MalformedFormatException(Application.loadResource(Rez.Strings.exceptionInvalidByte) as String); + } + + // Deserialize (unpack) a dictionary (map). + // @param byteArray byte array of MessagePack object(s) + // @param index index within the byte array to start parsing + // @return an array/tuple of an dictionary of MessagePack object key/pairs and the end index (after parsing) + function unpackDictionary(byteArray as ByteArray, index as Number) as Array { + var mapSize; + var headerSize; + var unpackedMap = {}; + var parseResults; + + if (byteArray[index] >= FORMAT_FIXMAP && byteArray[index] < FORMAT_FIXMAP+16) { + headerSize = 1; + mapSize = byteArray[index] - FORMAT_FIXMAP; + } else if (byteArray[index] == FORMAT_MAP16) { + headerSize = 3; + mapSize = (byteArray[index+1] << 8) + byteArray[index+2]; + } else { + throw new MalformedFormatException(Application.loadResource(Rez.Strings.exceptionUnsupportedFormatByte) as String); + } + + index += headerSize; + for (var i=0; i < mapSize; i++) { + // parse the key + parseResults = unpackRecursive(byteArray, index); + var key = parseResults[0]; + index = parseResults[1] as Number; + + // parse the value + parseResults = unpackRecursive(byteArray, index); + var value = parseResults[0]; + index = parseResults[1] as Number; + + unpackedMap[key] = value; + } + + return [unpackedMap, index] as Array; + } + + // Deserialize (unpack) a null. + // @param byteArray byte array of MessagePack object(s) + // @param index index within the byte array to start parsing + // @return an array/tuple of a null object and the end index (after parsing) + function unpackNull(byteArray as ByteArray, index as Number) as Array { + if (byteArray[index] == FORMAT_NIL) { + return [null, index+1] as Array; + } + throw new MalformedFormatException(Application.loadResource(Rez.Strings.exceptionInvalidByte) as String); + } + + // Deserialize (unpack) a whole number (integer). + // @param byteArray byte array of MessagePack object(s) + // @param index index within the byte array to start parsing + // @return an array/tuple of a number/long and the end index (after parsing) + function unpackNumber(byteArray as ByteArray, index as Number) as Array { + if (byteArray[index] >= 0x00 && byteArray[index] < 0x80) { + // value is the actual byte + return [byteArray[index], index+1] as Array; + } else if (byteArray[index] >= 0xE0 && byteArray[index] <= 0xFF) { + // value is 0x1FF-byte-1 + return [(byteArray[index]-FORMAT_NEG_FIXINT-1), index+1] as Array; + } else if (byteArray[index] == FORMAT_UINT8) { + // value is the byte after the format + return [byteArray[index+1], index+2] as Array; + } else if (byteArray[index] == FORMAT_UINT16) { + // value is the two bytes after the format + var uint16 = (byteArray[index+1] << 8) + byteArray[index+2]; + return [uint16, index+4] as Array; + } else if (byteArray[index] == FORMAT_UINT32) { + // value is the four bytes after the format + var uint32 = 0; + // if the resulting number will be >= 2,147,483,648 then we need to treat the first byte as a Long + if (byteArray[index+1] >= 0x80) { + uint32 = (byteArray[index+1].toLong() << 24); + } else { + uint32 = (byteArray[index+1] << 24); + } + uint32 = uint32 + (byteArray[index+2] << 16) + (byteArray[index+3] << 8) + byteArray[index+4]; + return [uint32, index+6] as Array; + } else if (byteArray[index] == FORMAT_UINT64) { + var uint64 = (byteArray[index+1].toLong() << 56) + (byteArray[index+2].toLong() << 48); + uint64 = uint64 + (byteArray[index+3].toLong() << 40) + (byteArray[index+4].toLong() << 32); + uint64 = uint64 + (byteArray[index+5].toLong() << 24) + (byteArray[index+6] << 16); + uint64 = uint64 + (byteArray[index+7] << 8) + byteArray[index+8]; + return [uint64, index+10] as Array; + } else if (byteArray[index] == FORMAT_INT8) { + // value is byte - 256 + return [(byteArray[index+1]-256), index+2] as Array; + } else if (byteArray[index] == FORMAT_INT16) { + // value is byte - 65536 + var int16 = (byteArray[index+1] << 8) + byteArray[index+2] - 65536; + return [int16, index+3] as Array; + } else if (byteArray[index] == FORMAT_INT32) { + // value is byte - 4294967296 + var int32 = (byteArray[index+1].toLong() << 24); + int32 = int32 + (byteArray[index+2] << 16) + (byteArray[index+3] << 8) + byteArray[index+4]; + int32 = (int32 - 4294967296l).toNumber(); + return [int32, index+6] as Array; + } else { + // value is byte - 9223372036854775807 - 9223372036854775807 - 2 + var int64 = (byteArray[index+1].toLong() << 56) + (byteArray[index+2].toLong() << 48); + int64 = int64 + (byteArray[index+3].toLong() << 40) + (byteArray[index+4].toLong() << 32); + int64 = int64 + (byteArray[index+5].toLong() << 24) + (byteArray[index+6] << 16); + int64 = int64 + (byteArray[index+7] << 8) + byteArray[index+8]; + int64 = int64 - 9223372036854775807l; + int64 = int64 - 9223372036854775807l; + int64 = int64 - 2; + return [int64, index+10] as Array; + } + } + + // Deserialize (unpack) a string. + // @param byteArray byte array of MessagePack object(s) + // @param index index within the byte array to start parsing + // @return an array/tuple of a string object and the end index (after parsing) + function unpackString(byteArray as ByteArray, index as Number) as Array { + var strLength; + var headerSize; + var unpackedStr = ""; + if (byteArray[index] >= FORMAT_FIXSTR && byteArray[index] < FORMAT_FIXSTR+16) { + strLength = byteArray[index] - FORMAT_FIXSTR; + headerSize = 1; + } else if (byteArray[index] == FORMAT_STR8) { + strLength = byteArray[index+1]; + headerSize = 2; + } else if (byteArray[index] == FORMAT_STR16) { + strLength = (byteArray[index+1] << 8) + byteArray[index+2]; + headerSize = 3; + } else { + throw new MalformedFormatException(Application.loadResource(Rez.Strings.exceptionInvalidByte) as String); + } + + var convertOptions = { + :fromRepresentation => StringUtil.REPRESENTATION_BYTE_ARRAY, + :toRepresentation => StringUtil.REPRESENTATION_STRING_PLAIN_TEXT + }; + try { + unpackedStr = StringUtil.convertEncodedString(byteArray.slice(index+headerSize,index+headerSize+strLength), convertOptions) as String; + } + catch (ex) { + } + + return [unpackedStr, index+headerSize+strLength] as Array; + } + } +} diff --git a/source/MessagePackTest.mc b/source/MessagePackTest.mc index d56d8e9..46a3bdb 100644 --- a/source/MessagePackTest.mc +++ b/source/MessagePackTest.mc @@ -24,8 +24,8 @@ SOFTWARE. Author: Douglas Robertson (GitHub: douglasr; Garmin Connect: dbrobert) */ +import Toybox.Application; import Toybox.Lang; -import Toybox.System; import Toybox.Test; module MessagePack { @@ -370,4 +370,427 @@ module MessagePack { } } + module Deserialize { + + // Wrapping all test cases within a module will allow the compiler + // to eliminate the entire module when not building unit tests. + (:test) + module Test { + + // Check if two given single dimension arrays are "equal"; that is, they have the same number of elements + // in the same order/index. This function is needed for this test suite as the Array object does not + // implement Object.equals() as one would expect. + // @param arrayA an array of MessagePack objects + // @param arrayB an array of MessagePack objects + // @return boolean, true if they are "equal" or false otherwise + function equalArrays(arrayA as Array?, arrayB as Array?) as Boolean { + if (arrayA == null || arrayB == null) { + return false; + } + if (arrayA.size() != arrayB.size()) { + return false; + } + for (var i=0; i < arrayA.size(); i++) { + if (arrayA[i] != arrayB[i] && !(arrayA[i] as MsgPackObject).equals(arrayB[i] as MsgPackObject)) { + // check if the objects are arrays or dictionaries + if (arrayA[i] instanceof Array) { + if (!equalArrays(arrayA[i] as Array, arrayB[i] as Array)) { + return false; + } + } else if (arrayA[i] instanceof Dictionary) { + if (!equalDictionaries(arrayA[i] as Dictionary, arrayB[i] as Dictionary)) { + return false; + } + } else { + return false; + } + } + } + return true; + } + + // Check if two given dictionaries (maps) are "equal"; that is, they have the same number of key/value + // pairs. This function is needed for this test suite as the Dictionary object does not implement + // Object.equals() as one would expect. + // @param arrayA an array of MessagePack objects + // @param arrayB an array of MessagePack objects + // @return boolean, true if they are "equal" or false otherwise + function equalDictionaries(dictA as Dictionary?, dictB as Dictionary?) as Boolean { + if (dictA == null || dictB == null) { + return false; + } + if (dictA.keys().size() != dictB.keys().size()) { + return false; + } + for (var i=0; i < dictA.keys().size(); i++) { + var key = dictA.keys()[i]; + if (!dictB.hasKey(key)) { + return false; + } + + if (dictA[key] == null && dictB[key] != null) { + return false; + } + if (dictA[key] != dictB[key] && !(dictA[key] as Object).equals(dictB[key])) { + // check if the objects are arrays or dictionaries + if (dictA[key] instanceof Array) { + if (!equalArrays(dictA[key] as Array, dictB[key] as Array)) { + return false; + } + } else if (dictA[key] instanceof Dictionary) { + if (!equalDictionaries(dictA[key] as Dictionary, dictB[key] as Dictionary)) { + return false; + } + } else { + return false; + } + } + } + return true; + } + + (:test) + function testUnusedFormat(logger as Logger) as Boolean { + try { + unpack([0xC1]b); + return false; // should never reach this as an exception should be thrown (see below) + } + catch (ex instanceof MalformedFormatException) { + // all good, an exception should be thrown as this format byte (0xC1) is unused + } + return true; + } + + (:test) + function testFixArray(logger as Logger) as Boolean { + Test.assert(equalArrays(unpackArray([0x90]b, 0)[0] as Array, [])); + Test.assert(equalArrays(unpackArray([0x91, 0x01]b, 0)[0] as Array, [1])); + Test.assert(equalArrays(unpackArray([0x96, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05]b, 0)[0] as Array, [0,1,2,3,4,5])); + + // check an array within an array (need to do this manually because .equals() doesn't work on arrays) + // value == [0,1,[10,11,12],3]; + var arrayWithinArray = unpackArray([0x94,0x00,0x01,0x93,0x0A,0x0B,0x0C,0x03]b, 0)[0] as Array; + Test.assertEqual(arrayWithinArray.size(), 4); + Test.assert((arrayWithinArray[2] as Array).size() == 3); + + Test.assert(equalArrays( + unpackArray([0x9F, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E]b, 0)[0] as Array, + [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14] + )); + return true; + } + + (:test) + function testArray16(logger as Logger) as Boolean { + Test.assert(equalArrays( + unpackArray([0xDC, 0x00, 0x11, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10]b, 0)[0] as Array, + [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16] + )); + return true; + } + + (:test) + function testBin8(logger as Logger) as Boolean { + // TODO: need to implement these tests + return false; + } + + (:test) + function testBin16(logger as Logger) as Boolean { + // TODO: need to implement these tests + return false; + } + + (:test) + function testBin32(logger as Logger) as Boolean { + // TODO: need to implement these tests + return false; + } + + (:test) + function testBoolean(logger as Logger) as Boolean { + Test.assert((unpackBoolean([0xC2]b, 0)[0] as Boolean) == false); + Test.assert((unpackBoolean([0xC3]b, 0)[0] as Boolean) == true); + try { + unpackNull([0xC2, 0x00]b, 0); + return false; + } + catch (ex instanceof MalformedFormatException) { + // all good; an exception should be thrown as there are extra bytes + } + try { + unpackNull([0x00]b, 0); + return false; + } + catch (ex instanceof MalformedFormatException) { + // all good; an exception should be thrown as 0x00 is not a valid boolean value + } + return true; + } + + (:test) + function testNull(logger as Logger) as Boolean { + var parseResults; + + parseResults = unpackNull([0xC0]b, 0); + Test.assert(parseResults[0] == null && parseResults[1] == 1); + + try { + unpack([0xC0, 0x00]b); + return false; + } + catch (ex instanceof MalformedFormatException) { + // all good, an exception should be thrown as there are extra bytes + } + return true; + } + + + (:test) + function testFixMap(logger as Logger) as Boolean { + Test.assert(equalDictionaries(unpackDictionary([0x80]b, 0)[0] as Dictionary, {})); + Test.assert(equalDictionaries(unpackDictionary([0x81, 0xA1, 0x78, 0x0D]b, 0)[0] as Dictionary, {"x" => 13})); + Test.assert(equalDictionaries( + unpackDictionary([0x85, 0xA1, 0x61, 0x00, 0xA1, 0x62, 0x01, 0xA1, 0x63, 0xA1, 0x61, 0xA1, 0x64, 0x03, 0xA1, 0x65, 0x04]b, 0)[0] as Dictionary, + { "a" => 0, "b" => 1, "c" => "a", "d" => 3, "e" => 4 } + )); + return true; + } + + (:test) + function testMap16(logger as Logger) as Boolean { + Test.assert(equalDictionaries( + unpackDictionary([ + 0xDE, 0x00, 0x10, + 0xA1, 0x61, 0x00, 0xA1, 0x62, 0x01, 0xA1, 0x63, 0x02, 0xA1, 0x64, 0x03, + 0xA1, 0x65, 0x04, 0xA1, 0x66, 0x05, 0xA1, 0x67, 0x06, 0xA1, 0x68, 0x07, + 0xA1, 0x69, 0x08, 0xA1, 0x6A, 0x09, 0xA1, 0x6B, 0x0A, 0xA1, 0x6C, 0x0B, + 0xA1, 0x6D, 0x0C, 0xA1, 0x6E, 0x0D, 0xA1, 0x6F, 0x0E, 0xA1, 0x70, 0x0F + ]b, 0)[0] as Dictionary, + { + "a" => 0, "b" => 1, "c" => 2, "d" => 3, + "e" => 4, "f" => 5, "g" => 6, "h" => 7, + "i" => 8, "j" => 9, "k" => 10, "l" => 11, + "m" => 12, "n" => 13, "o" => 14, "p" => 15 + } + )); + return true; + } + + (:test) + function testPosFixInt(logger as Logger) as Boolean { + Test.assertEqual(unpackNumber([0x00]b, 0)[0], 0); + Test.assertEqual(unpackNumber([0x38]b, 0)[0], 56); + Test.assertEqual(unpackNumber([0x7F]b, 0)[0], 127); + //Test.assertNotEqual(unpackNumber([0x80]b, 0)[0], 128); + return true; + } + + (:test) + function testNegFixInt(logger as Logger) as Boolean { + Test.assertEqual(unpackNumber([0xFF]b, 0)[0], -1); + Test.assertEqual(unpackNumber([0xF3]b, 0)[0], -13); + Test.assertEqual(unpackNumber([0xE1]b, 0)[0], -31); + Test.assertEqual(unpackNumber([0xE0]b, 0)[0], -32); + //Test.assertNotEqual(unpackNumber([0xDF]b, 0)[0], -33); + return true; + } + + (:test) + function testUInt8(logger as Logger) as Boolean { + Test.assertEqual(unpackNumber([0xCC, 0x80]b, 0)[0], 128); + Test.assertEqual(unpackNumber([0xCC, 0xC9]b, 0)[0], 201); + Test.assertEqual(unpackNumber([0xCC, 0xFF]b, 0)[0], 255); + Test.assertNotEqual(unpackNumber([0xCC, 0x01, 0x00]b, 0)[0], 256); + return true; + } + + (:test) + function testUInt16(logger as Logger) as Boolean { + Test.assertEqual(unpackNumber([0xCD, 0x01, 0x00]b, 0)[0], 256); + Test.assertEqual(unpackNumber([0xCD, 0xFF, 0xFF]b, 0)[0], 65535); + Test.assertNotEqual(unpackNumber([0xCD, 0x01, 0xFF, 0xFF]b, 0)[0], 65536); + Test.assertNotEqual(unpackNumber([0xCD, 0x00, 0x00]b, 0)[0], 65536); + return true; + } + + (:test) + function testUInt32(logger as Logger) as Boolean { + Test.assertEqual(unpackNumber([0xCE, 0x00, 0x01, 0x00, 0x00]b, 0)[0], 65536); + Test.assertEqual(unpackNumber([0xCE, 0x00, 0x98, 0x9A, 0x86]b, 0)[0], 10001030); + Test.assertEqual(unpackNumber([0xCE, 0x7F, 0xFF, 0xFF, 0xFF]b, 0)[0], 2147483647); + Test.assertEqual(unpackNumber([0xCE, 0x80, 0x00, 0x00, 0x00]b, 0)[0], 2147483648l); + Test.assertEqual(unpackNumber([0xCE, 0xFF, 0xFF, 0xFF, 0xFF]b, 0)[0], 4294967295l); + Test.assertNotEqual(unpackNumber([0xCE, 0x01, 0x00, 0x00, 0x00, 0x00]b, 0)[0], 4294967296l); + return true; + } + + (:test) + function testUInt64(logger as Logger) as Boolean { + Test.assertEqual(unpackNumber([0xCF, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]b, 0)[0], 4294967296l); + Test.assertEqual(unpackNumber([0xCF, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]b, 0)[0], 9223372036854775807l); + return true; + } + + (:test) + function testInt8(logger as Logger) as Boolean { + Test.assertEqual(unpackNumber([0xD0, 0x80]b, 0)[0], -128); + Test.assertEqual(unpackNumber([0xD0, 0x81]b, 0)[0], -127); + Test.assertEqual(unpackNumber([0xD0, 0x9C]b, 0)[0], -100); + Test.assertEqual(unpackNumber([0xD0, 0xDF]b, 0)[0], -33); + Test.assertEqual(unpackNumber([0xD0, 0xE0]b, 0)[0], -32); // not the best way to do -32 but is allowed (I think) + // FIXME: this test should be 127 + //Test.assertNotEqual(unpackNumber([0xD0, 0x7F]b, 0)[0], -129); + return true; + } + + (:test) + function testInt16(logger as Logger) as Boolean { + Test.assertEqual(unpackNumber([0xD1, 0xFF, 0x7F]b, 0)[0], -129); + Test.assertEqual(unpackNumber([0xD1, 0xC0, 0x00]b, 0)[0], -16384); + Test.assertEqual(unpackNumber([0xD1, 0x80, 0x00]b, 0)[0], -32768); + // FIXME: this test should be 32767 + //Test.assertNotEqual(unpackNumber([0xD1, 0x7F, 0xFF]b, 0)[0], -32769); + return true; + } + + (:test) + function testInt32(logger as Logger) as Boolean { + Test.assertEqual(unpackNumber([0xD2, 0xFF, 0xFF, 0x7F, 0xFF]b, 0)[0], -32769); + Test.assertEqual(unpackNumber([0xD2, 0xF9, 0xEC, 0xE2, 0x8B]b, 0)[0], -101916021); + Test.assertEqual(unpackNumber([0xD2, 0x80, 0x00, 0x00, 0x00]b, 0)[0], -2147483648); + + //Test.assertNotEqual(packNumber(-2147483649l), [0xD2, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF]b); + return true; + } + + (:test) + function testInt64(logger as Logger) as Boolean { + Test.assertEqual(unpackNumber([0xD3, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF]b, 0)[0], -2147483649l); + Test.assertEqual(unpackNumber([0xD3, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]b, 0)[0], -9223372036854775807l); + Test.assertEqual(unpackNumber([0xD3, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]b, 0)[0], -9223372036854775808l); + return true; + } + + (:test) + function testFixStr(logger as Logger) as Boolean { + var parseResults; + parseResults = unpackString([0xA0]b, 0); + Test.assertEqual(parseResults[0], ""); + + parseResults = unpackString([0xA6,0x47,0x61,0x72,0x6D,0x69,0x6E]b, 0); + Test.assertEqual(parseResults[0], "Garmin"); + + parseResults = unpackString([0xAF,0x47,0x61,0x72,0x6D,0x69,0x6E,0x20,0x77,0x65,0x61,0x72,0x61,0x62,0x6C,0x65]b, 0); + Test.assertEqual(parseResults[0], "Garmin wearable"); + + return true; + } + + (:test) + function testStr8(logger as Logger) as Boolean { + var parseResults; + parseResults = unpackString([0xD9,0x10,0x47,0x61,0x72,0x6D,0x69,0x6E,0x20,0x77,0x65,0x61,0x72,0x61,0x62,0x6C,0x65,0x73]b, 0); + Test.assertEqual(parseResults[0], "Garmin wearables"); + + return true; + } + + (:test) + function testStr16(logger as Logger) as Boolean { + var longStrPacked = new [259]b; // 256 byte string + format byte + 2 byte length + var longStrUnpacked = ""; + + longStrPacked[0] = 0xDA; + longStrPacked[1] = 0x01; + longStrPacked[2] = 0x00; + + for (var i=0; i < 256; i++) { + longStrPacked[i+3] = 0x61; + longStrUnpacked = longStrUnpacked + "a"; // string length of 256 characters, all letter 'a' + } + var parseResults = unpackString(longStrPacked, 0); + Test.assertEqual(parseResults[0], longStrUnpacked); + return true; + } + + (:test) + function testUnpack(logger as Logger) as Boolean { + Test.assert(unpack([0xC0]b) == null); // value: null + Test.assert((unpack([0xC2]b) as Boolean) == false); // value: false + Test.assert((unpack([0xC3]b) as Boolean) == true); // value: true + + // value: string == "Garmin" + Test.assertEqual((unpack([0xA6,0x47,0x61,0x72,0x6D,0x69,0x6E]b) as String), "Garmin"); + + // value: array == [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16] + Test.assert(equalArrays( + unpack([0xDC, 0x00, 0x11, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10]b) as Array, + [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16] as Array + )); + + // test array with map embedded + Test.assert(equalArrays( + unpack([0x95, 0x00, 0x01, 0x82, 0xA1, 0x61, 0x00, 0xA1, 0x62, 0x01, 0x03, 0x04]b) as Array, + [0, 1, {"a" => 0, "b" => 1}, 3, 4] as Array + )); + + // TODO: test array within an array + Test.assert(equalArrays( + unpack([0x95, 0x00, 0x01, 0x93, 0x0A, 0x0B, 0x0C, 0x03, 0x04]b) as Array, + [0, 1, [10,11,12], 3, 4] as Array + )); + + // test map with array embedded + Test.assert(equalDictionaries( + unpack([0x83, 0xA1, 0x61, 0x00, 0xA1, 0x62, 0x93, 0x00, 0x01, 0x02, 0xA1, 0x63, 0x02]b) as Dictionary, + { "a" => 0, "b" => [0, 1, 2], "c" => 2 } + )); + + // test map within a map + Test.assert(equalDictionaries( + unpack([0x82, 0xA1, 0x61, 0x00, 0xA1, 0x62, 0x81, 0xA1, 0x64, 0xA1, 0x65]b) as Dictionary, + { "a" => 0, "b" => { "d" => "e"} } + )); + + // test map with array AND map embedded + Test.assert(equalDictionaries( + unpack([0x83, 0xA1, 0x61, 0x00, 0xA1, 0x62, 0x93, 0x00, 0x01, 0x02, 0xA1, 0x63, 0x81, 0xA1, 0x64, 0xA1, 0x65]b) as Dictionary, + { "a" => 0, "b" => [0, 1, 2], "c" => { "d" => "e"} } + )); + + // FIXME: this fails for some reason... + // unpack([0x81, 0xA1, 0xC1, 0x0D]b); + + // test that the extra bytes exception is thrown + try { + unpack([0x96, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x11]b); + return false; + } + catch (ex instanceof MalformedFormatException) { + // catching this exception is good; it should throw an extra bytes exception + var errorMsg = Lang.format(Application.loadResource(Rez.Strings.exceptionExtraBytes) as String, [1]); + Test.assertEqual(ex.getErrorMessage() as String, errorMsg); + } + + // test that the invalid byte exception is thrown + try { + unpack([0x81, 0xC1, 0x78, 0x0D]b); + return false; + } + catch (ex instanceof MalformedFormatException) { + // catching this exception is good; it should throw an extra bytes exception + var errorMsg = Application.loadResource(Rez.Strings.exceptionInvalidByte) as String; + Test.assertEqual(ex.getErrorMessage() as String, errorMsg); + } + + // TODO: test that the unexpected end of buffer (not enough bytes) exception is thrown + + return true; + } + + } + + } + }