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/api.py b/javaobj/v2/api.py index 68bde15..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 """ @@ -80,7 +80,7 @@ def load_array(self, reader, type_code, size): return None def load_custom_writeObject(self, parser, reader, name): - # type: (JavaStreamParser, DataStreamReader, str) -> Optional[list] + # type: (JavaStreamParser, DataStreamReader, str) -> Optional[JavaClassDesc] """ Reads content stored from a custom writeObject. @@ -92,6 +92,6 @@ def load_custom_writeObject(self, parser, reader, name): :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: A JavaClassDesc instance or None """ return None diff --git a/javaobj/v2/beans.py b/javaobj/v2/beans.py index 42f3fa5..cb8f788 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 ClassDataType(IntEnum): """ @@ -196,7 +206,6 @@ def __hash__(self): def __eq__(self, other): return self.value == other - class JavaField: """ Represents a field in a Java class description diff --git a/javaobj/v2/core.py b/javaobj/v2/core.py index f17231d..5b126b6 100644 --- a/javaobj/v2/core.py +++ b/javaobj/v2/core.py @@ -362,7 +362,6 @@ def _do_classdesc(self, type_code): 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)) @@ -395,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 @@ -405,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 @@ -421,6 +424,9 @@ 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 @@ -461,7 +467,6 @@ def _read_class_annotations(self, class_desc=None): # Reset references self._reset() continue - java_object = self._read_content(type_code, True, class_desc) if java_object is not None and java_object.is_exception: @@ -481,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() @@ -546,14 +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..317e9a9 100644 --- a/tests/tests_v2.py +++ b/tests/tests_v2.py @@ -32,18 +32,21 @@ from __future__ import print_function # Standard library +from javaobj.utils import bytes_char +import javaobj.v2 as javaobj import logging import os 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()))) # Local -import javaobj.v2 as javaobj -from javaobj.utils import bytes_char # ------------------------------------------------------------------------------ @@ -54,6 +57,115 @@ # ------------------------------------------------------------------------------ +# ------------------------------------------------------------------------------ + +# 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][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] + 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(RandomChildTransformer, self).__init__({'RandomChild': RandomChildInstance}) + + +class CustomWriterTransformer(BaseTransformer): + def __init__(self): + super(CustomWriterTransformer, self).__init__({'CustomWriter': CustomWriterInstance}) + + +class JavaRandomTransformer(BaseTransformer): + def __init__(self): + super(JavaRandomTransformer, self).__init__() + self.name = "java.util.Random" + 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 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) + 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 +537,41 @@ 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) # ------------------------------------------------------------------------------