From 658c451930bb43d0ecb49b7bbb6d17b0b2a76f5d Mon Sep 17 00:00:00 2001 From: Jeppe Klitgaard Date: Sat, 11 Apr 2015 17:47:25 +0200 Subject: [PATCH 01/18] Added exceptions for serialization and deserialization. These will come in handy later --- minecraft/exceptions.py | 16 +++++++++++++++- tests/test_exceptions.py | 25 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/minecraft/exceptions.py b/minecraft/exceptions.py index 80f57cd9..e3d57f61 100644 --- a/minecraft/exceptions.py +++ b/minecraft/exceptions.py @@ -5,5 +5,19 @@ class YggdrasilError(Exception): """ - Base `Exception` for the Yggdrasil authentication service. + Base ``Exception`` for the Yggdrasil authentication service. + """ + + +class DeserializationError(Exception): + """ + ``Exception`` raised when something went wrong during the deserialization + process. + """ + + +class SerializationError(Exception): + """ + ``Exception`` raised when something went wrong during the serialization + process. """ diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 4f5c989c..533fb35b 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,4 +1,5 @@ from minecraft.exceptions import YggdrasilError +from minecraft.exceptions import DeserializationError, SerializationError import unittest @@ -13,3 +14,27 @@ def test_raise_yggdrasil_error_message(self): raise YggdrasilError("Error!") self.assertEqual(str(e.exception), "Error!") + + +class RaiseDeserializationError(unittest.TestCase): + def test_raise_deserialization_error(self): + with self.assertRaises(DeserializationError): + raise DeserializationError + + def test_raise_deserialization_error_message(self): + with self.assertRaises(DeserializationError) as e: + raise DeserializationError("Error!") + + self.assertEqual(str(e.exception), "Error!") + + +class RaiseSerializationError(unittest.TestCase): + def test_raise_serialization_error(self): + with self.assertRaises(SerializationError): + raise SerializationError + + def test_raise_serialization_error_message(self): + with self.assertRaises(SerializationError) as e: + raise SerializationError("Error!") + + self.assertEqual(str(e.exception), "Error!") From 3e4c2ddebf84071276674d3844100cf49334b1a7 Mon Sep 17 00:00:00 2001 From: Jeppe Klitgaard Date: Sat, 11 Apr 2015 18:35:26 +0200 Subject: [PATCH 02/18] Abstraction in exceptions tests --- tests/test_exceptions.py | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 533fb35b..14eaa0aa 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -4,37 +4,27 @@ import unittest -class RaiseYggdrasilError(unittest.TestCase): - def test_raise_yggdrasil_error(self): - with self.assertRaises(YggdrasilError): - raise YggdrasilError +class BaseRaiseExceptionTest(unittest.TestCase): + EXCEPTION_TO_TEST = Exception - def test_raise_yggdrasil_error_message(self): - with self.assertRaises(YggdrasilError) as e: - raise YggdrasilError("Error!") + def test_raise_error(self): + with self.assertRaises(self.EXCEPTION_TO_TEST): + raise self.EXCEPTION_TO_TEST - self.assertEqual(str(e.exception), "Error!") + def test_raise_error_message(self): + with self.assertRaises(self.EXCEPTION_TO_TEST) as e: + raise self.EXCEPTION_TO_TEST("Error!") + self.assertEqual(str(e.exception), "Error!") -class RaiseDeserializationError(unittest.TestCase): - def test_raise_deserialization_error(self): - with self.assertRaises(DeserializationError): - raise DeserializationError - def test_raise_deserialization_error_message(self): - with self.assertRaises(DeserializationError) as e: - raise DeserializationError("Error!") +class RaiseYggdrasilError(BaseRaiseExceptionTest): + EXCEPTION_TO_TEST = YggdrasilError - self.assertEqual(str(e.exception), "Error!") +class RaiseDeserializationError(BaseRaiseExceptionTest): + EXCEPTION_TO_TEST = DeserializationError -class RaiseSerializationError(unittest.TestCase): - def test_raise_serialization_error(self): - with self.assertRaises(SerializationError): - raise SerializationError - def test_raise_serialization_error_message(self): - with self.assertRaises(SerializationError) as e: - raise SerializationError("Error!") - - self.assertEqual(str(e.exception), "Error!") +class RaiseSerializationError(BaseRaiseExceptionTest): + EXCEPTION_TO_TEST = SerializationError From a589c1e61f4d634d808fdd6d77cdc06cf4cbfeb1 Mon Sep 17 00:00:00 2001 From: Jeppe Klitgaard Date: Sat, 11 Apr 2015 17:47:25 +0200 Subject: [PATCH 03/18] Added exceptions for serialization and deserialization. These will come in handy later --- minecraft/exceptions.py | 16 +++++++++++++++- tests/test_exceptions.py | 25 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/minecraft/exceptions.py b/minecraft/exceptions.py index 80f57cd9..e3d57f61 100644 --- a/minecraft/exceptions.py +++ b/minecraft/exceptions.py @@ -5,5 +5,19 @@ class YggdrasilError(Exception): """ - Base `Exception` for the Yggdrasil authentication service. + Base ``Exception`` for the Yggdrasil authentication service. + """ + + +class DeserializationError(Exception): + """ + ``Exception`` raised when something went wrong during the deserialization + process. + """ + + +class SerializationError(Exception): + """ + ``Exception`` raised when something went wrong during the serialization + process. """ diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 4f5c989c..533fb35b 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,4 +1,5 @@ from minecraft.exceptions import YggdrasilError +from minecraft.exceptions import DeserializationError, SerializationError import unittest @@ -13,3 +14,27 @@ def test_raise_yggdrasil_error_message(self): raise YggdrasilError("Error!") self.assertEqual(str(e.exception), "Error!") + + +class RaiseDeserializationError(unittest.TestCase): + def test_raise_deserialization_error(self): + with self.assertRaises(DeserializationError): + raise DeserializationError + + def test_raise_deserialization_error_message(self): + with self.assertRaises(DeserializationError) as e: + raise DeserializationError("Error!") + + self.assertEqual(str(e.exception), "Error!") + + +class RaiseSerializationError(unittest.TestCase): + def test_raise_serialization_error(self): + with self.assertRaises(SerializationError): + raise SerializationError + + def test_raise_serialization_error_message(self): + with self.assertRaises(SerializationError) as e: + raise SerializationError("Error!") + + self.assertEqual(str(e.exception), "Error!") From 8ca5c7f35f279fcafc954d616a08fac8185d8ff2 Mon Sep 17 00:00:00 2001 From: Jeppe Klitgaard Date: Sat, 11 Apr 2015 18:35:26 +0200 Subject: [PATCH 04/18] Abstraction in exceptions tests --- tests/test_exceptions.py | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 533fb35b..14eaa0aa 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -4,37 +4,27 @@ import unittest -class RaiseYggdrasilError(unittest.TestCase): - def test_raise_yggdrasil_error(self): - with self.assertRaises(YggdrasilError): - raise YggdrasilError +class BaseRaiseExceptionTest(unittest.TestCase): + EXCEPTION_TO_TEST = Exception - def test_raise_yggdrasil_error_message(self): - with self.assertRaises(YggdrasilError) as e: - raise YggdrasilError("Error!") + def test_raise_error(self): + with self.assertRaises(self.EXCEPTION_TO_TEST): + raise self.EXCEPTION_TO_TEST - self.assertEqual(str(e.exception), "Error!") + def test_raise_error_message(self): + with self.assertRaises(self.EXCEPTION_TO_TEST) as e: + raise self.EXCEPTION_TO_TEST("Error!") + self.assertEqual(str(e.exception), "Error!") -class RaiseDeserializationError(unittest.TestCase): - def test_raise_deserialization_error(self): - with self.assertRaises(DeserializationError): - raise DeserializationError - def test_raise_deserialization_error_message(self): - with self.assertRaises(DeserializationError) as e: - raise DeserializationError("Error!") +class RaiseYggdrasilError(BaseRaiseExceptionTest): + EXCEPTION_TO_TEST = YggdrasilError - self.assertEqual(str(e.exception), "Error!") +class RaiseDeserializationError(BaseRaiseExceptionTest): + EXCEPTION_TO_TEST = DeserializationError -class RaiseSerializationError(unittest.TestCase): - def test_raise_serialization_error(self): - with self.assertRaises(SerializationError): - raise SerializationError - def test_raise_serialization_error_message(self): - with self.assertRaises(SerializationError) as e: - raise SerializationError("Error!") - - self.assertEqual(str(e.exception), "Error!") +class RaiseSerializationError(BaseRaiseExceptionTest): + EXCEPTION_TO_TEST = SerializationError From bac87695fff24837d0e97f475d70a0a3905145fd Mon Sep 17 00:00:00 2001 From: Jeppe Klitgaard Date: Tue, 14 Apr 2015 19:49:11 +0200 Subject: [PATCH 05/18] Implemented some of the datatypes and some tests for them. --- minecraft/networking/datatypes.py | 310 ++++++++++++++++++++++++++++++ tests/test_datatypes.py | 147 ++++++++++++++ 2 files changed, 457 insertions(+) create mode 100644 minecraft/networking/datatypes.py create mode 100644 tests/test_datatypes.py diff --git a/minecraft/networking/datatypes.py b/minecraft/networking/datatypes.py new file mode 100644 index 00000000..1273606c --- /dev/null +++ b/minecraft/networking/datatypes.py @@ -0,0 +1,310 @@ +""" +Contains the datatypes used by the networking part of `pyminecraft`. +The types are described at http://wiki.vg/Protocol#Data_types + +These datatypes are used by the packet definitions. +""" + +__all__ = ["ENDIANNESS", + "Datatype", + "Boolean", + "Byte", "UnsignedByte", + "Short", "UnsignedShort", + "Integer", "UnsignedInteger", + "Long", "UnsignedLong", + "LongLong", "UnsignedLongLong", + "Float", + "Double", + "VarInt", "VarLong", + "String"] + +from minecraft.exceptions import DeserializationError, SerializationError +from io import BytesIO +import struct +import collections + +ENDIANNESS = "!" # Network, big-endian + + +class Datatype(object): + """ + Base object for all `pyminecraft` networking datatypes. + + + .. note:: + If ``ALLOWED_SERIALIZATION_TYPES`` is not empty, only the types found + in ``ALLOWED_SERIALIZATION_TYPES`` are allowed as serialization + ``data``. This does somewhat go against the Duck-typing principle. + + The same applies for ``ALLOWED_DESERIALIZATION_TYPES``. + """ + FORMAT = "" + SIZE = 0 + + ALLOWED_SERIALIZATION_TYPES = tuple() + ALLOWED_DESERIALIZATION_TYPES = tuple() + + @classmethod + def read(cls, fileobject): + bin_data = fileobject.read(cls.SIZE) + return cls.deserialize(bin_data) + + @classmethod + def deserialize(cls, data): + cls.raise_deserialization_data(data) + + deserialized_data = struct.unpack(ENDIANNESS + cls.FORMAT, data)[0] + return deserialized_data + + @classmethod + def write(cls, fileobject, data): + return fileobject.write(cls.serialize(data)) + + @classmethod + def serialize(cls, data): + cls.raise_serialization_data(data) + + serialized_data = struct.pack(ENDIANNESS + cls.FORMAT, data) + return serialized_data + + @classmethod + def raise_serialization_data(cls, data): + """ + Raises an appropriate ``Exception`` if ``data`` is not valid. + + :return: ``None`` + :rtype: ``None`` + :raises: ``TypeError``, ``ValueError`` + """ + if (cls.ALLOWED_SERIALIZATION_TYPES and + not any([isinstance(data, type_) for type_ + in cls.ALLOWED_SERIALIZATION_TYPES])): + + e = "'data's type ('{}') is not an allowed type." + e = e.format(type(data).__name__) + + raise TypeError(e) + + cls._raise_serialization_value_error_data(data) + + return None + + @staticmethod + def _raise_serialization_value_error_data(data): + """ + Raises a ValueError if ``data`` is not valid. + + :return: ``None`` + :rtype: ``None`` + :raises: ``ValueError`` + """ + return None + + @classmethod + def raise_deserialization_data(cls, data): + """ + Raises an appropriate ``Exception`` if ``data`` is not valid. + + :return: ``None`` + :rtype: ``None`` + :raises: ``TypeError``, ``ValueError`` + """ + if (cls.ALLOWED_DESERIALIZATION_TYPES and + not any([isinstance(data, type_) for type_ + in cls.ALLOWED_DESERIALIZATION_TYPES])): + + err = "'data's type ('{}') is not an allowed type." + err = err.format(type(data).__name__) + + raise TypeError(err) + + if cls.SIZE != len(data): + err = "'data' must have a length of {}, not {}" + err = err.format(str(cls.SIZE), len(data)) + + raise TypeError(err) + + return None + + +class Boolean(Datatype): + FORMAT = "?" + SIZE = 1 + + ALLOWED_SERIALIZATION_TYPES = (bool,) + ALLOWED_DESERIALIZATION_TYPES = (collections.Sequence,) + + +class Byte(Datatype): + FORMAT = "b" + SIZE = 1 + + @staticmethod + def _raise_serialization_value_error_data(data): + if not -128 <= data <= 127: + e = "'data' must be an integer with value between -128 and 127." + raise ValueError(e) + + +class UnsignedByte(Datatype): + FORMAT = "B" + SIZE = 1 + + +class Short(Datatype): + FORMAT = "h" + SIZE = 2 + + +class UnsignedShort(Datatype): + FORMAT = "H" + SIZE = 2 + + +class Integer(Datatype): + FORMAT = "i" + SIZE = 4 + + +class UnsignedInteger(Datatype): + FORMAT = "I" + SIZE = 4 + + +class Long(Datatype): + FORMAT = "l" + SIZE = 4 + + +class UnsignedLong(Datatype): + FORMAT = "L" + SIZE = 4 + + +class LongLong(Datatype): + FORMAT = "q" + SIZE = 8 + + +class UnsignedLongLong(Datatype): + FORMAT = "Q" + SIZE = 8 + + +class Float(Datatype): + FORMAT = "f" + SIZE = 4 + + +class Double(Datatype): + FORMAT = "d" + SIZE = 8 + + +class VarInt(Datatype): + # See: https://developers.google.com/protocol-buffers/docs/encoding#varints + # See: https://github.com/ammaraskar/pyCraft/blob/7e8df473520d57ca22fb57888681f51705128cdc/network/types.py#l123 # noqa + # See: https://github.com/google/protobuf/blob/0c59f2e6fc0a2cb8e8e3b4c7327f650e8586880a/python/google/protobuf/internal/decoder.py#l107 # noqa + # According to http://wiki.vg/Protocol#Data_types, + # MineCraftian VarInts can be at most 5 bytes. + + # Maximum integer value: size of serialized VarInt in bytes + SIZE_TABLE = { + 2**7: 1, + 2**14: 2, + 2**21: 3, + 2**28: 4, + 2**35: 5, + } + + # Largest element in SIZE_TABLE, assuming largest element is last. + MAX_SIZE = list(SIZE_TABLE.items())[-1][-1] + + @classmethod + def read(cls, fileobject): + number = 0 # The decoded number + + i = 0 # Incrementor + while True: + if i > cls.MAX_SIZE: # Check if we have exceeded max-size + name_of_self = str(type(cls)) + e = "Data too large to be a {}".format(name_of_self) + raise DeserializationError(e) + + try: + byte = ord(fileobject.read(1)) # Read a byte as integer + except TypeError: + e = "Fileobject ran out of data. Socket closed?" + raise DeserializationError(e) + + number |= ((byte & 0x7f) << (i * 7)) + if not (byte & 0x80): + break + + i += 1 + return number + + @classmethod + def deserialize(cls, data): + data_fileobject = BytesIO(bytes(data)) + return cls.read(data_fileobject) + + @classmethod + def serialize(cls, data): + if data > cls.SIZE_TABLE[-1][0]: + name_of_self = str(type(cls)) + e = "Number too big to serialize as {}".format(name_of_self) + raise SerializationError(e) + + result = bytes() # Where we store the serialized number + + while True: + byte = data & 0x7f + data >>= 7 + + result += UnsignedByte.serialize(byte | (0x80 if data > 0 else 0)) + + if not data: + break + + return result + + +class VarLong(VarInt): + # According to http://wiki.vg/Protocol#Data_types, + # MineCraftian VarInts can be at most 10 bytes. + SIZE_TABLE = VarInt.SIZE_TABLE + SIZE_TABLE.update( + { + 2**42: 6, + 2**49: 7, + 2**56: 8, + 2**63: 9, + 2**70: 10, + } + ) + + MAX_SIZE = list(SIZE_TABLE.items())[-1][-1] + + +class String(Datatype): + FORMAT = "utf-8" + + @classmethod + def read(cls, fileobject): + str_size = VarInt.read(fileobject) + string = fileobject.read(str_size).decode(cls.FORMAT) + + return string + + @classmethod + def deserialize(cls, data): + data_fileobject = BytesIO(bytes(data)) + return cls.read(data_fileobject) + + @classmethod + def serialize(cls, data): + data = data.encode(cls.FORMAT) + len_data = VarInt.serialize(len(data)) + + return len_data + data diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py new file mode 100644 index 00000000..ca3e968a --- /dev/null +++ b/tests/test_datatypes.py @@ -0,0 +1,147 @@ +from minecraft.networking.datatypes import * +from minecraft.exceptions import DeserializationError + +import unittest + + +# # Note, we use the actual classes as keys. +# # Format: DATATYPE_OBJ = (LIST_OF_VALID_VALUES, LIST_OF_INVALID_VALUES) +# TEST_DATA = { +# Boolean: [True, False], +# Byte: [-127, -25, 0, 125], +# UnsignedByte: [0, 125], +# Byte: [-22, 22], +# Short: [-340, 22, 350], +# UnsignedShort: [0, 400], +# Integer: [-1000, 1000], +# VarInt: [1, 250, 50000, 10000000], +# Long: [50000000], +# Float: [21.000301], +# Double: [36.004002], +# ShortPrefixedByteArray: [bytes(245)], +# VarIntPrefixedByteArray: [bytes(1234)], +# StringType: ["hello world"] +# } + + +class BaseDatatypeTester(unittest.TestCase): + DATATYPE_CLS = Datatype # We use Datatype as a an example here. + + # TEST_DATA_VALID_VALUES should have the following format: + # [(DESERIALIZED_VALUE, SERIALIZED_VALUE), ...] + # + # So that DESERIALIZED_VALUE is SERIALIZED_VALUE when serialized + # and vice versa. + + TEST_DATA_VALID_VALUES = [] + + # TEST_DATA_INVALID_SERIALIZATION_VALUES should be a list of tuples + # containing the value and the expected exception. + TEST_DATA_INVALID_SERIALIZATION_VALUES = [] + + # TEST_DATA_INVALID_DESERIALIZATION_VALUES should be a list of tuples + # containing the value and the expected exception. + TEST_DATA_INVALID_DESERIALIZATION_VALUES = [] + + def test_init(self): + d = self.DATATYPE_CLS() # noqa + + def test_init_with_arg(self): + # We shouldn't accept any parameters. + with self.assertRaises(TypeError): + d = self.DATATYPE_CLS("This is a positional argument...") # noqa + + def test_valid_data_serialization_values(self): + for deserialized_val, serialized_val in self.TEST_DATA_VALID_VALUES: + self.assertEqual(self.DATATYPE_CLS.serialize(deserialized_val), + serialized_val) + + def test_valid_data_deserialization_values(self): + for deserialized_val, serialized_val in self.TEST_DATA_VALID_VALUES: + self.assertEqual(self.DATATYPE_CLS.deserialize(serialized_val), + deserialized_val) + + def test_invalid_data_serialization_values(self): + for value, exception in self.TEST_DATA_INVALID_SERIALIZATION_VALUES: + with self.assertRaises(exception): + self.DATATYPE_CLS.serialize(value) + + def test_invalid_data_deserialization_values(self): + for value, exception in self.TEST_DATA_INVALID_DESERIALIZATION_VALUES: + with self.assertRaises(exception): + self.DATATYPE_CLS.deserialize(value) + + +class DatatypeTest(BaseDatatypeTester): + DATATYPE_CLS = Datatype + + +class BooleanTest(BaseDatatypeTester): + DATATYPE_CLS = Boolean + + TEST_DATA_VALID_VALUES = [ + (True, b"\x01"), + (False, b"\x00") + ] + + TEST_DATA_INVALID_SERIALIZATION_VALUES = [ + ("\x00", TypeError), + ("\x01", TypeError), + ("\x02", TypeError), + (-1, TypeError), + (0, TypeError), + (1, TypeError), + ("", TypeError), + ("Test", TypeError) + ] + + TEST_DATA_INVALID_DESERIALIZATION_VALUES = [ + (-1, TypeError), + (0, TypeError), + (1, TypeError), + ("", TypeError), + ("Test", TypeError), + (True, TypeError), + (False, TypeError) + ] + + +class ByteTest(BaseDatatypeTester): + DATATYPE_CLS = Byte + + TEST_DATA_VALID_VALUES = [ + (-128, b"\x80"), + (-22, b"\xea"), + (0, b"\x00"), + (22, b"\x16"), + (127, b"\x7f") + ] + + TEST_DATA_INVALID_SERIALIZATION_VALUES = [ + (-500, ValueError), + (128, ValueError), + (1024, ValueError), + + ] + +# def _bin(binstr): +# """ +# Accepts a pretty looking string of binary numbers and +# returns the binary number. + +# Parameters: +# binstr - a string with this format: `'1010 0010 0100'`. + +# Returns: +# Int +# """ +# binstr = binstr.replace(" ", "") # Remove all spaces. +# num = int("0b" + binstr, 2) + +# return num + + +# class VarIntTests(unittest.TestCase): +# def test1(self): +# self.assertEqual(VarInt.deserialize(_bin("0000 0001")), 1) +# self.assertEqual(VarInt.deserialize(_bin("1010 1100 0000 0010")), 300) From 73b990cc815f3322a4e85835813cedee1b569a76 Mon Sep 17 00:00:00 2001 From: Jeppe Klitgaard Date: Tue, 14 Apr 2015 20:06:53 +0200 Subject: [PATCH 06/18] Added DISALLOWED_SERIALIZATION_TYPES, making it easier to cherrypick which types should be allowed. This was done to handle bool being a subclass of int --- minecraft/networking/datatypes.py | 26 +++++++++++++++++++++++--- tests/test_datatypes.py | 10 ++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/minecraft/networking/datatypes.py b/minecraft/networking/datatypes.py index 1273606c..d0405fa6 100644 --- a/minecraft/networking/datatypes.py +++ b/minecraft/networking/datatypes.py @@ -37,6 +37,17 @@ class Datatype(object): ``data``. This does somewhat go against the Duck-typing principle. The same applies for ``ALLOWED_DESERIALIZATION_TYPES``. + + .. note:: + If ``DISALLOWED_SERIALIZATION_TYPES`` is not empty, only the types + found in ``DISALLOWED_SERIALIZATION_TYPES`` are allowed as + serialization ``data``. This does somewhat go against the + Duck-typing principle. + + ``DISALLOWED_SERIALIZATION_TYPES`` exists as a way to exclude certain + subclasses of a given type. + + The same applies for ``DISALLOWED_DESERIALIZATION_TYPES``. """ FORMAT = "" SIZE = 0 @@ -44,6 +55,9 @@ class Datatype(object): ALLOWED_SERIALIZATION_TYPES = tuple() ALLOWED_DESERIALIZATION_TYPES = tuple() + DISALLOWED_SERIALIZATION_TYPES = tuple() + DISALLOWED_SERIALIZATION_TYPES = tuple() + @classmethod def read(cls, fileobject): bin_data = fileobject.read(cls.SIZE) @@ -76,14 +90,17 @@ def raise_serialization_data(cls, data): :rtype: ``None`` :raises: ``TypeError``, ``ValueError`` """ + error_message = "'data's type ('{}') is not an allowed type." + error_message = error_message.format(type(data).__name__) if (cls.ALLOWED_SERIALIZATION_TYPES and not any([isinstance(data, type_) for type_ in cls.ALLOWED_SERIALIZATION_TYPES])): - e = "'data's type ('{}') is not an allowed type." - e = e.format(type(data).__name__) + raise TypeError(error_message) - raise TypeError(e) + for type_ in cls.DISALLOWED_SERIALIZATION_TYPES: + if isinstance(data, type_): + raise TypeError(error_message) cls._raise_serialization_value_error_data(data) @@ -139,6 +156,9 @@ class Byte(Datatype): FORMAT = "b" SIZE = 1 + ALLOWED_SERIALIZATION_TYPES = (int,) + DISALLOWED_SERIALIZATION_TYPES = (bool,) + @staticmethod def _raise_serialization_value_error_data(data): if not -128 <= data <= 127: diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index ca3e968a..9f654a9d 100644 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -64,6 +64,7 @@ def test_valid_data_deserialization_values(self): def test_invalid_data_serialization_values(self): for value, exception in self.TEST_DATA_INVALID_SERIALIZATION_VALUES: with self.assertRaises(exception): + print(value) self.DATATYPE_CLS.serialize(value) def test_invalid_data_deserialization_values(self): @@ -121,7 +122,16 @@ class ByteTest(BaseDatatypeTester): (-500, ValueError), (128, ValueError), (1024, ValueError), + ("", TypeError), + ("Test", TypeError), + (b"\x00", TypeError), + (b"\x80", TypeError), + (True, TypeError), + (False, TypeError), + ] + TEST_DATA_INVALID_DESERIALIZATION_VALUES = [ + ] # def _bin(binstr): From a31f1c543ff4a7fc4998346e05def4ba5ae3b9c1 Mon Sep 17 00:00:00 2001 From: Jeppe Klitgaard Date: Thu, 16 Apr 2015 09:02:54 +0200 Subject: [PATCH 07/18] More datatype abstraction: * NumberDatatype * StringDatatype _raise_serialization_value_error_data is now a classmethod instead of a classmethod. Tests for MIN and MAX number values of NumberDatatypes have been made more abstract. TEST_DATA_* has been removed from variable names. Sometimes it can get a little too rediculous with these var-names, don't you think? --- minecraft/networking/datatypes.py | 112 +++++++++++++++++------ tests/test_datatypes.py | 146 ++++++++++++++++++++++-------- 2 files changed, 196 insertions(+), 62 deletions(-) diff --git a/minecraft/networking/datatypes.py b/minecraft/networking/datatypes.py index d0405fa6..9325c0c9 100644 --- a/minecraft/networking/datatypes.py +++ b/minecraft/networking/datatypes.py @@ -6,7 +6,7 @@ """ __all__ = ["ENDIANNESS", - "Datatype", + "Datatype", "NumberDatatype", "StringDatatype", "Boolean", "Byte", "UnsignedByte", "Short", "UnsignedShort", @@ -92,6 +92,7 @@ def raise_serialization_data(cls, data): """ error_message = "'data's type ('{}') is not an allowed type." error_message = error_message.format(type(data).__name__) + if (cls.ALLOWED_SERIALIZATION_TYPES and not any([isinstance(data, type_) for type_ in cls.ALLOWED_SERIALIZATION_TYPES])): @@ -106,8 +107,8 @@ def raise_serialization_data(cls, data): return None - @staticmethod - def _raise_serialization_value_error_data(data): + @classmethod + def _raise_serialization_value_error_data(cls, data): """ Raises a ValueError if ``data`` is not valid. @@ -137,13 +138,51 @@ def raise_deserialization_data(cls, data): if cls.SIZE != len(data): err = "'data' must have a length of {}, not {}" - err = err.format(str(cls.SIZE), len(data)) + err = err.format(str(cls.SIZE), str(len(data))) - raise TypeError(err) + raise ValueError(err) + + return None + + +class NumberDatatype(Datatype): + """ + Base abstract class for all number-like minecraft networking datatypes. + + .. note:: + Numbers to be serialized must be between this classes + ``MIN_NUMBER_VALUE`` and ``MAX_NUMBER_VALUE``, or a ``ValueError`` will + be raised. + + If ``MIN_NUMBER_VALUE`` or ``MAX_NUMBER_VALUE`` are ``None`` + (as in the case of float), checking is left to the ``struct`` module. + """ + + MIN_NUMBER_VALUE = None + MAX_NUMBER_VALUE = None + + ALLOWED_SERIALIZATION_TYPES = (int,) + DISALLOWED_SERIALIZATION_TYPES = (bool,) + + @classmethod + def _raise_serialization_value_error_data(cls, data): + if (cls.MIN_NUMBER_VALUE is not None + and cls.MAX_NUMBER_VALUE is not None): + + if not cls.MIN_NUMBER_VALUE <= data <= cls.MAX_NUMBER_VALUE: + err = "'data' must be an integer with value between {} and {}." + err = err.format(str(cls.MIN_NUMBER_VALUE), + str(cls.MAX_NUMBER_VALUE)) + + raise ValueError(err) return None +class StringDatatype(Datatype): + pass + + class Boolean(Datatype): FORMAT = "?" SIZE = 1 @@ -152,76 +191,97 @@ class Boolean(Datatype): ALLOWED_DESERIALIZATION_TYPES = (collections.Sequence,) -class Byte(Datatype): +class Byte(NumberDatatype): FORMAT = "b" SIZE = 1 - ALLOWED_SERIALIZATION_TYPES = (int,) - DISALLOWED_SERIALIZATION_TYPES = (bool,) + MIN_NUMBER_VALUE = -128 + MAX_NUMBER_VALUE = 127 - @staticmethod - def _raise_serialization_value_error_data(data): - if not -128 <= data <= 127: - e = "'data' must be an integer with value between -128 and 127." - raise ValueError(e) - -class UnsignedByte(Datatype): +class UnsignedByte(NumberDatatype): FORMAT = "B" SIZE = 1 + MIN_NUMBER_VALUE = 0 + MAX_NUMBER_VALUE = 255 + -class Short(Datatype): +class Short(NumberDatatype): FORMAT = "h" SIZE = 2 + MIN_NUMBER_VALUE = -32768 + MAX_NUMBER_VALUE = 32767 -class UnsignedShort(Datatype): + +class UnsignedShort(NumberDatatype): FORMAT = "H" SIZE = 2 + MIN_NUMBER_VALUE = 0 + MAX_NUMBER_VALUE = 65535 + -class Integer(Datatype): +class Integer(NumberDatatype): FORMAT = "i" SIZE = 4 + MIN_NUMBER_VALUE = -2147483648 + MAX_NUMBER_VALUE = 2147483647 -class UnsignedInteger(Datatype): + +class UnsignedInteger(NumberDatatype): FORMAT = "I" SIZE = 4 + MIN_NUMBER_VALUE = 0 + MAX_NUMBER_VALUE = 4294967295 + -class Long(Datatype): +class Long(NumberDatatype): FORMAT = "l" SIZE = 4 + MIN_NUMBER_VALUE = -2147483648 + MAX_NUMBER_VALUE = 2147483647 -class UnsignedLong(Datatype): + +class UnsignedLong(NumberDatatype): FORMAT = "L" SIZE = 4 + MIN_NUMBER_VALUE = 0 + MAX_NUMBER_VALUE = 4294967295 + -class LongLong(Datatype): +class LongLong(NumberDatatype): FORMAT = "q" SIZE = 8 + MIN_NUMBER_VALUE = -9223372036854775808 + MAX_NUMBER_VALUE = 9223372036854775807 -class UnsignedLongLong(Datatype): + +class UnsignedLongLong(NumberDatatype): FORMAT = "Q" SIZE = 8 + MIN_NUMBER_VALUE = 0 + MAX_NUMBER_VALUE = 18446744073709551615 + -class Float(Datatype): +class Float(NumberDatatype): FORMAT = "f" SIZE = 4 -class Double(Datatype): +class Double(NumberDatatype): FORMAT = "d" SIZE = 8 -class VarInt(Datatype): +class VarInt(NumberDatatype): # See: https://developers.google.com/protocol-buffers/docs/encoding#varints # See: https://github.com/ammaraskar/pyCraft/blob/7e8df473520d57ca22fb57888681f51705128cdc/network/types.py#l123 # noqa # See: https://github.com/google/protobuf/blob/0c59f2e6fc0a2cb8e8e3b4c7327f650e8586880a/python/google/protobuf/internal/decoder.py#l107 # noqa diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index 9f654a9d..c0f3dbd2 100644 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -27,21 +27,21 @@ class BaseDatatypeTester(unittest.TestCase): DATATYPE_CLS = Datatype # We use Datatype as a an example here. - # TEST_DATA_VALID_VALUES should have the following format: + # VALID_VALUES should have the following format: # [(DESERIALIZED_VALUE, SERIALIZED_VALUE), ...] # # So that DESERIALIZED_VALUE is SERIALIZED_VALUE when serialized # and vice versa. - TEST_DATA_VALID_VALUES = [] + VALID_VALUES = [] - # TEST_DATA_INVALID_SERIALIZATION_VALUES should be a list of tuples + # INVALID_SERIALIZATION_VALUES should be a list of tuples # containing the value and the expected exception. - TEST_DATA_INVALID_SERIALIZATION_VALUES = [] + INVALID_SERIALIZATION_VALUES = [] - # TEST_DATA_INVALID_DESERIALIZATION_VALUES should be a list of tuples + # INVALID_DESERIALIZATION_VALUES should be a list of tuples # containing the value and the expected exception. - TEST_DATA_INVALID_DESERIALIZATION_VALUES = [] + INVALID_DESERIALIZATION_VALUES = [] def test_init(self): d = self.DATATYPE_CLS() # noqa @@ -52,40 +52,84 @@ def test_init_with_arg(self): d = self.DATATYPE_CLS("This is a positional argument...") # noqa def test_valid_data_serialization_values(self): - for deserialized_val, serialized_val in self.TEST_DATA_VALID_VALUES: + for deserialized_val, serialized_val in self.VALID_VALUES: self.assertEqual(self.DATATYPE_CLS.serialize(deserialized_val), serialized_val) def test_valid_data_deserialization_values(self): - for deserialized_val, serialized_val in self.TEST_DATA_VALID_VALUES: + for deserialized_val, serialized_val in self.VALID_VALUES: self.assertEqual(self.DATATYPE_CLS.deserialize(serialized_val), deserialized_val) def test_invalid_data_serialization_values(self): - for value, exception in self.TEST_DATA_INVALID_SERIALIZATION_VALUES: + for value, exception in self.INVALID_SERIALIZATION_VALUES: with self.assertRaises(exception): - print(value) self.DATATYPE_CLS.serialize(value) def test_invalid_data_deserialization_values(self): - for value, exception in self.TEST_DATA_INVALID_DESERIALIZATION_VALUES: + for value, exception in self.INVALID_DESERIALIZATION_VALUES: with self.assertRaises(exception): self.DATATYPE_CLS.deserialize(value) +class BaseNumberDatatypeTester(BaseDatatypeTester): + BASE_NUMBER_INVALID_SERIALIZATION_VALUES = [ + ("", TypeError), + ("Test", TypeError), + (b"\x00", TypeError), + (b"\x80", TypeError), + (True, TypeError), + (False, TypeError) + ] + + def base_number_invalid_data_serialization_values(self): + values_to_test = BASE_INVALID_SERIALIZATION_VALUES + values_to_test.extend([ + (self.DATATYPE_CLS.MIN_NUMBER_VALUE - 1, ValueError), + (self.DATATYPE_CLS.MAX_NUMBER_VALUE + 1, ValueError) + ]) + + for value, exception in values_to_test: + with self.assertRaises(exception): + self.DATATYPE_CLS.serialize(value) + + +class BaseStringDatatypeTester(BaseDatatypeTester): + pass + + +BASE_INVALID_DESERIALIZATION_VALUES = [ + (-1, TypeError), + (0, TypeError), + (1, TypeError), + ("", ValueError), + ("Test", ValueError), + (True, TypeError), + (False, TypeError) +] + + class DatatypeTest(BaseDatatypeTester): DATATYPE_CLS = Datatype +class NumberDatatypeTest(BaseNumberDatatypeTester): + DATATYPE_CLS = NumberDatatype + + +class StringDatatypeTest(BaseStringDatatypeTester): + DATATYPE_CLS = StringDatatype + + class BooleanTest(BaseDatatypeTester): DATATYPE_CLS = Boolean - TEST_DATA_VALID_VALUES = [ + VALID_VALUES = [ (True, b"\x01"), (False, b"\x00") ] - TEST_DATA_INVALID_SERIALIZATION_VALUES = [ + INVALID_SERIALIZATION_VALUES = [ ("\x00", TypeError), ("\x01", TypeError), ("\x02", TypeError), @@ -96,21 +140,19 @@ class BooleanTest(BaseDatatypeTester): ("Test", TypeError) ] - TEST_DATA_INVALID_DESERIALIZATION_VALUES = [ - (-1, TypeError), - (0, TypeError), - (1, TypeError), - ("", TypeError), - ("Test", TypeError), - (True, TypeError), - (False, TypeError) - ] + # Use list(BASE_INVALID_DESERIALIZATION_VALUES) instead of + # just = BASE_INVALID_DESERIALIZATION_VALUES, cause we want a COPY + # of the list, NOT a reference (that we'll later extend!) + INVALID_DESERIALIZATION_VALUES = list(BASE_INVALID_DESERIALIZATION_VALUES) + INVALID_DESERIALIZATION_VALUES.extend([ + (b"\x00\x01", ValueError) + ]) -class ByteTest(BaseDatatypeTester): +class ByteTest(BaseNumberDatatypeTester): DATATYPE_CLS = Byte - TEST_DATA_VALID_VALUES = [ + VALID_VALUES = [ (-128, b"\x80"), (-22, b"\xea"), (0, b"\x00"), @@ -118,22 +160,54 @@ class ByteTest(BaseDatatypeTester): (127, b"\x7f") ] - TEST_DATA_INVALID_SERIALIZATION_VALUES = [ - (-500, ValueError), - (128, ValueError), - (1024, ValueError), - ("", TypeError), - ("Test", TypeError), - (b"\x00", TypeError), - (b"\x80", TypeError), - (True, TypeError), - (False, TypeError), + INVALID_DESERIALIZATION_VALUES = list(BASE_INVALID_DESERIALIZATION_VALUES) + INVALID_DESERIALIZATION_VALUES.extend([ + (b"\x01\x20", ValueError), + ]) + + +class UnsignedByteTest(BaseNumberDatatypeTester): + DATATYPE_CLS = UnsignedByte + + VALID_VALUES = [ + (0, b"\x00"), + (127, b"\x7f"), + (255, b"\xff") ] - TEST_DATA_INVALID_DESERIALIZATION_VALUES = [ - + INVALID_DESERIALIZATION_VALUES = ByteTest.INVALID_DESERIALIZATION_VALUES + + +class ShortTest(BaseNumberDatatypeTester): + DATATYPE_CLS = Short + + VALID_VALUES = [ + (-32768, b"\x80\x00"), + (-10000, b"\xd8\xf0"), + (0, b"\x00\x00"), + (5000, b"\x13\x88"), + (32767, b"\x7f\xff") ] + INVALID_DESERIALIZATION_VALUES = list(BASE_INVALID_DESERIALIZATION_VALUES) + INVALID_DESERIALIZATION_VALUES.extend([ + (b"\xff", ValueError), + (b"\xff\x01\x6e", ValueError) + ]) + + +class UnsignedShortTest(BaseNumberDatatypeTester): + DATATYPE_CLS = UnsignedShort + + VALID_VALUES = [ + (0, b"\x00\x00"), + (10000, b"'\x10"), + (32767, b"\x7f\xff"), + (65535, b"\xff\xff") + ] + + INVALID_DESERIALIZATION_VALUES = ShortTest.INVALID_DESERIALIZATION_VALUES + # def _bin(binstr): # """ # Accepts a pretty looking string of binary numbers and From 89a4368c1e7a5040bc7c691c55532578c2a31ae2 Mon Sep 17 00:00:00 2001 From: Jeppe Klitgaard Date: Thu, 16 Apr 2015 09:12:26 +0200 Subject: [PATCH 08/18] Added tests for compat.py --- tests/test_compat.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tests/test_compat.py diff --git a/tests/test_compat.py b/tests/test_compat.py new file mode 100644 index 00000000..d114ee95 --- /dev/null +++ b/tests/test_compat.py @@ -0,0 +1,8 @@ +from minecraft import compat # noqa unused-import + +import unittest + + +class TestCompatInput(unittest.TestCase): + def test_import_input(self): + from minecraft.compat import input # noqa unused-import From 5eddb67489570e49e1adda7a454b79069265212f Mon Sep 17 00:00:00 2001 From: Jeppe Klitgaard Date: Thu, 16 Apr 2015 15:49:43 +0200 Subject: [PATCH 09/18] Removed non-empty string deserialization test, as strings and bytes are the same on python2. Fixed a non PEP8-compliant space. Added tests for Integer and UnsignedInteger datatypes. --- tests/test_datatypes.py | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index c0f3dbd2..6f04c16c 100644 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -103,7 +103,6 @@ class BaseStringDatatypeTester(BaseDatatypeTester): (0, TypeError), (1, TypeError), ("", ValueError), - ("Test", ValueError), (True, TypeError), (False, TypeError) ] @@ -141,7 +140,7 @@ class BooleanTest(BaseDatatypeTester): ] # Use list(BASE_INVALID_DESERIALIZATION_VALUES) instead of - # just = BASE_INVALID_DESERIALIZATION_VALUES, cause we want a COPY + # just = BASE_INVALID_DESERIALIZATION_VALUES, cause we want a COPY # of the list, NOT a reference (that we'll later extend!) INVALID_DESERIALIZATION_VALUES = list(BASE_INVALID_DESERIALIZATION_VALUES) INVALID_DESERIALIZATION_VALUES.extend([ @@ -208,6 +207,39 @@ class UnsignedShortTest(BaseNumberDatatypeTester): INVALID_DESERIALIZATION_VALUES = ShortTest.INVALID_DESERIALIZATION_VALUES + +class IntegerTest(BaseNumberDatatypeTester): + DATATYPE_CLS = Integer + + VALID_VALUES = [ + (-2147483648, b"\x80\x00\x00\x00"), + (-1000000, b"\xff\xf0\xbd\xc0"), + (0, b"\x00\x00\x00\x00"), + (10000000, b"\x00\x98\x96\x80"), + (2147483647, b"\x7f\xff\xff\xff") + ] + + INVALID_DESERIALIZATION_VALUES = list(BASE_INVALID_DESERIALIZATION_VALUES) + INVALID_DESERIALIZATION_VALUES.extend([ + (b"\xff", ValueError), + (b"\x00\x01", ValueError), + (b"\x76\x80\x80\x10\xff", ValueError) + ]) + + +class UnsignedIntegerTest(BaseNumberDatatypeTester): + DATATYPE_CLS = UnsignedInteger + + VALID_VALUES = [ + (0, "\x00\x00\x00\x00"), + (10000000, "\x00\x98\x96\x80"), + (2147483647, b"\x7f\xff\xff\xff"), + (4294967295, "\xff\xff\xff\xff") + ] + + INVALID_DESERIALIZATION_VALUES = IntegerTest.INVALID_DESERIALIZATION_VALUES + + # def _bin(binstr): # """ # Accepts a pretty looking string of binary numbers and From 4e6f223a7d8da0099f053322d16eafc4197769f2 Mon Sep 17 00:00:00 2001 From: Jeppe Klitgaard Date: Thu, 16 Apr 2015 15:59:18 +0200 Subject: [PATCH 10/18] Fixed 3 of the VALID_VALUES on the UnsignedIntegerTest not being bytes. Added Long and UnsignedLong tests. --- tests/test_datatypes.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index 6f04c16c..4fae167f 100644 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -231,15 +231,23 @@ class UnsignedIntegerTest(BaseNumberDatatypeTester): DATATYPE_CLS = UnsignedInteger VALID_VALUES = [ - (0, "\x00\x00\x00\x00"), - (10000000, "\x00\x98\x96\x80"), + (0, b"\x00\x00\x00\x00"), + (10000000, b"\x00\x98\x96\x80"), (2147483647, b"\x7f\xff\xff\xff"), - (4294967295, "\xff\xff\xff\xff") + (4294967295, b"\xff\xff\xff\xff") ] INVALID_DESERIALIZATION_VALUES = IntegerTest.INVALID_DESERIALIZATION_VALUES +class LongTest(IntegerTest): + DATATYPE_CLS = Long + + +class UnsignedLongTest(UnsignedInteger): + DATATYPE_CLS = UnsignedLong + + # def _bin(binstr): # """ # Accepts a pretty looking string of binary numbers and From ab4d78dc534b6456ca5c2c07cdb6f22b641aa5a2 Mon Sep 17 00:00:00 2001 From: Jeppe Klitgaard Date: Thu, 16 Apr 2015 16:43:14 +0200 Subject: [PATCH 11/18] Started using six --- minecraft/compat.py | 15 +-------------- requirements.txt | 1 + start.py | 2 +- tests/test_compat.py | 7 ++----- tox.ini | 1 + 5 files changed, 6 insertions(+), 20 deletions(-) diff --git a/minecraft/compat.py b/minecraft/compat.py index 9a06b6db..0614c52c 100644 --- a/minecraft/compat.py +++ b/minecraft/compat.py @@ -3,17 +3,4 @@ both Python2 and Python3 while using the same codebase. """ -# Raw input -> input shenangians -# example -# > from minecraft.compat import input -# > input("asd") - -# Hi, I'm pylint, and sometimes I act silly, at which point my programmer -# overlords need to correct me. - -# pylint: disable=undefined-variable,redefined-builtin,invalid-name -try: - input = raw_input -except NameError: - input = input -# pylint: enable=undefined-variable,redefined-builtin,invalid-name +# Currently empty, this is sure to grow in time. diff --git a/requirements.txt b/requirements.txt index d75395b4..470aeda6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ cryptography requests +six diff --git a/start.py b/start.py index 4cd6c1b0..4819a6af 100644 --- a/start.py +++ b/start.py @@ -6,7 +6,7 @@ from minecraft.exceptions import YggdrasilError from minecraft.networking.connection import Connection from minecraft.networking.packets import ChatMessagePacket, ChatPacket -from minecraft.compat import input +from six.moves import input def get_options(): diff --git a/tests/test_compat.py b/tests/test_compat.py index d114ee95..8b88060e 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -1,8 +1,5 @@ from minecraft import compat # noqa unused-import -import unittest +import unittest # noqa unused-import - -class TestCompatInput(unittest.TestCase): - def test_import_input(self): - from minecraft.compat import input # noqa unused-import +# Currently empty, eventually it should grow. diff --git a/tox.ini b/tox.ini index 4031a793..4d7fa84b 100644 --- a/tox.ini +++ b/tox.ini @@ -13,6 +13,7 @@ deps = nose requests cryptography + six [testenv:py27] deps = From cc184f3e4db6080efbedaa3fecbc9f496710d80c Mon Sep 17 00:00:00 2001 From: Jeppe Klitgaard Date: Thu, 16 Apr 2015 17:05:44 +0200 Subject: [PATCH 12/18] Added tests for LongLong and UnsignedLongLong. Implemented compatibility layer for the long type (has been removed in py3) Added test for long-type compat. --- minecraft/compat.py | 8 +++++++- minecraft/networking/datatypes.py | 3 ++- tests/test_compat.py | 8 +++++++- tests/test_datatypes.py | 34 +++++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/minecraft/compat.py b/minecraft/compat.py index 0614c52c..638f5277 100644 --- a/minecraft/compat.py +++ b/minecraft/compat.py @@ -3,4 +3,10 @@ both Python2 and Python3 while using the same codebase. """ -# Currently empty, this is sure to grow in time. +import six + +# ### LONG ### +if six.PY3: + long = int +else: + long = long diff --git a/minecraft/networking/datatypes.py b/minecraft/networking/datatypes.py index 9325c0c9..e87efeb0 100644 --- a/minecraft/networking/datatypes.py +++ b/minecraft/networking/datatypes.py @@ -19,6 +19,7 @@ "String"] from minecraft.exceptions import DeserializationError, SerializationError +from minecraft.compat import long from io import BytesIO import struct import collections @@ -161,7 +162,7 @@ class NumberDatatype(Datatype): MIN_NUMBER_VALUE = None MAX_NUMBER_VALUE = None - ALLOWED_SERIALIZATION_TYPES = (int,) + ALLOWED_SERIALIZATION_TYPES = (int, long) DISALLOWED_SERIALIZATION_TYPES = (bool,) @classmethod diff --git a/tests/test_compat.py b/tests/test_compat.py index 8b88060e..fc45894d 100644 --- a/tests/test_compat.py +++ b/tests/test_compat.py @@ -2,4 +2,10 @@ import unittest # noqa unused-import -# Currently empty, eventually it should grow. + +class TestCompatLong(unittest.TestCase): + def test_import_long(self): + from minecraft.compat import long # noqa unused-import + + def test_long(self): + compat.long(45) diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index 4fae167f..03f3d042 100644 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -248,6 +248,40 @@ class UnsignedLongTest(UnsignedInteger): DATATYPE_CLS = UnsignedLong +class LongLongTest(BaseNumberDatatypeTester): + DATATYPE_CLS = LongLong + + VALID_VALUES = [ + (-9223372036854775808, b"\x80\x00\x00\x00\x00\x00\x00\x00"), + (-1000000, b"\xff\xff\xff\xff\xff\xf0\xbd\xc0"), + (0, b"\x00\x00\x00\x00\x00\x00\x00\x00"), + (10000000, b"\x00\x00\x00\x00\x00\x98\x96\x80"), + (9223372036854775807, b"\x7f\xff\xff\xff\xff\xff\xff\xff") + ] + + INVALID_DESERIALIZATION_VALUES = list(BASE_INVALID_DESERIALIZATION_VALUES) + INVALID_DESERIALIZATION_VALUES.extend([ + (b"\xff", ValueError), + (b"\x00\x01", ValueError), + (b"\x76\x80\x80\x10\xff", ValueError), + (b"\x55\x44\x33\x22\x11\x66\x77\x88\x99", ValueError) + ]) + + +class UnsignedLongLongTest(BaseNumberDatatypeTester): + DATATYPE_CLS = UnsignedLongLong + + VALID_VALUES = [ + (0, b"\x00\x00\x00\x00\x00\x00\x00\x00"), + (10000000, b"\x00\x00\x00\x00\x00\x98\x96\x80"), + (9223372036854775807, b"\x7f\xff\xff\xff\xff\xff\xff\xff"), + (18446744073709551615, b"\xff\xff\xff\xff\xff\xff\xff\xff") + ] + + INVALID_DESERIALIZATION_VALUES = \ + LongLongTest.INVALID_DESERIALIZATION_VALUES + + # def _bin(binstr): # """ # Accepts a pretty looking string of binary numbers and From 20e7c70acfbfc8d36ad5aebfa6b8aa97829b54a2 Mon Sep 17 00:00:00 2001 From: Jeppe Klitgaard Date: Thu, 16 Apr 2015 18:49:23 +0200 Subject: [PATCH 13/18] Implemented tests for the Float datatype. This required the implementation of a new method on BaseDatatypeTester. dynamic_assert_equal is overridden by floating point datatypes, and instead uses TestCase.assertAlmostEqual. --- minecraft/networking/datatypes.py | 5 ++- tests/test_datatypes.py | 51 ++++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/minecraft/networking/datatypes.py b/minecraft/networking/datatypes.py index e87efeb0..e36db060 100644 --- a/minecraft/networking/datatypes.py +++ b/minecraft/networking/datatypes.py @@ -276,8 +276,11 @@ class Float(NumberDatatype): FORMAT = "f" SIZE = 4 + ALLOWED_SERIALIZATION_TYPES = (int, long, float) + DISALLOWED_SERIALIZATION_TYPES = (bool,) + -class Double(NumberDatatype): +class Double(Float): FORMAT = "d" SIZE = 8 diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index 03f3d042..5c2af308 100644 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -43,6 +43,13 @@ class BaseDatatypeTester(unittest.TestCase): # containing the value and the expected exception. INVALID_DESERIALIZATION_VALUES = [] + def dynamic_assert_equal(self, first, second): + """ + Overriden by floating point datatypes in order to handle + the floating point issue. + """ + return self.assertEqual(first, second) + def test_init(self): d = self.DATATYPE_CLS() # noqa @@ -53,13 +60,15 @@ def test_init_with_arg(self): def test_valid_data_serialization_values(self): for deserialized_val, serialized_val in self.VALID_VALUES: - self.assertEqual(self.DATATYPE_CLS.serialize(deserialized_val), - serialized_val) + self.dynamic_assert_equal( + self.DATATYPE_CLS.serialize(deserialized_val), + serialized_val) def test_valid_data_deserialization_values(self): for deserialized_val, serialized_val in self.VALID_VALUES: - self.assertEqual(self.DATATYPE_CLS.deserialize(serialized_val), - deserialized_val) + self.dynamic_assert_equal( + self.DATATYPE_CLS.deserialize(serialized_val), + deserialized_val) def test_invalid_data_serialization_values(self): for value, exception in self.INVALID_SERIALIZATION_VALUES: @@ -84,10 +93,14 @@ class BaseNumberDatatypeTester(BaseDatatypeTester): def base_number_invalid_data_serialization_values(self): values_to_test = BASE_INVALID_SERIALIZATION_VALUES - values_to_test.extend([ - (self.DATATYPE_CLS.MIN_NUMBER_VALUE - 1, ValueError), - (self.DATATYPE_CLS.MAX_NUMBER_VALUE + 1, ValueError) - ]) + + if (cls.MIN_NUMBER_VALUE is not None + and cls.MAX_NUMBER_VALUE is not None): + + values_to_test.extend([ + (self.DATATYPE_CLS.MIN_NUMBER_VALUE - 1, ValueError), + (self.DATATYPE_CLS.MAX_NUMBER_VALUE + 1, ValueError) + ]) for value, exception in values_to_test: with self.assertRaises(exception): @@ -282,6 +295,28 @@ class UnsignedLongLongTest(BaseNumberDatatypeTester): LongLongTest.INVALID_DESERIALIZATION_VALUES +class FloatTest(BaseNumberDatatypeTester): + DATATYPE_CLS = Float + + VALID_VALUES = [ + (-100.5467, b"\xc2\xc9\x17\xe9"), + (0.00000, b"\x00\x00\x00\x00"), + (5000.72, b"E\x9cE\xc3"), + (65.123565787856342347, b"B\x82?D"), + ] + + INVALID_DESERIALIZATION_VALUES = list(BASE_INVALID_DESERIALIZATION_VALUES) + INVALID_DESERIALIZATION_VALUES.extend([ + (b"\xff", ValueError), + (b"\x00\x01", ValueError), + (b"\x76\x80\x80\x10\xff", ValueError), + (b"\x55\x44\x33\x22\x11\x66\x77\x88\x99", ValueError) + ]) + + def dynamic_assert_equal(self, first, second): + return self.assertAlmostEqual(first, second, places=3) + + # def _bin(binstr): # """ # Accepts a pretty looking string of binary numbers and From 026a410208d83bbc0c75fe18883291cf1d947959 Mon Sep 17 00:00:00 2001 From: Jeppe Klitgaard Date: Thu, 16 Apr 2015 19:22:17 +0200 Subject: [PATCH 14/18] Added tests for Double datatype --- tests/test_datatypes.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index 5c2af308..b632c310 100644 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -317,6 +317,18 @@ def dynamic_assert_equal(self, first, second): return self.assertAlmostEqual(first, second, places=3) +class DoubleTest(FloatTest): + DATATYPE_CLS = Double + + VALID_VALUES = [ + (-10000560.86432, b"\xc1c\x13\x16\x1b\xa8\x82k"), + (-56.672345756870345754623, b"\xc0LV\x0fl\xfe\xaef"), + (0.00000, b"\x00\x00\x00\x00\x00\x00\x00\x00"), + (5000.72, b"@\xb3\x88\xb8Q\xeb\x85\x1f"), + (65.123565787856342347, b"@PG\xe8\x80zo\xd6"), + (5324342541.72123, b"A\xf3\xd5\xb0P\xdb\x8a(") + ] + # def _bin(binstr): # """ # Accepts a pretty looking string of binary numbers and From 69d7e74464a60c3dbd8683a16f4d09c5cdf20ce8 Mon Sep 17 00:00:00 2001 From: Jeppe Klitgaard Date: Thu, 16 Apr 2015 19:22:57 +0200 Subject: [PATCH 15/18] Removed useless commented code. --- tests/test_datatypes.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index b632c310..c5bdb1f8 100644 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -4,26 +4,6 @@ import unittest -# # Note, we use the actual classes as keys. -# # Format: DATATYPE_OBJ = (LIST_OF_VALID_VALUES, LIST_OF_INVALID_VALUES) -# TEST_DATA = { -# Boolean: [True, False], -# Byte: [-127, -25, 0, 125], -# UnsignedByte: [0, 125], -# Byte: [-22, 22], -# Short: [-340, 22, 350], -# UnsignedShort: [0, 400], -# Integer: [-1000, 1000], -# VarInt: [1, 250, 50000, 10000000], -# Long: [50000000], -# Float: [21.000301], -# Double: [36.004002], -# ShortPrefixedByteArray: [bytes(245)], -# VarIntPrefixedByteArray: [bytes(1234)], -# StringType: ["hello world"] -# } - - class BaseDatatypeTester(unittest.TestCase): DATATYPE_CLS = Datatype # We use Datatype as a an example here. @@ -344,9 +324,3 @@ class DoubleTest(FloatTest): # num = int("0b" + binstr, 2) # return num - - -# class VarIntTests(unittest.TestCase): -# def test1(self): -# self.assertEqual(VarInt.deserialize(_bin("0000 0001")), 1) -# self.assertEqual(VarInt.deserialize(_bin("1010 1100 0000 0010")), 300) From 868bd149f5754545281908023e7b7545707c8f4a Mon Sep 17 00:00:00 2001 From: Jeppe Klitgaard Date: Thu, 16 Apr 2015 19:24:32 +0200 Subject: [PATCH 16/18] PEP8 --- tests/test_datatypes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index c5bdb1f8..07dfce79 100644 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -1,5 +1,4 @@ -from minecraft.networking.datatypes import * -from minecraft.exceptions import DeserializationError +from minecraft.networking.datatypes import * # noqa undefined-names import unittest From e1f8f0254a78471e53f863daef88490bd9ec2473 Mon Sep 17 00:00:00 2001 From: Jeppe Klitgaard Date: Thu, 16 Apr 2015 20:22:49 +0200 Subject: [PATCH 17/18] Ignored python2-specific code on coverage report. --- minecraft/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minecraft/compat.py b/minecraft/compat.py index 638f5277..cf6c3d57 100644 --- a/minecraft/compat.py +++ b/minecraft/compat.py @@ -8,5 +8,5 @@ # ### LONG ### if six.PY3: long = int -else: +else: # pragma: no cover long = long From 55ff270f167d36cd67c637332d7db9ad1b5c68ce Mon Sep 17 00:00:00 2001 From: Jeppe Klitgaard Date: Thu, 16 Apr 2015 23:38:02 +0200 Subject: [PATCH 18/18] Tests for VarInt Removed unused import (SerializationError) We now check if Datatype.SIZE is a number using ABC. Added 2 decorators for raising serialized and deserialized data exceptions. Datatype.SIZE can now be either a number or a sequence. If sequence, first value is MIN_SIZE and second value is MAX_SIZE VarInt.serialize now raises ValueError instead of SerializationError when number is too big to serialize. --- minecraft/networking/datatypes.py | 69 ++++++++++++++++++++++++++----- tests/test_datatypes.py | 6 +++ 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/minecraft/networking/datatypes.py b/minecraft/networking/datatypes.py index e36db060..9410ca6a 100644 --- a/minecraft/networking/datatypes.py +++ b/minecraft/networking/datatypes.py @@ -18,19 +18,52 @@ "VarInt", "VarLong", "String"] -from minecraft.exceptions import DeserializationError, SerializationError +from minecraft.exceptions import DeserializationError from minecraft.compat import long from io import BytesIO import struct import collections +import numbers ENDIANNESS = "!" # Network, big-endian +def raise_serialization_data(func): + """ + A decorator to be used on a ``Datatype``.serialize definition. + + Must be placed before a classmethod decorator. + """ + def wrapped(cls, data): + cls.raise_serialization_data(data) + + return func(cls, data) + + return wrapped + + +def raise_deserialization_data(func): + """ + A decorator to be used on a ``Datatype``.serialize definition. + + Must be placed before a classmethod decorator. + """ + def wrapped(cls, data): + cls.raise_deserialization_data(data) + + return func(cls, data) + + return wrapped + + class Datatype(object): """ Base object for all `pyminecraft` networking datatypes. + ``Datatype``.SIZE can be either a number, specifying an exact required size + of data to be deserialized, or it can be a tuple like this: + ``(MIN_SIZE, MAX_SIZE)`` + .. note:: If ``ALLOWED_SERIALIZATION_TYPES`` is not empty, only the types found @@ -65,9 +98,8 @@ def read(cls, fileobject): return cls.deserialize(bin_data) @classmethod + @raise_deserialization_data def deserialize(cls, data): - cls.raise_deserialization_data(data) - deserialized_data = struct.unpack(ENDIANNESS + cls.FORMAT, data)[0] return deserialized_data @@ -76,9 +108,8 @@ def write(cls, fileobject, data): return fileobject.write(cls.serialize(data)) @classmethod + @raise_serialization_data def serialize(cls, data): - cls.raise_serialization_data(data) - serialized_data = struct.pack(ENDIANNESS + cls.FORMAT, data) return serialized_data @@ -137,11 +168,23 @@ def raise_deserialization_data(cls, data): raise TypeError(err) - if cls.SIZE != len(data): - err = "'data' must have a length of {}, not {}" - err = err.format(str(cls.SIZE), str(len(data))) + if isinstance(cls.SIZE, numbers.Number): + if cls.SIZE != len(data): + err = "'data' must have a length of {}, not {}" + err = err.format(str(cls.SIZE), str(len(data))) - raise ValueError(err) + raise ValueError(err) + + elif isinstance(cls.SIZE, collections.Sequence): + if not cls.SIZE[0] <= len(data) <= cls.SIZE[1]: + err = "'data' must have a length between {} and {}, not {}" + err = err.format(str(cls.SIZE[0]), str(cls.SIZE[1]), + str(len(data))) + + raise ValueError(err) + + else: + raise TypeError("'cls.SIZE' must be a number or a sequence.") return None @@ -304,6 +347,8 @@ class VarInt(NumberDatatype): # Largest element in SIZE_TABLE, assuming largest element is last. MAX_SIZE = list(SIZE_TABLE.items())[-1][-1] + SIZE = (1, MAX_SIZE) + @classmethod def read(cls, fileobject): number = 0 # The decoded number @@ -329,16 +374,18 @@ def read(cls, fileobject): return number @classmethod + @raise_deserialization_data def deserialize(cls, data): data_fileobject = BytesIO(bytes(data)) return cls.read(data_fileobject) @classmethod + @raise_serialization_data def serialize(cls, data): if data > cls.SIZE_TABLE[-1][0]: name_of_self = str(type(cls)) e = "Number too big to serialize as {}".format(name_of_self) - raise SerializationError(e) + raise ValueError(e) result = bytes() # Where we store the serialized number @@ -370,6 +417,8 @@ class VarLong(VarInt): MAX_SIZE = list(SIZE_TABLE.items())[-1][-1] + SIZE = (1, MAX_SIZE) + class String(Datatype): FORMAT = "utf-8" diff --git a/tests/test_datatypes.py b/tests/test_datatypes.py index 07dfce79..4a1f4909 100644 --- a/tests/test_datatypes.py +++ b/tests/test_datatypes.py @@ -308,6 +308,12 @@ class DoubleTest(FloatTest): (5324342541.72123, b"A\xf3\xd5\xb0P\xdb\x8a(") ] + +class VarIntTest(BaseNumberDatatypeTester): + DATATYPE_CLS = VarInt + + INVALID_DESERIALIZATION_VALUES = BASE_INVALID_DESERIALIZATION_VALUES + # def _bin(binstr): # """ # Accepts a pretty looking string of binary numbers and