• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tcalmant / python-javaobj / 26727430648

31 May 2026 11:21PM UTC coverage: 78.938% (-0.02%) from 78.962%
26727430648

Pull #65

github

web-flow
Merge 2e4bf5d7f into ccb2fae0c
Pull Request #65: Bug fix

0 of 4 new or added lines in 3 files covered. (0.0%)

3 existing lines in 1 file now uncovered.

2586 of 3276 relevant lines covered (78.94%)

4.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.58
/javaobj/v1/unmarshaller.py
1
#!/usr/bin/python
2
# -- Content-Encoding: utf-8 --
3
"""
4
Provides functions for reading Java objects serialized by ObjectOutputStream.
5
This form of object representation is a standard data interchange format in
6
Java world.
7

8
javaobj module exposes an API familiar to users of the standard library
9
marshal, pickle and json modules.
10

11
See:
12
http://download.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html
13

14
:authors: Volodymyr Buell, Thomas Calmant
15
:license: Apache License 2.0
16
:version: 0.4.4
17
:status: Alpha
18

19
..
20

21
    Copyright 2024 Thomas Calmant
22

23
    Licensed under the Apache License, Version 2.0 (the "License");
24
    you may not use this file except in compliance with the License.
25
    You may obtain a copy of the License at
26

27
        http://www.apache.org/licenses/LICENSE-2.0
28

29
    Unless required by applicable law or agreed to in writing, software
30
    distributed under the License is distributed on an "AS IS" BASIS,
31
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
32
    See the License for the specific language governing permissions and
33
    limitations under the License.
34
"""
35

36
from __future__ import absolute_import
7✔
37

38
# Standard library
39
from typing import Any, Union
7✔
40
import os
7✔
41
import struct
7✔
42

43
# Javaobj modules
44
from .beans import (
7✔
45
    JavaClass,
46
    JavaString,
47
    JavaObject,
48
    JavaByteArray,
49
    JavaEnum,
50
    JavaArray,
51
)
52
from ..constants import (
7✔
53
    StreamConstants,
54
    ClassDescFlags,
55
    TerminalCode,
56
    TypeCode,
57
    StreamCodeDebug,
58
)
59
from ..utils import (
7✔
60
    log_debug,
61
    log_error,
62
    read_to_str,
63
    to_unicode,
64
    unicode_char,
65
    hexdump,
66
)
67

68
numpy = None  # Imported only when really used
7✔
69

70
# ------------------------------------------------------------------------------
71

72
__all__ = ("JavaObjectUnmarshaller",)
7✔
73

74
# Module version
75
__version_info__ = (0, 4, 4)
7✔
76
__version__ = ".".join(str(x) for x in __version_info__)
7✔
77

78
# Documentation strings format
79
__docformat__ = "restructuredtext en"
7✔
80

81
# ------------------------------------------------------------------------------
82

83
# Convertion of a Java type char to its NumPy equivalent
84
NUMPY_TYPE_MAP = {
7✔
85
    TypeCode.TYPE_BYTE: "B",
86
    TypeCode.TYPE_CHAR: ">u2",
87
    TypeCode.TYPE_DOUBLE: ">d",
88
    TypeCode.TYPE_FLOAT: ">f",
89
    TypeCode.TYPE_INTEGER: ">i",
90
    TypeCode.TYPE_LONG: ">l",
91
    TypeCode.TYPE_SHORT: ">h",
92
    TypeCode.TYPE_BOOLEAN: ">B",
93
}
94

95
# ------------------------------------------------------------------------------
96

97

98
class JavaObjectUnmarshaller:
7✔
99
    """
100
    Deserializes a Java serialization stream
101
    """
102

103
    def __init__(self, stream, use_numpy_arrays=False):
7✔
104
        """
105
        Sets up members
106

107
        :param stream: An input stream (opened in binary/bytes mode)
108
        :raise IOError: Invalid input stream
109
        """
110
        self.use_numpy_arrays = use_numpy_arrays
7✔
111

112
        # Numpy array support
113
        if self.use_numpy_arrays:
7✔
114
            try:
×
115
                global numpy
116
                import numpy as np
×
117

118
                numpy = np
×
119
            except ImportError:
×
120
                pass
×
121

122
        # Check stream
123
        if stream is None:
7✔
124
            raise IOError("No input stream given")
×
125

126
        # Prepare the association Terminal Symbol -> Reading method
127
        self.opmap = {
7✔
128
            TerminalCode.TC_NULL: self.do_null,
129
            TerminalCode.TC_CLASSDESC: self.do_classdesc,
130
            TerminalCode.TC_OBJECT: self.do_object,
131
            TerminalCode.TC_STRING: self.do_string,
132
            TerminalCode.TC_LONGSTRING: self.do_string_long,
133
            TerminalCode.TC_ARRAY: self.do_array,
134
            TerminalCode.TC_CLASS: self.do_class,
135
            TerminalCode.TC_BLOCKDATA: self.do_blockdata,
136
            TerminalCode.TC_BLOCKDATALONG: self.do_blockdata_long,
137
            TerminalCode.TC_REFERENCE: self.do_reference,
138
            TerminalCode.TC_ENUM: self.do_enum,
139
            # note that we are reusing do_null:
140
            TerminalCode.TC_ENDBLOCKDATA: self.do_null,
141
        }
142

143
        # Set up members
144
        self.current_object = None
7✔
145
        self.reference_counter = 0
7✔
146
        self.references = []
7✔
147
        self.object_transformers = []
7✔
148
        self.object_stream = stream
7✔
149

150
        # Read the stream header (magic & version)
151
        self._readStreamHeader()
7✔
152

153
    def readObject(self, ignore_remaining_data=False):
7✔
154
        """
155
        Reads an object from the input stream
156

157
        :param ignore_remaining_data: If True, don't log an error when
158
                                      unused trailing bytes are remaining
159
        :return: The unmarshalled object
160
        :raise Exception: Any exception that occurred during unmarshalling
161
        """
162
        try:
7✔
163
            # TODO: add expects
164
            _, res = self._read_and_exec_opcode(ident=0)
7✔
165

166
            position_bak = self.object_stream.tell()
7✔
167
            the_rest = self.object_stream.read()
7✔
168
            if not ignore_remaining_data and len(the_rest) != 0:
7✔
169
                log_error(
7✔
170
                    "Warning!!!!: Stream still has {0} bytes left. "
171
                    "Enable debug mode of logging to see the hexdump.".format(
172
                        len(the_rest)
173
                    )
174
                )
175
                log_debug("\n{0}".format(hexdump(the_rest)))
7✔
176
            else:
177
                log_debug("Java Object unmarshalled successfully!")
7✔
178

179
            self.object_stream.seek(position_bak)
7✔
180
            return res
7✔
181
        except Exception:
×
182
            self._oops_dump_state(ignore_remaining_data)
×
183
            raise
×
184

185
    def add_transformer(self, transformer):
7✔
186
        """
187
        Appends an object transformer to the deserialization process
188

189
        :param transformer: An object with a transform(obj) method
190
        """
191
        self.object_transformers.append(transformer)
7✔
192

193
    def _readStreamHeader(self):
7✔
194
        """
195
        Reads the magic header of a Java serialization stream
196

197
        :raise IOError: Invalid magic header (not a Java stream)
198
        """
199
        (magic, version) = self._readStruct(">HH")
7✔
200
        if (
7✔
201
            magic != StreamConstants.STREAM_MAGIC
202
            or version != StreamConstants.STREAM_VERSION
203
        ):
204
            raise IOError(
×
205
                "The stream is not java serialized object. "
206
                "Invalid stream header: {0:04X}{1:04X}".format(magic, version)
207
            )
208

209
    def _read_and_exec_opcode(self, ident=0, expect=None):
7✔
210
        """
211
        Reads the next opcode, and executes its handler
212

213
        :param ident: Log identation level
214
        :param expect: A list of expected opcodes
215
        :return: A tuple: (opcode, result of the handler)
216
        :raise IOError: Read opcode is not one of the expected ones
217
        :raise RuntimeError: Unknown opcode
218
        """
219
        position = self.object_stream.tell()
7✔
220
        (opid,) = self._readStruct(">B")
7✔
221
        log_debug(
7✔
222
            "OpCode: 0x{0:X} -- {1} (at offset 0x{2:X})".format(
223
                opid, StreamCodeDebug.op_id(opid), position
224
            ),
225
            ident,
226
        )
227

228
        if expect and opid not in expect:
7✔
229
            raise IOError(
×
230
                "Unexpected opcode 0x{0:X} -- {1} "
231
                "(at offset 0x{2:X})".format(
232
                    opid, StreamCodeDebug.op_id(opid), position
233
                )
234
            )
235

236
        try:
7✔
237
            handler = self.opmap[opid]
7✔
238
        except KeyError:
×
239
            raise RuntimeError(
×
240
                "Unknown OpCode in the stream: 0x{0:X} "
241
                "(at offset 0x{1:X})".format(opid, position)
242
            )
243
        else:
244
            return opid, handler(ident=ident)
7✔
245

246
    def _readStruct(self, unpack):
7✔
247
        """
248
        Reads from the input stream, using struct
249

250
        :param unpack: An unpack format string
251
        :return: The result of struct.unpack (tuple)
252
        :raise RuntimeError: End of stream reached during unpacking
253
        """
254
        length = struct.calcsize(unpack)
7✔
255
        ba = self.object_stream.read(length)
7✔
256

257
        if len(ba) != length:
7✔
258
            raise RuntimeError(
×
259
                "Stream has been ended unexpectedly while unmarshaling."
260
            )
261

262
        return struct.unpack(unpack, ba)
7✔
263

264
    def _readString(self, length_fmt="H"):
7✔
265
        """
266
        Reads a serialized string
267

268
        :param length_fmt: Structure format of the string length (H or Q)
269
        :return: The deserialized string
270
        :raise RuntimeError: Unexpected end of stream
271
        """
272
        (length,) = self._readStruct(">{0}".format(length_fmt))
7✔
273
        ba = self.object_stream.read(length)
7✔
274
        return to_unicode(ba)
7✔
275

276
    def do_classdesc(self, parent=None, ident=0):
7✔
277
        """
278
        Handles a TC_CLASSDESC opcode
279

280
        :param parent:
281
        :param ident: Log indentation level
282
        :return: A JavaClass object
283
        """
284
        # TC_CLASSDESC className serialVersionUID newHandle classDescInfo
285
        # classDescInfo:
286
        #   classDescFlags fields classAnnotation superClassDesc
287
        # classDescFlags:
288
        #   (byte)                 // Defined in Terminal Symbols and Constants
289
        # fields:
290
        #   (short)<count>  fieldDesc[count]
291

292
        # fieldDesc:
293
        #   primitiveDesc
294
        #   objectDesc
295
        # primitiveDesc:
296
        #   prim_typecode fieldName
297
        # objectDesc:
298
        #   obj_typecode fieldName className1
299
        clazz = JavaClass()
7✔
300
        log_debug("[classdesc]", ident)
7✔
301
        class_name = self._readString()
7✔
302
        clazz.name = class_name
7✔
303
        log_debug("Class name: %s" % class_name, ident)
7✔
304

305
        # serialVersionUID is a Java (signed) long => 8 bytes
306
        serialVersionUID, classDescFlags = self._readStruct(">qB")
7✔
307
        clazz.serialVersionUID = serialVersionUID
7✔
308
        clazz.flags = classDescFlags
7✔
309

310
        self._add_reference(clazz, ident)
7✔
311

312
        log_debug(
7✔
313
            "Serial: 0x{0:X} / {0:d} - classDescFlags: 0x{1:X} {2}".format(
314
                serialVersionUID,
315
                classDescFlags,
316
                StreamCodeDebug.flags(classDescFlags),
317
            ),
318
            ident,
319
        )
320
        (length,) = self._readStruct(">H")
7✔
321
        log_debug("Fields num: 0x{0:X}".format(length), ident)
7✔
322

323
        clazz.fields_names = []
7✔
324
        clazz.fields_types = []
7✔
325
        for fieldId in range(length):
7✔
326
            (typecode,) = self._readStruct(">B")
7✔
327
            field_name = self._readString()
7✔
328
            base_field_type = self._convert_char_to_type(typecode)
7✔
329

330
            log_debug("> Reading field {0}".format(field_name), ident)
7✔
331

332
            if base_field_type == TypeCode.TYPE_ARRAY:
7✔
333
                _, field_type = self._read_and_exec_opcode(
7✔
334
                    ident=ident + 1,
335
                    expect=(TerminalCode.TC_STRING, TerminalCode.TC_REFERENCE),
336
                )
337

338
                if type(field_type) is not JavaString:  # pylint:disable=C0123
7✔
339
                    raise AssertionError(
×
340
                        "Field type must be a JavaString, "
341
                        "not {0}".format(type(field_type))
342
                    )
343

344
            elif base_field_type == TypeCode.TYPE_OBJECT:
7✔
345
                _, field_type = self._read_and_exec_opcode(
7✔
346
                    ident=ident + 1,
347
                    expect=(TerminalCode.TC_STRING, TerminalCode.TC_REFERENCE),
348
                )
349

350
                if isinstance(field_type, JavaClass):
7✔
351
                    # FIXME: ugly trick
352
                    field_type = JavaString(field_type.name)
×
353

354
                if type(field_type) is not JavaString:  # pylint:disable=C0123
7✔
355
                    raise AssertionError(
×
356
                        "Field type must be a JavaString, "
357
                        "not {0}".format(type(field_type))
358
                    )
359
            else:
360
                # Convert the TypeCode to its char value
361
                field_type = JavaString(str(chr(base_field_type.value)))
7✔
362

363
            log_debug(
7✔
364
                "< FieldName: 0x{0:X} Name:{1} Type:{2} ID:{3}".format(
365
                    typecode, field_name, field_type, fieldId
366
                ),
367
                ident,
368
            )
369
            assert field_name is not None
7✔
370
            assert field_type is not None
7✔
371

372
            clazz.fields_names.append(field_name)
7✔
373
            clazz.fields_types.append(field_type)
7✔
374

375
        if parent:
7✔
376
            parent.__fields = clazz.fields_names  # pylint:disable=W0212
×
377
            parent.__types = clazz.fields_types  # pylint:disable=W0212
×
378

379
        # classAnnotation
380
        (opid,) = self._readStruct(">B")
7✔
381
        log_debug(
7✔
382
            "OpCode: 0x{0:X} -- {1} (classAnnotation)".format(
383
                opid, StreamCodeDebug.op_id(opid)
384
            ),
385
            ident,
386
        )
387
        if opid != TerminalCode.TC_ENDBLOCKDATA:
7✔
388
            raise NotImplementedError("classAnnotation isn't implemented yet")
×
389

390
        # superClassDesc
391
        log_debug("Reading Super Class of {0}".format(clazz.name), ident)
7✔
392
        _, superclassdesc = self._read_and_exec_opcode(
7✔
393
            ident=ident + 1,
394
            expect=(
395
                TerminalCode.TC_CLASSDESC,
396
                TerminalCode.TC_NULL,
397
                TerminalCode.TC_REFERENCE,
398
            ),
399
        )
400
        log_debug(
7✔
401
            "Super Class for {0}: {1}".format(clazz.name, str(superclassdesc)),
402
            ident,
403
        )
404
        clazz.superclass = superclassdesc
7✔
405
        return clazz
7✔
406

407
    def do_blockdata(self, parent=None, ident=0):
7✔
408
        """
409
        Handles TC_BLOCKDATA opcode
410

411
        :param parent:
412
        :param ident: Log indentation level
413
        :return: A string containing the block data
414
        """
415
        # TC_BLOCKDATA (unsigned byte)<size> (byte)[size]
416
        log_debug("[blockdata]", ident)
7✔
417
        (length,) = self._readStruct(">B")
7✔
418
        ba = self.object_stream.read(length)
7✔
419

420
        # Ensure we have an str
421
        return read_to_str(ba)
7✔
422

423
    def do_blockdata_long(self, parent=None, ident=0):
7✔
424
        """
425
        Handles TC_BLOCKDATALONG opcode
426

427
        :param parent:
428
        :param ident: Log indentation level
429
        :return: A string containing the block data
430
        """
431
        # TC_BLOCKDATALONG (int)<size> (byte)[size]
432
        log_debug("[blockdatalong]", ident)
×
433
        (length,) = self._readStruct(">I")
×
434
        ba = self.object_stream.read(length)
×
435

436
        # Ensure we have an str
437
        return read_to_str(ba)
×
438

439
    def do_class(self, parent=None, ident=0):
7✔
440
        """
441
        Handles TC_CLASS opcode
442

443
        :param parent:
444
        :param ident: Log indentation level
445
        :return: A JavaClass object
446
        """
447
        # TC_CLASS classDesc newHandle
448
        log_debug("[class]", ident)
7✔
449

450
        # TODO: what to do with "(ClassDesc)prevObject".
451
        # (see 3rd line for classDesc:)
452
        _, classdesc = self._read_and_exec_opcode(
7✔
453
            ident=ident + 1,
454
            expect=(
455
                TerminalCode.TC_CLASSDESC,
456
                TerminalCode.TC_PROXYCLASSDESC,
457
                TerminalCode.TC_NULL,
458
                TerminalCode.TC_REFERENCE,
459
            ),
460
        )
461
        log_debug("Classdesc: {0}".format(classdesc), ident)
7✔
462
        self._add_reference(classdesc, ident)
7✔
463
        return classdesc
7✔
464

465
    def do_object(self, parent=None, ident=0):
7✔
466
        """
467
        Handles a TC_OBJECT opcode
468

469
        :param parent:
470
        :param ident: Log indentation level
471
        :return: A JavaClass object
472
        """
473
        # TC_OBJECT classDesc newHandle classdata[]  // data for each class
474
        java_object = JavaObject()
7✔
475
        log_debug("[object]", ident)
7✔
476
        log_debug(
7✔
477
            "java_object.annotations just after instantiation: {0}".format(
478
                java_object.annotations
479
            ),
480
            ident,
481
        )
482

483
        # TODO: what to do with "(ClassDesc)prevObject".
484
        # (see 3rd line for classDesc:)
485
        opcode, classdesc = self._read_and_exec_opcode(
7✔
486
            ident=ident + 1,
487
            expect=(
488
                TerminalCode.TC_CLASSDESC,
489
                TerminalCode.TC_PROXYCLASSDESC,
490
                TerminalCode.TC_NULL,
491
                TerminalCode.TC_REFERENCE,
492
            ),
493
        )
494
        # self.TC_REFERENCE hasn't shown in spec, but actually is here
495

496
        # Create object
497
        for transformer in self.object_transformers:
7✔
498
            java_object = transformer.create(classdesc, self)
7✔
499
            if java_object is not None:
7✔
500
                break
7✔
501

502
        # Store classdesc of this object
503
        java_object.classdesc = classdesc
7✔
504

505
        # Store the reference
506
        self._add_reference(java_object, ident)
7✔
507

508
        # classdata[]
509

510
        if (
7✔
511
            classdesc is not None
512
            and classdesc.flags & ClassDescFlags.SC_EXTERNALIZABLE
513
            and not classdesc.flags & ClassDescFlags.SC_BLOCK_DATA
514
        ):
515
            # TODO:
516
            raise NotImplementedError("externalContents isn't implemented yet")
×
517

518
        # Process class data for the entire hierarchy
519
        self._read_serializable_data(java_object, classdesc, ident)
7✔
520

521
        log_debug(">>> java_object: {0}".format(java_object), ident)
7✔
522
        return java_object
7✔
523

524
    def _read_serializable_data(self, java_object, classdesc, ident):
7✔
525
        """
526
        Read serializable data following the Java specification more closely.
527
        According to the spec, for each class in the hierarchy (from super to sub):
528
        1. Read primitive fields
529
        2. If SC_WRITE_METHOD is set, read custom writeObject data until TC_ENDBLOCKDATA
530
        """
531
        if classdesc.superclass:
7✔
532
            self._read_serializable_data(java_object, classdesc.superclass, ident + 1)
7✔
533

534
        if classdesc.flags & ClassDescFlags.SC_SERIALIZABLE:
7✔
535
            # TODO: look at ObjectInputStream.readSerialData()
536
            # FIXME: Handle the SC_WRITE_METHOD flag
537

538
            log_debug("Constructing class...", ident)
7✔
539
            log_debug("Class: {0}".format(classdesc.name), ident + 1)
7✔
540
            if classdesc.fields_names:
7✔
541
                class_fields_str = " - ".join(
7✔
542
                    " ".join((str(field_type), field_name))
543
                    for field_type, field_name in zip(
544
                        classdesc.fields_types, classdesc.fields_names
545
                    )
546
                )
547
                log_debug(class_fields_str, ident + 2)
7✔
548

549
            log_debug("Values count: {0}".format(len(classdesc.fields_names)), ident)
7✔
550
            log_debug("Prepared list of values: {0}".format(classdesc.fields_names), ident)
7✔
551
            log_debug("Prepared list of types: {0}".format(classdesc.fields_types), ident)
7✔
552

553
            for field_name, field_type in zip(classdesc.fields_names, classdesc.fields_types):
7✔
554
                log_debug(
7✔
555
                    "Reading field: {0} - {1}".format(field_type, field_name)
556
                )
557
                res = self._read_value(field_type, ident, name=field_name)
7✔
558
                java_object.__setattr__(field_name, res)
7✔
559

560
        has_write_method = classdesc.flags & ClassDescFlags.SC_SERIALIZABLE and classdesc.flags & ClassDescFlags.SC_WRITE_METHOD
7✔
561
        has_block_data = classdesc.flags & ClassDescFlags.SC_EXTERNALIZABLE and classdesc.flags & ClassDescFlags.SC_BLOCK_DATA
7✔
562
        if has_write_method or has_block_data:
7✔
563
            # objectAnnotation
564
            log_debug(
7✔
565
                "java_object.annotations before: {0}".format(
566
                    java_object.annotations
567
                ),
568
                ident,
569
            )
570

571
            opcode = None
7✔
572
            while opcode != TerminalCode.TC_ENDBLOCKDATA:
7✔
573
                opcode, obj = self._read_and_exec_opcode(ident=ident + 1)
7✔
574
                # , expect=[self.TC_ENDBLOCKDATA, self.TC_BLOCKDATA,
575
                # self.TC_OBJECT, self.TC_NULL, self.TC_REFERENCE])
576
                if opcode != TerminalCode.TC_ENDBLOCKDATA:
7✔
577
                    java_object.annotations.append(obj)
7✔
578

579
                log_debug("objectAnnotation value: {0}".format(obj), ident)
7✔
580

581
            log_debug(
7✔
582
                "java_object.annotations after: {0}".format(
583
                    java_object.annotations
584
                ),
585
                ident,
586
            )
587

588
        # Allow extra loading operations
589
        if hasattr(java_object, "__extra_loading__"):
7✔
590
            log_debug("Java object has extra loading capability.")
7✔
591
            java_object.__extra_loading__(self, ident)
7✔
592

593
    def do_string(self, parent=None, ident=0):
7✔
594
        """
595
        Handles a TC_STRING opcode
596

597
        :param parent:
598
        :param ident: Log indentation level
599
        :return: A string
600
        """
601
        log_debug("[string]", ident)
7✔
602
        ba = JavaString(self._readString())
7✔
603
        self._add_reference(ba, ident)
7✔
604
        return ba
7✔
605

606
    def do_string_long(self, parent=None, ident=0):
7✔
607
        """
608
        Handles a TC_LONGSTRING opcode
609

610
        :param parent:
611
        :param ident: Log indentation level
612
        :return: A string
613
        """
614
        log_debug("[long string]", ident)
×
615
        ba = JavaString(self._readString("Q"))
×
616
        self._add_reference(ba, ident)
×
617
        return ba
×
618

619
    def do_array(self, parent=None, ident=0):
7✔
620
        """
621
        Handles a TC_ARRAY opcode
622

623
        :param parent:
624
        :param ident: Log indentation level
625
        :return: A list of deserialized objects
626
        """
627
        # TC_ARRAY classDesc newHandle (int)<size> values[size]
628
        log_debug("[array]", ident)
7✔
629
        _, classdesc = self._read_and_exec_opcode(
7✔
630
            ident=ident + 1,
631
            expect=(
632
                TerminalCode.TC_CLASSDESC,
633
                TerminalCode.TC_PROXYCLASSDESC,
634
                TerminalCode.TC_NULL,
635
                TerminalCode.TC_REFERENCE,
636
            ),
637
        )
638

639
        array = JavaArray(classdesc)
7✔
640

641
        self._add_reference(array, ident)
7✔
642

643
        (size,) = self._readStruct(">i")
7✔
644
        log_debug("size: {0}".format(size), ident)
7✔
645

646
        array_type_code = TypeCode(ord(classdesc.name[0]))
7✔
647
        assert array_type_code == TypeCode.TYPE_ARRAY
7✔
648
        type_code = TypeCode(ord(classdesc.name[1]))
7✔
649

650
        if type_code in (TypeCode.TYPE_OBJECT, TypeCode.TYPE_ARRAY):
7✔
651
            for _ in range(size):
7✔
652
                _, res = self._read_and_exec_opcode(ident=ident + 1)
7✔
653
                log_debug("Object value: {0}".format(res), ident)
7✔
654
                array.append(res)
7✔
655
        elif type_code == TypeCode.TYPE_BYTE:
7✔
656
            array = JavaByteArray(self.object_stream.read(size), classdesc)
7✔
657
        elif self.use_numpy_arrays and numpy is not None:
7✔
658
            array = numpy.fromfile(
×
659
                self.object_stream,
660
                dtype=NUMPY_TYPE_MAP[type_code],
661
                count=size,
662
            )
663
        else:
664
            for _ in range(size):
7✔
665
                res = self._read_value(type_code, ident)
7✔
666
                log_debug("Native value: {0}".format(repr(res)), ident)
7✔
667
                array.append(res)
7✔
668

669
        return array
7✔
670

671
    def do_reference(self, parent=None, ident=0):
7✔
672
        """
673
        Handles a TC_REFERENCE opcode
674

675
        :param parent:
676
        :param ident: Log indentation level
677
        :return: The referenced object
678
        """
679
        (handle,) = self._readStruct(">L")
7✔
680
        log_debug("## Reference handle: 0x{0:X}".format(handle), ident)
7✔
681
        ref = self.references[handle - StreamConstants.BASE_REFERENCE_IDX]
7✔
682
        log_debug("###-> Type: {0} - Value: {1}".format(type(ref), ref), ident)
7✔
683
        return ref
7✔
684

685
    @staticmethod
7✔
686
    def do_null(parent=None, ident=0):
7✔
687
        """
688
        Handles a TC_NULL opcode
689

690
        :param parent:
691
        :param ident: Log indentation level
692
        :return: Always None
693
        """
694
        return None
7✔
695

696
    def do_enum(self, parent=None, ident=0):
7✔
697
        """
698
        Handles a TC_ENUM opcode
699

700
        :param parent:
701
        :param ident: Log indentation level
702
        :return: A JavaEnum object
703
        """
704
        # TC_ENUM classDesc newHandle enumConstantName
705
        enum = JavaEnum()
7✔
706
        _, classdesc = self._read_and_exec_opcode(
7✔
707
            ident=ident + 1,
708
            expect=(
709
                TerminalCode.TC_CLASSDESC,
710
                TerminalCode.TC_PROXYCLASSDESC,
711
                TerminalCode.TC_NULL,
712
                TerminalCode.TC_REFERENCE,
713
            ),
714
        )
715
        enum.classdesc = classdesc
7✔
716
        self._add_reference(enum, ident)
7✔
717
        (
7✔
718
            _,
719
            enumConstantName,
720
        ) = self._read_and_exec_opcode(  # pylint:disable=C0103
721
            ident=ident + 1,
722
            expect=(TerminalCode.TC_STRING, TerminalCode.TC_REFERENCE),
723
        )
724
        enum.constant = enumConstantName
7✔
725
        return enum
7✔
726

727
    def _read_value(self, raw_field_type, ident, name=""):
7✔
728
        # type: (Union[bytes, int, TypeCode], int, str) -> Any
729
        """
730
        Reads the next value, of the given type
731

732
        :param raw_field_type: A serialization typecode
733
        :param ident: Log indentation
734
        :param name: Field name (for logs)
735
        :return: The read value
736
        :raise RuntimeError: Unknown field type
737
        """
738
        if isinstance(raw_field_type, TypeCode):
7✔
739
            field_type = raw_field_type
7✔
740
        elif isinstance(raw_field_type, int):
7✔
741
            field_type = TypeCode(raw_field_type)
×
742
        else:
743
            # We don't need details for arrays and objects
744
            raw_code = raw_field_type[0]
7✔
745
            if isinstance(raw_code, int):
7✔
746
                field_type = TypeCode(raw_code)
×
747
            else:
748
                field_type = TypeCode(ord(raw_code))
7✔
749

750
        if field_type == TypeCode.TYPE_BOOLEAN:
7✔
751
            (val,) = self._readStruct(">B")
7✔
752
            res = bool(val)  # type: Any
7✔
753
        elif field_type == TypeCode.TYPE_BYTE:
7✔
754
            (res,) = self._readStruct(">b")
×
755
        elif field_type == TypeCode.TYPE_CHAR:
7✔
756
            # TYPE_CHAR is defined by the serialization specification
757
            # but not used in the implementation, so this is
758
            # a hypothetical code
759
            res = unicode_char(self._readStruct(">H")[0])
7✔
760
        elif field_type == TypeCode.TYPE_SHORT:
7✔
761
            (res,) = self._readStruct(">h")
×
762
        elif field_type == TypeCode.TYPE_INTEGER:
7✔
763
            (res,) = self._readStruct(">i")
7✔
764
        elif field_type == TypeCode.TYPE_LONG:
7✔
765
            (res,) = self._readStruct(">q")
×
766
        elif field_type == TypeCode.TYPE_FLOAT:
7✔
767
            (res,) = self._readStruct(">f")
7✔
768
        elif field_type == TypeCode.TYPE_DOUBLE:
7✔
769
            (res,) = self._readStruct(">d")
×
770
        elif field_type in (TypeCode.TYPE_OBJECT, TypeCode.TYPE_ARRAY):
7✔
771
            _, res = self._read_and_exec_opcode(ident=ident + 1)
7✔
772
        else:
773
            raise RuntimeError("Unknown typecode: {0}".format(field_type))
×
774

775
        log_debug(
7✔
776
            "* {0} {1}: {2}".format(chr(field_type.value), name, repr(res)),
777
            ident,
778
        )
779
        return res
7✔
780

781
    @staticmethod
7✔
782
    def _convert_char_to_type(type_char):
7✔
783
        # type: (Any) -> TypeCode
784
        """
785
        Ensures a read character is a typecode.
786

787
        :param type_char: Read typecode
788
        :return: The typecode as an integer (using ord)
789
        :raise RuntimeError: Unknown typecode
790
        """
791
        typecode = type_char
7✔
792
        if not isinstance(type_char, int):
7✔
793
            typecode = ord(type_char)
×
794

795
        try:
7✔
796
            return TypeCode(typecode)
7✔
797
        except ValueError:
×
798
            raise RuntimeError(
×
799
                "Typecode {0} ({1}) isn't supported.".format(
800
                    type_char, typecode
801
                )
802
            )
803

804
    def _add_reference(self, obj, ident=0):
7✔
805
        """
806
        Adds a read reference to the marshaler storage
807

808
        :param obj: Reference to add
809
        :param ident: Log indentation level
810
        """
811
        log_debug(
7✔
812
            "## New reference handle 0x{0:X}: {1} -> {2}".format(
813
                len(self.references) + StreamConstants.BASE_REFERENCE_IDX,
814
                type(obj).__name__,
815
                repr(obj),
816
            ),
817
            ident,
818
        )
819
        self.references.append(obj)
7✔
820

821
    def _oops_dump_state(self, ignore_remaining_data=False):
7✔
822
        """
823
        Log a deserialization error
824

825
        :param ignore_remaining_data: If True, don't log an error when
826
                                      unused trailing bytes are remaining
827
        """
828
        log_error("==Oops state dump" + "=" * (30 - 17))
×
829
        log_error("References: {0}".format(self.references))
×
830
        log_error(
×
831
            "Stream seeking back at -16 byte "
832
            "(2nd line is an actual position!):"
833
        )
834

835
        # Do not use a keyword argument; clamp to avoid seeking before start
NEW
836
        current_pos = self.object_stream.tell()
×
NEW
837
        self.object_stream.seek(max(0, current_pos - 16))
×
838
        position = self.object_stream.tell()
×
839
        the_rest = self.object_stream.read()
×
840

841
        if not ignore_remaining_data and len(the_rest) != 0:
×
842
            log_error(
×
843
                "Warning!!!!: Stream still has {0} bytes left:\n{1}".format(
844
                    len(the_rest), hexdump(the_rest, position)
845
                )
846
            )
847

848
        log_error("=" * 30)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc