From d44f84b02a06300be41c29da1e539cd0ebe4e6a8 Mon Sep 17 00:00:00 2001 From: Fede A Date: Thu, 9 Apr 2020 01:00:45 -0300 Subject: [PATCH 01/10] adds suport for SC_WRITE_METHOD --- javaobj/v2/api.py | 16 +++++++ javaobj/v2/beans.py | 21 ++++++++- javaobj/v2/core.py | 111 +++++++++++++++++++------------------------- 3 files changed, 85 insertions(+), 63 deletions(-) diff --git a/javaobj/v2/api.py b/javaobj/v2/api.py index b02bd74..c349711 100644 --- a/javaobj/v2/api.py +++ b/javaobj/v2/api.py @@ -78,3 +78,19 @@ def load_array(self, reader, field_type, size): :param size: Number of elements in the array """ return None + + def load_custom_writeObject(self, parser, reader, name): + """ + Reads content stored from a custom writeObject. + + This method is called only if the class description has both the + ``SC_SERIALIZABLE`` and ``SC_WRITE_METHOD`` flags set. + + The stream parsing will stop and fail if this method returns None. + + :param parser: The JavaStreamParser in use + :param reader: The data stream reader + :param name: The class description name + :return: An array with the parsed fields or None + """ + return None diff --git a/javaobj/v2/beans.py b/javaobj/v2/beans.py index 4db7d89..f33276a 100644 --- a/javaobj/v2/beans.py +++ b/javaobj/v2/beans.py @@ -60,6 +60,16 @@ class ContentType(IntEnum): BLOCKDATA = 6 EXCEPTIONSTATE = 7 +class ClassDataType(IntEnum): + """ + Class data types + """ + + NOWRCLASS = 0 + WRCLASS = 1 + EXTERNAL_CONTENTS = 2 + OBJECT_ANNOTATION = 3 + class ClassDescType(IntEnum): """ @@ -181,7 +191,6 @@ def __hash__(self): def __eq__(self, other): return self.value == other - class JavaField: """ Represents a field in a Java class description @@ -304,6 +313,16 @@ def fields_types(self): """ return [field.type for field in self.fields] + @property + def data_type(self): + if (ClassDescFlags.SC_SERIALIZABLE & self.desc_flags): + return ClassDataType.WRCLASS if (ClassDescFlags.SC_WRITE_METHOD & self.desc_flags) else ClassDataType.NOWRCLASS + elif (ClassDescFlags.SC_EXTERNALIZABLE & self.desc_flags): + return ClassDataType.OBJECT_ANNOTATION if (ClassDescFlags.SC_WRITE_METHOD & self.desc_flags) else ClassDataType.EXTERNAL_CONTENTS + + raise ValueError("Unhandled Class Data Type") + + def is_array_class(self): # type: () -> bool """ diff --git a/javaobj/v2/core.py b/javaobj/v2/core.py index aa6cd79..d3e55d2 100644 --- a/javaobj/v2/core.py +++ b/javaobj/v2/core.py @@ -48,6 +48,7 @@ ExceptionRead, ClassDescType, FieldType, + ClassDataType, ) from .stream import DataStreamReader from ..constants import ( @@ -57,6 +58,7 @@ TypeCode, PRIMITIVE_TYPES, ) + from ..modifiedutf8 import decode_modified_utf8 # ------------------------------------------------------------------------------ @@ -276,7 +278,7 @@ def _do_null(self, _): """ return None - def _read_content(self, type_code, block_data): + def _read_content(self, type_code, block_data, class_desc=None): # type: (int, bool) -> ParsedJavaContent """ Parses the next content @@ -290,6 +292,9 @@ def _read_content(self, type_code, block_data): try: handler = self.__type_code_handlers[type_code] except KeyError: + '''Looking for an external reader''' + if class_desc and class_desc.data_type == ClassDataType.WRCLASS: + return self._custom_readObject(class_desc.name) raise ValueError("Unknown type code: 0x{0:x}".format(type_code)) else: try: @@ -297,7 +302,7 @@ def _read_content(self, type_code, block_data): except ExceptionRead as ex: return ex.exception_object - def _read_new_string(self, type_code): + def _read_new_string(self, type_code, field_name=None): # type: (int) -> JavaString """ Reads a Java String @@ -321,7 +326,7 @@ def _read_new_string(self, type_code): raise ValueError("Invalid string length: {0}".format(length)) elif length < 65536: self._log.warning("Small string stored as a long one") - + # Parse the content data = self.__fd.read(length) java_str = JavaString(handle, data) @@ -338,12 +343,10 @@ def _read_classdesc(self): type_code = self.__reader.read_byte() return self._do_classdesc(type_code) - def _do_classdesc(self, type_code, must_be_new=False): + def _do_classdesc(self, type_code): # type: (int, bool) -> JavaClassDesc """ Parses a class description - - :param must_be_new: Check if the class description is really a new one """ if type_code == TerminalCode.TC_CLASSDESC: # Do the real job @@ -352,32 +355,29 @@ def _do_classdesc(self, type_code, must_be_new=False): handle = self._new_handle() desc_flags = self.__reader.read_byte() nb_fields = self.__reader.read_short() + if nb_fields < 0: raise ValueError("Invalid field count: {0}".format(nb_fields)) fields = [] # type: List[JavaField] for _ in range(nb_fields): field_type = self.__reader.read_byte() - if field_type in PRIMITIVE_TYPES: - # Primitive type - field_name = self.__reader.read_UTF() - fields.append(JavaField(FieldType(field_type), field_name)) - elif field_type in (TypeCode.TYPE_OBJECT, TypeCode.TYPE_ARRAY,): - # Array or object type - field_name = self.__reader.read_UTF() + field_name = self.__reader.read_UTF() + class_name = None + + if field_type in (TypeCode.TYPE_OBJECT, TypeCode.TYPE_ARRAY): # String type code str_type_code = self.__reader.read_byte() class_name = self._read_new_string(str_type_code) - fields.append( - JavaField( - FieldType(field_type), field_name, class_name, - ), - ) - else: + elif field_type not in PRIMITIVE_TYPES: raise ValueError( "Invalid field type char: 0x{0:x}".format(field_type) ) + fields.append(JavaField( + FieldType(field_type), field_name, class_name + )) + # Setup the class description bean class_desc = JavaClassDesc(ClassDescType.NORMALCLASS) class_desc.name = name @@ -385,7 +385,7 @@ def _do_classdesc(self, type_code, must_be_new=False): class_desc.handle = handle class_desc.desc_flags = desc_flags class_desc.fields = fields - class_desc.annotations = self._read_class_annotations() + class_desc.annotations = self._read_class_annotations(class_desc) class_desc.super_class = self._read_classdesc() # Store the reference to the parsed bean @@ -393,16 +393,9 @@ def _do_classdesc(self, type_code, must_be_new=False): return class_desc elif type_code == TerminalCode.TC_NULL: # Null reference - if must_be_new: - raise ValueError("Got Null instead of a new class description") return None elif type_code == TerminalCode.TC_REFERENCE: # Reference to an already loading class description - if must_be_new: - raise ValueError( - "Got a reference instead of a new class description" - ) - previous = self._do_reference() if not isinstance(previous, JavaClassDesc): raise ValueError("Referenced object is not a class description") @@ -424,10 +417,20 @@ def _do_classdesc(self, type_code, must_be_new=False): # Store the reference to the parsed bean self._set_handle(handle, class_desc) return class_desc - + raise ValueError("Expected a valid class description starter") - def _read_class_annotations(self): + + def _custom_readObject(self, class_name): + self.__fd.seek(-1, os.SEEK_CUR) + for transformer in self.__transformers: + class_data = transformer.load_custom_writeObject(self, self.__reader, class_name) + if class_data: + return class_data + raise ValueError("Custom readObject can not be processed") + + + def _read_class_annotations(self, class_desc=None): # type: () -> List[ParsedJavaContent] """ Reads the annotations associated to a class @@ -442,8 +445,8 @@ def _read_class_annotations(self): # Reset references self._reset() continue + java_object = self._read_content(type_code, True, class_desc) - java_object = self._read_content(type_code, True) if java_object is not None and java_object.is_exception: raise ExceptionRead(java_object) @@ -503,31 +506,16 @@ def _read_class_data(self, instance): for cd in classes: values = {} # type: Dict[JavaField, Any] - if cd.desc_flags & ClassDescFlags.SC_SERIALIZABLE: - if cd.desc_flags & ClassDescFlags.SC_EXTERNALIZABLE: - raise ValueError( - "SC_EXTERNALIZABLE & SC_SERIALIZABLE encountered" - ) - - for field in cd.fields: - values[field] = self._read_field_value(field.type) - - all_data[cd] = values - - if cd.desc_flags & ClassDescFlags.SC_WRITE_METHOD: - if cd.desc_flags & ClassDescFlags.SC_ENUM: - raise ValueError( - "SC_ENUM & SC_WRITE_METHOD encountered!" - ) - - annotations[cd] = self._read_class_annotations() - elif cd.desc_flags & ClassDescFlags.SC_EXTERNALIZABLE: - if cd.desc_flags & ClassDescFlags.SC_SERIALIZABLE: - raise ValueError( - "SC_EXTERNALIZABLE & SC_SERIALIZABLE encountered" - ) - - if cd.desc_flags & ClassDescFlags.SC_BLOCK_DATA: + cd.validate() + if cd.data_type == ClassDataType.NOWRCLASS or cd.data_type == ClassDataType.WRCLASS: + if cd.data_type == ClassDataType.NOWRCLASS: + for field in cd.fields: + values[field] = self._read_field_value(field.type) + all_data[cd] = values + else: + annotations[cd] = self._read_class_annotations(cd) + else: + if cd.data_type == ClassDataType.OBJECT_ANNOTATION: # Call the transformer if possible if not instance.load_from_blockdata(self, self.__reader): # Can't read :/ @@ -535,8 +523,7 @@ def _read_class_data(self, instance): "hit externalizable with nonzero SC_BLOCK_DATA; " "can't interpret data" ) - - annotations[cd] = self._read_class_annotations() + annotations[cd] = self._read_class_annotations(cd) # Fill the instance object instance.annotations = annotations @@ -568,11 +555,11 @@ def _read_field_value(self, field_type): return self.__reader.read_bool() elif field_type in (FieldType.OBJECT, FieldType.ARRAY): sub_type_code = self.__reader.read_byte() - if ( - field_type == FieldType.ARRAY - and sub_type_code != TerminalCode.TC_ARRAY - ): - raise ValueError("Array type listed, but type code != TC_ARRAY") + if field_type == FieldType.ARRAY: + if sub_type_code == TerminalCode.TC_REFERENCE: + return self._do_classdesc(sub_type_code) + elif sub_type_code != TerminalCode.TC_ARRAY: + raise ValueError("Array type listed, but type code != TC_ARRAY") content = self._read_content(sub_type_code, False) if content is not None and content.is_exception: From 3d0a1a45f4d524e45ba479d4f2cbfbd961f9bd6a Mon Sep 17 00:00:00 2001 From: Fede A Date: Thu, 9 Apr 2020 01:07:37 -0300 Subject: [PATCH 02/10] removes unused parameter --- javaobj/v2/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javaobj/v2/core.py b/javaobj/v2/core.py index d3e55d2..9af8b48 100644 --- a/javaobj/v2/core.py +++ b/javaobj/v2/core.py @@ -302,7 +302,7 @@ def _read_content(self, type_code, block_data, class_desc=None): except ExceptionRead as ex: return ex.exception_object - def _read_new_string(self, type_code, field_name=None): + def _read_new_string(self, type_code): # type: (int) -> JavaString """ Reads a Java String From 574b076d8c533f454b0f6ab38e386a8ee009180e Mon Sep 17 00:00:00 2001 From: Fede A Date: Sun, 12 Apr 2020 23:20:57 -0300 Subject: [PATCH 03/10] fixes _read_class_data --- javaobj/v2/beans.py | 4 ++++ javaobj/v2/core.py | 15 ++++++++++++--- javaobj/v2/transformers.py | 4 +--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/javaobj/v2/beans.py b/javaobj/v2/beans.py index f33276a..ee7ea07 100644 --- a/javaobj/v2/beans.py +++ b/javaobj/v2/beans.py @@ -254,6 +254,9 @@ def __init__(self, class_desc_type): # The super class of this one, if any self.super_class = None # type: Optional[JavaClassDesc] + # Indicates if it is a super class + self.is_super_class = False + # List of the interfaces of the class self.interfaces = [] # type: List[str] @@ -387,6 +390,7 @@ def __init__(self): self.annotations = ( {} ) # type: Dict[JavaClassDesc, List[ParsedJavaContent]] + self.is_external_instance = False def __str__(self): return "[instance 0x{0:x}: type {1}]".format( diff --git a/javaobj/v2/core.py b/javaobj/v2/core.py index 9af8b48..d398c0e 100644 --- a/javaobj/v2/core.py +++ b/javaobj/v2/core.py @@ -51,6 +51,7 @@ ClassDataType, ) from .stream import DataStreamReader +from .transformers import DefaultObjectTransformer from ..constants import ( ClassDescFlags, StreamConstants, @@ -492,6 +493,10 @@ def _do_object(self, type_code=0): self._log.debug("Done reading object handle %x", handle) return instance + def _is_default_supported(self, class_name): + default_transf = [x for x in self.__transformers if isinstance(x, DefaultObjectTransformer)] + return len(default_transf) and class_name in default_transf[0]._type_mapper + def _read_class_data(self, instance): # type: (JavaInstance) -> None """ @@ -508,12 +513,16 @@ def _read_class_data(self, instance): values = {} # type: Dict[JavaField, Any] cd.validate() if cd.data_type == ClassDataType.NOWRCLASS or cd.data_type == ClassDataType.WRCLASS: - if cd.data_type == ClassDataType.NOWRCLASS: + read_custom_data = cd.data_type == ClassDataType.WRCLASS and cd.is_super_class and not self._is_default_supported(cd.name) + if read_custom_data or cd.data_type == ClassDataType.WRCLASS and instance.is_external_instance: + annotations[cd] = self._read_class_annotations(cd) + else: for field in cd.fields: values[field] = self._read_field_value(field.type) all_data[cd] = values - else: - annotations[cd] = self._read_class_annotations(cd) + + if cd.data_type == ClassDataType.WRCLASS: + annotations[cd] = self._read_class_annotations(cd) else: if cd.data_type == ClassDataType.OBJECT_ANNOTATION: # Call the transformer if possible diff --git a/javaobj/v2/transformers.py b/javaobj/v2/transformers.py index fa99186..c9da287 100644 --- a/javaobj/v2/transformers.py +++ b/javaobj/v2/transformers.py @@ -37,9 +37,7 @@ # Javaobj from .api import ObjectTransformer -from .beans import JavaClassDesc, JavaInstance -from .core import JavaStreamParser -from .stream import DataStreamReader +from .beans import JavaInstance from ..constants import TerminalCode, TypeCode from ..utils import to_bytes, log_error, log_debug, read_struct, read_string From fd30e4e2cb43174a87542c283a064eb266371e7c Mon Sep 17 00:00:00 2001 From: Fede A Date: Wed, 15 Apr 2020 09:55:15 -0300 Subject: [PATCH 04/10] adds tests for custom writeObject --- AUTHORS | 1 + javaobj/v2/core.py | 25 +++-- tests/java/src/test/java/OneTest.java | 146 +++++++++----------------- tests/tests_v2.py | 138 ++++++++++++++++++++++++ 4 files changed, 204 insertions(+), 106 deletions(-) diff --git a/AUTHORS b/AUTHORS index 4408af6..e92f3ad 100644 --- a/AUTHORS +++ b/AUTHORS @@ -11,3 +11,4 @@ Many thanks to the contributors: * Jason Spencer, Google LLC (@j8spencer) * @guywithface * Chris van Marle (@qistoph) +* Federico Alves (@UruDev) \ No newline at end of file diff --git a/javaobj/v2/core.py b/javaobj/v2/core.py index 5d1de3b..5b126b6 100644 --- a/javaobj/v2/core.py +++ b/javaobj/v2/core.py @@ -333,7 +333,7 @@ def _read_new_string(self, type_code): raise ValueError("Invalid string length: {0}".format(length)) elif length < 65536: self._log.warning("Small string stored as a long one") - + # Parse the content data = self.__fd.read(length) java_str = JavaString(handle, data) @@ -394,6 +394,9 @@ def _do_classdesc(self, type_code): class_desc.annotations = self._read_class_annotations(class_desc) class_desc.super_class = self._read_classdesc() + if class_desc.super_class: + class_desc.super_class.is_super_class = True + # Store the reference to the parsed bean self._set_handle(handle, class_desc) return class_desc @@ -404,7 +407,8 @@ def _do_classdesc(self, type_code): # Reference to an already loading class description previous = self._do_reference() if not isinstance(previous, JavaClassDesc): - raise ValueError("Referenced object is not a class description") + raise ValueError( + "Referenced object is not a class description") return previous elif type_code == TerminalCode.TC_PROXYCLASSDESC: # Proxy class description @@ -420,10 +424,13 @@ def _do_classdesc(self, type_code): class_desc.annotations = self._read_class_annotations() class_desc.super_class = self._read_classdesc() + if class_desc.super_class: + class_desc.super_class.is_super_class = True + # Store the reference to the parsed bean self._set_handle(handle, class_desc) return class_desc - + raise ValueError("Expected a valid class description starter") def _custom_readObject(self, class_name): @@ -479,6 +486,9 @@ def _create_instance(self, class_desc): for transformer in self.__transformers: instance = transformer.create_instance(class_desc) if instance is not None: + if class_desc.name: + instance.is_external_instance = not self._is_default_supported( + class_desc.name) return instance return JavaInstance() @@ -544,15 +554,8 @@ def _read_class_data(self, instance): cd.data_type == ClassDataType.NOWRCLASS or cd.data_type == ClassDataType.WRCLASS ): - read_custom_data = ( - cd.data_type == ClassDataType.WRCLASS - and cd.is_super_class - and not self._is_default_supported(cd.name) - ) - if ( - read_custom_data - or cd.data_type == ClassDataType.WRCLASS + cd.data_type == ClassDataType.WRCLASS and instance.is_external_instance ): annotations[cd] = self._read_class_annotations(cd) diff --git a/tests/java/src/test/java/OneTest.java b/tests/java/src/test/java/OneTest.java index d17cdde..643f51a 100644 --- a/tests/java/src/test/java/OneTest.java +++ b/tests/java/src/test/java/OneTest.java @@ -21,6 +21,7 @@ import java.util.Set; import java.util.TreeSet; import java.util.Vector; +import java.util.Random; import javax.swing.JScrollPane; import javax.swing.SwingUtilities; @@ -326,7 +327,7 @@ public void testTime() throws Exception { ZonedDateTime.now(), }); oos.flush(); - } + } /** * Tests th pull request #27 by @qistoph: @@ -388,115 +389,70 @@ public void windowClosing(final WindowEvent e) { }); } - // public void test_readObject() throws Exception { - // String s = "HelloWorld"; - // oos.writeObject(s); - // oos.close(); - // ois = new ObjectInputStream(new ByteArrayInputStream(bao.toByteArray())); - // assertEquals("Read incorrect Object value", s, ois.readObject()); - // ois.close(); - // - // // Regression for HARMONY-91 - // // dynamically create serialization byte array for the next hierarchy: - // // - class A implements Serializable - // // - class C extends A - // - // byte[] cName = C.class.getName().getBytes("UTF-8"); - // byte[] aName = A.class.getName().getBytes("UTF-8"); - // - // ByteArrayOutputStream out = new ByteArrayOutputStream(); - // - // byte[] begStream = new byte[] { (byte) 0xac, (byte) 0xed, // STREAM_MAGIC - // (byte) 0x00, (byte) 0x05, // STREAM_VERSION - // (byte) 0x73, // TC_OBJECT - // (byte) 0x72, // TC_CLASSDESC - // (byte) 0x00, // only first byte for C class name length - // }; - // - // out.write(begStream, 0, begStream.length); - // out.write(cName.length); // second byte for C class name length - // out.write(cName, 0, cName.length); // C class name - // - // byte[] midStream = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, - // (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - // (byte) 0x21, // serialVersionUID = 33L - // (byte) 0x02, // flags - // (byte) 0x00, (byte) 0x00, // fields : none - // (byte) 0x78, // TC_ENDBLOCKDATA - // (byte) 0x72, // Super class for C: TC_CLASSDESC for A class - // (byte) 0x00, // only first byte for A class name length - // }; - // - // out.write(midStream, 0, midStream.length); - // out.write(aName.length); // second byte for A class name length - // out.write(aName, 0, aName.length); // A class name - // - // byte[] endStream = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, - // (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, - // (byte) 0x0b, // serialVersionUID = 11L - // (byte) 0x02, // flags - // (byte) 0x00, (byte) 0x01, // fields - // - // (byte) 0x4c, // field description: type L (object) - // (byte) 0x00, (byte) 0x04, // length - // // field = 'name' - // (byte) 0x6e, (byte) 0x61, (byte) 0x6d, (byte) 0x65, - // - // (byte) 0x74, // className1: TC_STRING - // (byte) 0x00, (byte) 0x12, // length - // // - // (byte) 0x4c, (byte) 0x6a, (byte) 0x61, (byte) 0x76, - // (byte) 0x61, (byte) 0x2f, (byte) 0x6c, (byte) 0x61, - // (byte) 0x6e, (byte) 0x67, (byte) 0x2f, (byte) 0x53, - // (byte) 0x74, (byte) 0x72, (byte) 0x69, (byte) 0x6e, - // (byte) 0x67, (byte) 0x3b, - // - // (byte) 0x78, // TC_ENDBLOCKDATA - // (byte) 0x70, // NULL super class for A class - // - // // classdata - // (byte) 0x74, // TC_STRING - // (byte) 0x00, (byte) 0x04, // length - // (byte) 0x6e, (byte) 0x61, (byte) 0x6d, (byte) 0x65, // value - // }; - // - // out.write(endStream, 0, endStream.length); - // out.flush(); - // - // // read created serial. form - // ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream( - // out.toByteArray())); - // Object o = ois.readObject(); - // assertEquals(C.class, o.getClass()); - // - // // Regression for HARMONY-846 - // assertNull(new ObjectInputStream() {}.readObject()); - // } - + + /** + * Tests the pull request #38 by @UruDev: + * Add support for custom writeObject + */ + @Test + public void testCustomWriteObject() throws Exception { + CustomClass writer = new CustomClass(); + writer.start(oos); + } } class SuperAaaa implements Serializable { - - /** - * - */ private static final long serialVersionUID = 1L; public boolean bool = true; public int integer = -1; public String superString = "Super!!"; - } class TestConcrete extends SuperAaaa implements Serializable { - - /** - * - */ private static final long serialVersionUID = 1L; public String childString = "Child!!"; TestConcrete() { super(); } +} + +//Custom writeObject section +class CustomClass implements Serializable { + private static final long serialVersionUID = 1; + + public void start(ObjectOutputStream out) throws Exception { + this.writeObject(out); + } + + private void writeObject(ObjectOutputStream out) throws IOException { + CustomWriter custom = new CustomWriter(42); + out.writeObject(custom); + out.flush(); + } +} + +class RandomChild extends Random { + private static final long serialVersionUID = 1; + private int num = 1; + private double doub = 4.5; + RandomChild(int seed) { + super(seed); + } +} + +class CustomWriter implements Serializable { + protected RandomChild custom_obj = null; + + CustomWriter(int seed) { + custom_obj = new RandomChild(seed); + } + + private static final long serialVersionUID = 1; + private static final int CURRENT_SERIAL_VERSION = 0; + private void writeObject(ObjectOutputStream out) throws IOException { + out.writeInt(CURRENT_SERIAL_VERSION); + out.writeObject(custom_obj); + } } diff --git a/tests/tests_v2.py b/tests/tests_v2.py index 5daeddd..7f671b0 100644 --- a/tests/tests_v2.py +++ b/tests/tests_v2.py @@ -37,6 +37,9 @@ import subprocess import sys import unittest +import struct + +from io import BytesIO # Prepare Python path to import javaobj sys.path.insert(0, os.path.abspath(os.path.dirname(os.getcwd()))) @@ -54,6 +57,108 @@ # ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + +# Custom writeObject parsing classes +class CustomWriterInstance(javaobj.beans.JavaInstance): + def __init__(self): + javaobj.beans.JavaInstance.__init__(self) + + def load_from_instance(self): + """ + Updates the content of this instance + from its parsed fields and annotations + :return: True on success, False on error + """ + if self.classdesc and self.classdesc in self.annotations: + fields = ['int_not_in_fields'] + self.classdesc.fields_names + raw_data = self.annotations[self.classdesc] + int_not_in_fields = struct.unpack('>i', BytesIO(raw_data[0].data).read(4))[0] + custom_obj = raw_data[1] + values = [int_not_in_fields, custom_obj] + self.field_data = dict(zip(fields, values)) + return True + + return False + +class RandomChildInstance(javaobj.beans.JavaInstance): + def load_from_instance(self): + """ + Updates the content of this instance + from its parsed fields and annotations + :return: True on success, False on error + """ + if self.classdesc and self.classdesc in self.field_data: + fields = self.classdesc.fields_names + values = self.field_data[self.classdesc].values() + self.field_data = dict(zip(fields, values)) + if self.classdesc.super_class and self.classdesc.super_class in self.annotations: + super_class = self.annotations[self.classdesc.super_class][0] + self.annotations = dict( + zip(super_class.fields_names, super_class.field_data)) + return True + + return False + +class BaseTransformer(javaobj.transformers.ObjectTransformer): + """ + Creates a JavaInstance object with custom loading methods for the + classes it can handle + """ + def __init__(self, handled_classes={}): + self.instance = None + self.HANDLED_CLASSES = handled_classes + + def create_instance(self, classdesc): + """ + Transforms a parsed Java object into a Python object + + :param classdesc: The description of a Java class + :return: The Python form of the object, or the original JavaObject + """ + if classdesc.name in self.HANDLED_CLASSES: + self.instance = self.HANDLED_CLASSES[classdesc.name]() + return self.instance + + return None + +class RandomChildTransformer(BaseTransformer): + def __init__(self): + super().__init__({'RandomChild': RandomChildInstance}) + +class CustomWriterTransformer(BaseTransformer): + def __init__(self): + super().__init__({'CustomWriter': CustomWriterInstance}) + +class JavaRandomTransformer(BaseTransformer): + def __init__(self): + super().__init__() + self.name = "java.util.Random" + self.fields = { + 'haveNextNextGaussian': javaobj.beans.FieldType.BOOLEAN, + 'nextNextGaussian': javaobj.beans.FieldType.DOUBLE, + 'seed': javaobj.beans.FieldType.LONG + } + + def load_custom_writeObject(self, parser, reader, name): + if name == self.name: + fields = [] + values = [] + for field_name, field_type in self.fields.items(): + values.append(parser._read_field_value(field_type)) + fields.append(javaobj.beans.JavaField(field_type, field_name)) + + class_desc = javaobj.beans.JavaClassDesc( + javaobj.beans.ClassDescType.NORMALCLASS) + class_desc.name = self.name + class_desc.desc_flags = javaobj.beans.ClassDataType.EXTERNAL_CONTENTS + class_desc.fields = fields + class_desc.field_data = values + return class_desc + return None + +# ------------------------------------------------------------------------------ + class TestJavaobjV2(unittest.TestCase): """ @@ -425,6 +530,39 @@ def test_qistoph_pr_27(self): for key, value in pobj.items(): self.assertEqual(parent_map[key], value) + def test_writeObject(self): + """ + Tests support for custom writeObject (PR #38) + """ + + ser = self.read_file("testCustomWriteObject.ser") + transformers = [CustomWriterTransformer(), RandomChildTransformer(), JavaRandomTransformer()] + pobj = javaobj.loads(ser, *transformers) + + self.assertEqual(isinstance(pobj, CustomWriterInstance), True) + self.assertEqual(isinstance(pobj.field_data['custom_obj'], RandomChildInstance), True) + + parent_data = pobj.field_data + child_data = parent_data['custom_obj'].field_data + super_data = parent_data['custom_obj'].annotations + expected = { + 'int_not_in_fields': 0, + 'custom_obj': { + 'field_data': { + 'doub': 4.5, + 'num': 1 + } , + 'annotations': { + 'haveNextNextGaussian': False, + 'nextNextGaussian': 0.0, + 'seed': 25214903879 + } + } + } + + self.assertEqual(expected['int_not_in_fields'], parent_data['int_not_in_fields']) + self.assertEqual(expected['custom_obj']['field_data'], child_data) + self.assertEqual(expected['custom_obj']['annotations'], super_data) # ------------------------------------------------------------------------------ From e7a231c930a8225a1e21a634a6bf5ca23fdd8a08 Mon Sep 17 00:00:00 2001 From: Fede A Date: Wed, 15 Apr 2020 21:12:00 -0300 Subject: [PATCH 05/10] switch dict for OrderedDict for backward compatibility --- tests/tests_v2.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/tests_v2.py b/tests/tests_v2.py index 7f671b0..13254ae 100644 --- a/tests/tests_v2.py +++ b/tests/tests_v2.py @@ -39,6 +39,7 @@ import unittest import struct +from collections import OrderedDict from io import BytesIO # Prepare Python path to import javaobj @@ -76,7 +77,7 @@ def load_from_instance(self): int_not_in_fields = struct.unpack('>i', BytesIO(raw_data[0].data).read(4))[0] custom_obj = raw_data[1] values = [int_not_in_fields, custom_obj] - self.field_data = dict(zip(fields, values)) + self.field_data = OrderedDict(zip(fields, values)) return True return False @@ -91,10 +92,10 @@ def load_from_instance(self): if self.classdesc and self.classdesc in self.field_data: fields = self.classdesc.fields_names values = self.field_data[self.classdesc].values() - self.field_data = dict(zip(fields, values)) + self.field_data = OrderedDict(zip(fields, values)) if self.classdesc.super_class and self.classdesc.super_class in self.annotations: super_class = self.annotations[self.classdesc.super_class][0] - self.annotations = dict( + self.annotations = OrderedDict( zip(super_class.fields_names, super_class.field_data)) return True @@ -545,7 +546,7 @@ def test_writeObject(self): parent_data = pobj.field_data child_data = parent_data['custom_obj'].field_data super_data = parent_data['custom_obj'].annotations - expected = { + expected = OrderedDict({ 'int_not_in_fields': 0, 'custom_obj': { 'field_data': { @@ -558,7 +559,7 @@ def test_writeObject(self): 'seed': 25214903879 } } - } + }) self.assertEqual(expected['int_not_in_fields'], parent_data['int_not_in_fields']) self.assertEqual(expected['custom_obj']['field_data'], child_data) From 9dab084c0bfc013e8625d57892ef2ae1786bd3c5 Mon Sep 17 00:00:00 2001 From: Fede A Date: Wed, 15 Apr 2020 21:41:51 -0300 Subject: [PATCH 06/10] revert OrderedDict and instead use dict names --- tests/tests_v2.py | 53 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/tests/tests_v2.py b/tests/tests_v2.py index 13254ae..f27eaf8 100644 --- a/tests/tests_v2.py +++ b/tests/tests_v2.py @@ -32,6 +32,8 @@ from __future__ import print_function # Standard library +from javaobj.utils import bytes_char +import javaobj.v2 as javaobj import logging import os import subprocess @@ -39,15 +41,12 @@ import unittest import struct -from collections import OrderedDict from io import BytesIO # Prepare Python path to import javaobj sys.path.insert(0, os.path.abspath(os.path.dirname(os.getcwd()))) # Local -import javaobj.v2 as javaobj -from javaobj.utils import bytes_char # ------------------------------------------------------------------------------ @@ -61,6 +60,8 @@ # ------------------------------------------------------------------------------ # Custom writeObject parsing classes + + class CustomWriterInstance(javaobj.beans.JavaInstance): def __init__(self): javaobj.beans.JavaInstance.__init__(self) @@ -74,14 +75,16 @@ def load_from_instance(self): if self.classdesc and self.classdesc in self.annotations: fields = ['int_not_in_fields'] + self.classdesc.fields_names raw_data = self.annotations[self.classdesc] - int_not_in_fields = struct.unpack('>i', BytesIO(raw_data[0].data).read(4))[0] + int_not_in_fields = struct.unpack( + '>i', BytesIO(raw_data[0].data).read(4))[0] custom_obj = raw_data[1] values = [int_not_in_fields, custom_obj] - self.field_data = OrderedDict(zip(fields, values)) + self.field_data = dict(zip(fields, values)) return True - + return False + class RandomChildInstance(javaobj.beans.JavaInstance): def load_from_instance(self): """ @@ -92,20 +95,22 @@ def load_from_instance(self): if self.classdesc and self.classdesc in self.field_data: fields = self.classdesc.fields_names values = self.field_data[self.classdesc].values() - self.field_data = OrderedDict(zip(fields, values)) + self.field_data = dict(zip(fields, values)) if self.classdesc.super_class and self.classdesc.super_class in self.annotations: super_class = self.annotations[self.classdesc.super_class][0] - self.annotations = OrderedDict( + self.annotations = dict( zip(super_class.fields_names, super_class.field_data)) return True return False + class BaseTransformer(javaobj.transformers.ObjectTransformer): """ Creates a JavaInstance object with custom loading methods for the classes it can handle """ + def __init__(self, handled_classes={}): self.instance = None self.HANDLED_CLASSES = handled_classes @@ -123,14 +128,17 @@ def create_instance(self, classdesc): return None + class RandomChildTransformer(BaseTransformer): def __init__(self): super().__init__({'RandomChild': RandomChildInstance}) + class CustomWriterTransformer(BaseTransformer): def __init__(self): super().__init__({'CustomWriter': CustomWriterInstance}) + class JavaRandomTransformer(BaseTransformer): def __init__(self): super().__init__() @@ -537,33 +545,44 @@ def test_writeObject(self): """ ser = self.read_file("testCustomWriteObject.ser") - transformers = [CustomWriterTransformer(), RandomChildTransformer(), JavaRandomTransformer()] + transformers = [CustomWriterTransformer( + ), RandomChildTransformer(), JavaRandomTransformer()] pobj = javaobj.loads(ser, *transformers) self.assertEqual(isinstance(pobj, CustomWriterInstance), True) - self.assertEqual(isinstance(pobj.field_data['custom_obj'], RandomChildInstance), True) - + self.assertEqual(isinstance( + pobj.field_data['custom_obj'], RandomChildInstance), True) + parent_data = pobj.field_data child_data = parent_data['custom_obj'].field_data super_data = parent_data['custom_obj'].annotations - expected = OrderedDict({ + expected = { 'int_not_in_fields': 0, 'custom_obj': { 'field_data': { 'doub': 4.5, 'num': 1 - } , + }, 'annotations': { 'haveNextNextGaussian': False, 'nextNextGaussian': 0.0, 'seed': 25214903879 } } - }) + } - self.assertEqual(expected['int_not_in_fields'], parent_data['int_not_in_fields']) - self.assertEqual(expected['custom_obj']['field_data'], child_data) - self.assertEqual(expected['custom_obj']['annotations'], super_data) + self.assertEqual(expected['int_not_in_fields'], + parent_data['int_not_in_fields']) + self.assertEqual(expected['custom_obj'] + ['field_data']['doub'], child_data['doub']) + self.assertEqual(expected['custom_obj'] + ['field_data']['num'], child_data['num']) + self.assertEqual(expected['custom_obj']['annotations'] + ['haveNextNextGaussian'], super_data['haveNextNextGaussian']) + self.assertEqual(expected['custom_obj']['annotations'] + ['nextNextGaussian'], super_data['nextNextGaussian']) + self.assertEqual(expected['custom_obj'] + ['annotations']['seed'], super_data['seed']) # ------------------------------------------------------------------------------ From c74e6121a018208b166de3ba002595bf61370068 Mon Sep 17 00:00:00 2001 From: Fede A Date: Wed, 15 Apr 2020 22:21:01 -0300 Subject: [PATCH 07/10] custom dict construction --- tests/tests_v2.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/tests_v2.py b/tests/tests_v2.py index f27eaf8..77f3631 100644 --- a/tests/tests_v2.py +++ b/tests/tests_v2.py @@ -61,6 +61,11 @@ # Custom writeObject parsing classes +def assign_dict(fields, values): + ret = dict() + for i, v in enumerate(fields): + ret[v] = values[i] + return ret class CustomWriterInstance(javaobj.beans.JavaInstance): def __init__(self): @@ -79,7 +84,7 @@ def load_from_instance(self): '>i', BytesIO(raw_data[0].data).read(4))[0] custom_obj = raw_data[1] values = [int_not_in_fields, custom_obj] - self.field_data = dict(zip(fields, values)) + self.field_data = assign_dict(fields, values) return True return False @@ -95,11 +100,10 @@ def load_from_instance(self): if self.classdesc and self.classdesc in self.field_data: fields = self.classdesc.fields_names values = self.field_data[self.classdesc].values() - self.field_data = dict(zip(fields, values)) + self.field_data = assign_dict(fields, list(values)) if self.classdesc.super_class and self.classdesc.super_class in self.annotations: super_class = self.annotations[self.classdesc.super_class][0] - self.annotations = dict( - zip(super_class.fields_names, super_class.field_data)) + self.annotations = assign_dict(super_class.fields_names, super_class.field_data) return True return False @@ -131,17 +135,17 @@ def create_instance(self, classdesc): class RandomChildTransformer(BaseTransformer): def __init__(self): - super().__init__({'RandomChild': RandomChildInstance}) + super(RandomChildTransformer, self).__init__({'RandomChild': RandomChildInstance}) class CustomWriterTransformer(BaseTransformer): def __init__(self): - super().__init__({'CustomWriter': CustomWriterInstance}) + super(CustomWriterTransformer, self).__init__({'CustomWriter': CustomWriterInstance}) class JavaRandomTransformer(BaseTransformer): def __init__(self): - super().__init__() + super(JavaRandomTransformer, self).__init__() self.name = "java.util.Random" self.fields = { 'haveNextNextGaussian': javaobj.beans.FieldType.BOOLEAN, From 56c526d4482de1f8d4defcad72b90587a51d674c Mon Sep 17 00:00:00 2001 From: Fede A Date: Wed, 15 Apr 2020 23:11:22 -0300 Subject: [PATCH 08/10] uses lists instead dict and fixes python 2 compatibility --- tests/tests_v2.py | 46 ++++++++++++++++------------------------------ 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/tests/tests_v2.py b/tests/tests_v2.py index 77f3631..96fd8a4 100644 --- a/tests/tests_v2.py +++ b/tests/tests_v2.py @@ -60,13 +60,6 @@ # ------------------------------------------------------------------------------ # Custom writeObject parsing classes - -def assign_dict(fields, values): - ret = dict() - for i, v in enumerate(fields): - ret[v] = values[i] - return ret - class CustomWriterInstance(javaobj.beans.JavaInstance): def __init__(self): javaobj.beans.JavaInstance.__init__(self) @@ -84,7 +77,7 @@ def load_from_instance(self): '>i', BytesIO(raw_data[0].data).read(4))[0] custom_obj = raw_data[1] values = [int_not_in_fields, custom_obj] - self.field_data = assign_dict(fields, values) + self.field_data = dict(zip(fields, values)) return True return False @@ -100,15 +93,16 @@ def load_from_instance(self): if self.classdesc and self.classdesc in self.field_data: fields = self.classdesc.fields_names values = self.field_data[self.classdesc].values() - self.field_data = assign_dict(fields, list(values)) + self.field_data = dict(zip(fields, values)) if self.classdesc.super_class and self.classdesc.super_class in self.annotations: super_class = self.annotations[self.classdesc.super_class][0] - self.annotations = assign_dict(super_class.fields_names, super_class.field_data) + self.annotations = dict(zip(super_class.fields_names, super_class.field_data)) return True return False +__metaclass__ = type class BaseTransformer(javaobj.transformers.ObjectTransformer): """ Creates a JavaInstance object with custom loading methods for the @@ -147,19 +141,20 @@ class JavaRandomTransformer(BaseTransformer): def __init__(self): super(JavaRandomTransformer, self).__init__() self.name = "java.util.Random" - self.fields = { - 'haveNextNextGaussian': javaobj.beans.FieldType.BOOLEAN, - 'nextNextGaussian': javaobj.beans.FieldType.DOUBLE, - 'seed': javaobj.beans.FieldType.LONG - } + self.field_names = ['haveNextNextGaussian', 'nextNextGaussian', 'seed'] + self.field_types = [ + javaobj.beans.FieldType.BOOLEAN, + javaobj.beans.FieldType.DOUBLE, + javaobj.beans.FieldType.LONG + ] def load_custom_writeObject(self, parser, reader, name): if name == self.name: fields = [] values = [] - for field_name, field_type in self.fields.items(): - values.append(parser._read_field_value(field_type)) - fields.append(javaobj.beans.JavaField(field_type, field_name)) + for index, value in enumerate(self.field_types): + values.append(parser._read_field_value(value)) + fields.append(javaobj.beans.JavaField(value, self.field_names[index])) class_desc = javaobj.beans.JavaClassDesc( javaobj.beans.ClassDescType.NORMALCLASS) @@ -575,18 +570,9 @@ def test_writeObject(self): } } - self.assertEqual(expected['int_not_in_fields'], - parent_data['int_not_in_fields']) - self.assertEqual(expected['custom_obj'] - ['field_data']['doub'], child_data['doub']) - self.assertEqual(expected['custom_obj'] - ['field_data']['num'], child_data['num']) - self.assertEqual(expected['custom_obj']['annotations'] - ['haveNextNextGaussian'], super_data['haveNextNextGaussian']) - self.assertEqual(expected['custom_obj']['annotations'] - ['nextNextGaussian'], super_data['nextNextGaussian']) - self.assertEqual(expected['custom_obj'] - ['annotations']['seed'], super_data['seed']) + self.assertEqual(expected['int_not_in_fields'], parent_data['int_not_in_fields']) + self.assertEqual(expected['custom_obj']['field_data'], child_data) + self.assertEqual(expected['custom_obj']['annotations'], super_data) # ------------------------------------------------------------------------------ From c7ebb0ec518ae05d8716ce30a72eca61cfefdb6f Mon Sep 17 00:00:00 2001 From: Fede A Date: Wed, 15 Apr 2020 23:19:31 -0300 Subject: [PATCH 09/10] extends from object on ObjectTransformer for python2 compatibility --- javaobj/v2/api.py | 2 +- tests/tests_v2.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/javaobj/v2/api.py b/javaobj/v2/api.py index ba87747..8f7cfa9 100644 --- a/javaobj/v2/api.py +++ b/javaobj/v2/api.py @@ -44,7 +44,7 @@ # ------------------------------------------------------------------------------ -class ObjectTransformer: +class ObjectTransformer(object): """ Representation of an object transformer """ diff --git a/tests/tests_v2.py b/tests/tests_v2.py index 96fd8a4..fb03685 100644 --- a/tests/tests_v2.py +++ b/tests/tests_v2.py @@ -102,7 +102,6 @@ def load_from_instance(self): return False -__metaclass__ = type class BaseTransformer(javaobj.transformers.ObjectTransformer): """ Creates a JavaInstance object with custom loading methods for the From 80acea13207b6ff8f36e65d3e6c593739d47c42d Mon Sep 17 00:00:00 2001 From: Fede A Date: Wed, 15 Apr 2020 23:37:37 -0300 Subject: [PATCH 10/10] removes dict values call --- tests/tests_v2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests_v2.py b/tests/tests_v2.py index fb03685..317e9a9 100644 --- a/tests/tests_v2.py +++ b/tests/tests_v2.py @@ -92,7 +92,7 @@ def load_from_instance(self): """ if self.classdesc and self.classdesc in self.field_data: fields = self.classdesc.fields_names - values = self.field_data[self.classdesc].values() + values = [self.field_data[self.classdesc][self.classdesc.fields[i]] for i in range(len(fields))] self.field_data = dict(zip(fields, values)) if self.classdesc.super_class and self.classdesc.super_class in self.annotations: super_class = self.annotations[self.classdesc.super_class][0]