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

tcalmant / python-javaobj / 26726407003

31 May 2026 10:32PM UTC coverage: 78.709% (+0.008%) from 78.701%
26726407003

push

github

web-flow
Merge pull request #63 from tcalmant/v3

Addition of a v3 package

788 of 1003 new or added lines in 7 files covered. (78.56%)

2403 of 3053 relevant lines covered (78.71%)

4.44 hits per line

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

74.59
/javaobj/v3/parser.py
1
#!/usr/bin/env python3
2
"""
3
Parser for the Java Object Serialization stream format (v3)
4

5
:authors: Thomas Calmant
6
:license: Apache License 2.0
7
:version: 0.5.0
8
:status: Alpha
9

10
..
11

12
    Copyright 2026 Thomas Calmant
13

14
    Licensed under the Apache License, Version 2.0 (the "License");
15
    you may not use this file except in compliance with the License.
16
    You may obtain a copy of the License at
17

18
        http://www.apache.org/licenses/LICENSE-2.0
19

20
    Unless required by applicable law or agreed to in writing, software
21
    distributed under the License is distributed on an "AS IS" BASIS,
22
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
23
    See the License for the specific language governing permissions and
24
    limitations under the License.
25
"""
26

27
# Standard library
28
import logging
3✔
29
from typing import IO, Any
3✔
30

31
# Javaobj
32
from ..constants import (
3✔
33
    ClassDescFlags,
34
    StreamConstants,
35
    TerminalCode,
36
    TypeCode,
37
)
38
from .beans import (
3✔
39
    BlockData,
40
    ClassDataType,
41
    ClassDescType,
42
    ExceptionState,
43
    FieldType,
44
    JavaArray,
45
    JavaClass,
46
    JavaClassDesc,
47
    JavaEnum,
48
    JavaField,
49
    JavaInstance,
50
    JavaString,
51
    ParsedContent,
52
)
53
from .exceptions import (
3✔
54
    ParseError,
55
    SecurityError,
56
    UnexpectedOpcodeError,
57
    UnsupportedFeatureError,
58
)
59
from .reader import DataReader
3✔
60
from .transformers import DefaultObjectTransformer, ObjectTransformer
3✔
61

62
# ------------------------------------------------------------------------------
63

64
# Module version
65
__version_info__ = (0, 5, 0)
3✔
66
__version__ = ".".join(str(x) for x in __version_info__)
3✔
67

68
# Documentation strings format
69
__docformat__ = "restructuredtext en"
3✔
70

71
# ------------------------------------------------------------------------------
72

73
__all__ = ["JavaStreamParser"]
3✔
74

75
_log = logging.getLogger("javaobj.v3.parser")
3✔
76

77

78
class _ExceptionRead(Exception):
3✔
79
    """Internal signal: a TC_EXCEPTION object was parsed and should propagate."""
80

81
    def __init__(self, content: JavaInstance) -> None:
3✔
NEW
82
        self.exception_object = content
×
83

84

85
class JavaStreamParser:
3✔
86
    """
87
    Stateful parser for the Java Object Serialization stream format.
88

89
    Usage::
90

91
        parser = JavaStreamParser(fd, transformers)
92
        contents = parser.run()
93

94
    Parameters
95
    ----------
96
    fd:
97
        A readable binary file-like object positioned at the start of a Java
98
        serialized stream (magic ``0xACED``).
99
    transformers:
100
        Ordered list of :class:`~javaobj.v3.transformers.ObjectTransformer`
101
        instances.  Transformers are tried in order; the first one that
102
        returns a non-``None`` result wins.
103
    max_array_size:
104
        Maximum number of bytes allocatable by a single array or bulk-read
105
        operation.  Raises :class:`~javaobj.v3.exceptions.SecurityError` on
106
        breach.
107
    max_depth:
108
        Maximum recursion depth of the parse tree.  Raises
109
        :class:`~javaobj.v3.exceptions.SecurityError` on breach.
110
    """
111

112
    def __init__(
3✔
113
        self,
114
        fd: IO[bytes],
115
        transformers: list[ObjectTransformer],
116
        *,
117
        max_array_size: int = DataReader.DEFAULT_MAX_ARRAY_SIZE,
118
        max_depth: int = DataReader.DEFAULT_MAX_DEPTH,
119
    ) -> None:
120
        self._fd = fd
3✔
121
        self._reader = DataReader(
3✔
122
            fd,
123
            max_array_size=max_array_size,
124
            max_string_size=max_array_size,
125
        )
126
        self._transformers = list(transformers)
3✔
127
        self._max_depth = max_depth
3✔
128

129
        # Handle table: maps handle int → ParsedContent
130
        self._handles: dict[int, ParsedContent] = {}
3✔
131
        # Saved handle snapshots from TC_RESET events
132
        self._handle_maps: list[dict[int, ParsedContent]] = []
3✔
133
        self._current_handle = int(StreamConstants.BASE_REFERENCE_IDX)
3✔
134

135
        # Current recursion depth
136
        self._depth = 0
3✔
137

138
    # ------------------------------------------------------------------
139
    # Public entry point
140
    # ------------------------------------------------------------------
141

142
    def run(self) -> list[ParsedContent]:
3✔
143
        """
144
        Parses the stream and returns a list of top-level content objects.
145

146
        :raises ParseError: On malformed stream data.
147
        :raises SecurityError: If a configured limit is breached.
148
        :raises UnsupportedFeatureError: If an unimplemented feature is used.
149
        """
150
        magic = self._reader.read_ushort()
3✔
151
        if magic != StreamConstants.STREAM_MAGIC:
3✔
152
            raise ParseError(f"Invalid stream magic: 0x{magic:04x} (expected 0xACED)", 0)
3✔
153
        version = self._reader.read_ushort()
3✔
154
        if version != StreamConstants.STREAM_VERSION:
3✔
NEW
155
            raise ParseError(
×
156
                f"Unsupported stream version: 0x{version:04x} "
157
                f"(expected 0x{int(StreamConstants.STREAM_VERSION):04x})",
158
                2,
159
            )
160

161
        self._reset()
3✔
162
        contents: list[ParsedContent] = []
3✔
163

164
        while True:
3✔
165
            try:
3✔
166
                opcode = self._reader.read_ubyte()
3✔
167
            except EOFError:
3✔
168
                break
3✔
169

170
            if opcode == TerminalCode.TC_RESET:
3✔
NEW
171
                self._reset()
×
NEW
172
                continue
×
173

174
            start_offset = self._reader.offset - 1
3✔
175
            item = self._read_content(opcode, block_data_allowed=True)
3✔
176

177
            if isinstance(item, JavaInstance) and item.is_exception:
3✔
178
                # Wrap exception instances together with their raw bytes.
NEW
179
                end_offset = self._reader.offset
×
NEW
180
                self._fd.seek(start_offset)
×
NEW
181
                raw = self._fd.read(end_offset - start_offset)
×
NEW
182
                item = ExceptionState(
×
183
                    exception_object=item,
184
                    stream_data=raw,
185
                    handle=item.handle,
186
                )
187

188
            contents.append(item)
3✔
189

190
        if self._handles:
3✔
191
            self._handle_maps.append(dict(self._handles))
3✔
192

193
        return contents
3✔
194

195
    # ------------------------------------------------------------------
196
    # Internal state management
197
    # ------------------------------------------------------------------
198

199
    def _reset(self) -> None:
3✔
200
        """Saves the current handle map and starts a fresh one (TC_RESET)."""
201
        if self._handles:
3✔
NEW
202
            self._handle_maps.append(dict(self._handles))
×
203
        self._handles.clear()
3✔
204
        self._current_handle = int(StreamConstants.BASE_REFERENCE_IDX)
3✔
205

206
    def _new_handle(self) -> int:
3✔
207
        """Allocates and returns the next handle value."""
208
        h = self._current_handle
3✔
209
        self._current_handle += 1
3✔
210
        return h
3✔
211

212
    def _set_handle(self, handle: int, obj: ParsedContent) -> None:
3✔
213
        """Stores *obj* under *handle* in the current handle table."""
214
        if handle in self._handles:
3✔
NEW
215
            raise ParseError(f"Duplicate handle 0x{handle:x}", self._reader.offset)
×
216
        self._handles[handle] = obj
3✔
217

218
    # ------------------------------------------------------------------
219
    # Content dispatcher
220
    # ------------------------------------------------------------------
221

222
    def _read_content(
3✔
223
        self,
224
        opcode: int,
225
        *,
226
        block_data_allowed: bool,
227
        class_desc: JavaClassDesc | None = None,
228
    ) -> ParsedContent:
229
        """
230
        Dispatches *opcode* to the appropriate ``_do_*`` method.
231

232
        :param opcode: The TC_* byte already read from the stream.
233
        :param block_data_allowed: Whether TC_BLOCKDATA records are valid here.
234
        :param class_desc: Optional context class (for WRCLASS custom reading).
235
        :raises SecurityError: If the maximum recursion depth is exceeded.
236
        :raises ParseError: On unrecognised opcode.
237
        """
238
        self._depth += 1
3✔
239
        if self._depth > self._max_depth:
3✔
240
            raise SecurityError(
3✔
241
                f"Maximum parse depth ({self._max_depth}) exceeded at offset 0x{self._reader.offset:x}"
242
            )
243
        try:
3✔
244
            match opcode:
3✔
245
                case TerminalCode.TC_NULL:
3✔
246
                    return None
3✔
247
                case TerminalCode.TC_OBJECT:
3✔
248
                    return self._do_object()
3✔
249
                case TerminalCode.TC_CLASS:
3✔
250
                    return self._do_class()
3✔
251
                case TerminalCode.TC_ARRAY:
3✔
252
                    return self._do_array()
3✔
253
                case (TerminalCode.TC_STRING | TerminalCode.TC_LONGSTRING) as str_code:
3✔
254
                    return self._do_string(str_code)
3✔
255
                case TerminalCode.TC_ENUM:
3✔
256
                    return self._do_enum()
3✔
257
                case (TerminalCode.TC_CLASSDESC | TerminalCode.TC_PROXYCLASSDESC) as cd_code:
3✔
NEW
258
                    return self._do_classdesc(cd_code)
×
259
                case TerminalCode.TC_REFERENCE:
3✔
260
                    return self._do_reference()
3✔
261
                case TerminalCode.TC_EXCEPTION:
3✔
NEW
262
                    return self._do_exception()
×
263
                case (TerminalCode.TC_BLOCKDATA | TerminalCode.TC_BLOCKDATALONG) as bd_code:
3✔
264
                    if not block_data_allowed:
3✔
NEW
265
                        raise ParseError(
×
266
                            "Unexpected TC_BLOCKDATA where not allowed",
267
                            self._reader.offset,
268
                        )
269
                    return self._do_block_data(bd_code)
3✔
270
                case _:
3✔
271
                    # Last resort: check whether a transformer can handle
272
                    # a custom writeObject for the active class_desc.
273
                    if (
3✔
274
                        class_desc is not None
275
                        and class_desc.name
276
                        and class_desc.data_type == ClassDataType.WRCLASS
277
                    ):
278
                        # Rewind one byte so the transformer sees the opcode.
NEW
279
                        self._fd.seek(-1, 1)
×
NEW
280
                        result = self._custom_read_object(class_desc.name)
×
NEW
281
                        if result is not None:
×
NEW
282
                            return result
×
283

284
                    raise ParseError(
3✔
285
                        f"Unknown opcode 0x{opcode:02x}",
286
                        self._reader.offset,
287
                    )
288
        finally:
289
            self._depth -= 1
3✔
290

291
    # ------------------------------------------------------------------
292
    # TC_OBJECT
293
    # ------------------------------------------------------------------
294

295
    def _do_object(self) -> JavaInstance:
3✔
296
        """Parses a TC_OBJECT record and returns a :class:`JavaInstance`."""
297
        classdesc = self._read_classdesc()
3✔
298

299
        handle = self._new_handle()
3✔
300
        _log.debug("TC_OBJECT handle=0x%x class=%s", handle, classdesc)
3✔
301

302
        instance = self._create_instance(classdesc)
3✔
303
        instance.classdesc = classdesc
3✔
304
        instance.handle = handle
3✔
305

306
        self._set_handle(handle, instance)
3✔
307
        self._read_class_data(instance)
3✔
308
        instance.load_from_instance()
3✔
309

310
        _log.debug("Done reading object handle=0x%x", handle)
3✔
311
        return instance
3✔
312

313
    def _create_instance(self, classdesc: JavaClassDesc | None) -> JavaInstance:
3✔
314
        """
315
        Tries each transformer in order; falls back to plain JavaInstance.
316
        """
317
        if classdesc is not None and classdesc.name:
3✔
318
            for t in self._transformers:
3✔
319
                inst = t.create_instance(classdesc)
3✔
320
                if inst is not None:
3✔
321
                    return inst
3✔
322
        return JavaInstance()
3✔
323

324
    # ------------------------------------------------------------------
325
    # TC_CLASS
326
    # ------------------------------------------------------------------
327

328
    def _do_class(self) -> JavaClass:
3✔
329
        """Parses a TC_CLASS record."""
330
        classdesc = self._read_classdesc()
3✔
331
        if classdesc is None:
3✔
NEW
332
            raise ParseError("TC_CLASS requires a non-null class descriptor", self._reader.offset)
×
333
        handle = self._new_handle()
3✔
334
        obj = JavaClass(handle=handle, classdesc=classdesc)
3✔
335
        self._set_handle(handle, obj)
3✔
336
        return obj
3✔
337

338
    # ------------------------------------------------------------------
339
    # TC_ARRAY
340
    # ------------------------------------------------------------------
341

342
    def _do_array(self) -> JavaArray:
3✔
343
        """Parses a TC_ARRAY record."""
344
        classdesc = self._read_classdesc()
3✔
345
        if classdesc is None:
3✔
NEW
346
            raise ParseError("TC_ARRAY requires a non-null class descriptor", self._reader.offset)
×
347
        handle = self._new_handle()
3✔
348

349
        name = classdesc.name or ""
3✔
350
        if len(name) < 2:
3✔
NEW
351
            raise ParseError(
×
352
                f"Array class desc has invalid name {name!r}",
353
                self._reader.offset,
354
            )
355

356
        # The second character of the class name encodes the element type.
357
        element_type_byte = ord(name[1].encode("latin-1"))
3✔
358
        try:
3✔
359
            element_type = FieldType(element_type_byte)
3✔
NEW
360
        except ValueError:
×
NEW
361
            raise ParseError(
×
362
                f"Unknown array element type byte 0x{element_type_byte:02x}",
363
                self._reader.offset,
364
            )
365

366
        size = self._reader.read_int()
3✔
367
        if size < 0:
3✔
NEW
368
            raise ParseError(f"Invalid array size {size}", self._reader.offset)
×
369

370
        # Try transformers first (e.g. NumpyArrayTransformer)
371
        type_code = TypeCode(element_type_byte)
3✔
372
        data: bytes | list[Any] | None = None
3✔
373
        for t in self._transformers:
3✔
374
            data = t.load_array(self._reader, type_code, size)
3✔
375
            if data is not None:
3✔
NEW
376
                break
×
377

378
        if data is None:
3✔
379
            if element_type == FieldType.BYTE:
3✔
380
                # Efficient bulk read for byte arrays
381
                data = self._reader.read_bytes(size)
3✔
382
            else:
383
                data = [self._read_field_value(element_type) for _ in range(size)]
3✔
384

385
        array = JavaArray(
3✔
386
            handle=handle,
387
            classdesc=classdesc,
388
            element_type=element_type,
389
            data=data,
390
        )
391
        self._set_handle(handle, array)
3✔
392
        return array
3✔
393

394
    # ------------------------------------------------------------------
395
    # TC_STRING / TC_LONGSTRING
396
    # ------------------------------------------------------------------
397

398
    def _do_string(self, opcode: int) -> JavaString:
3✔
399
        """Parses a TC_STRING or TC_LONGSTRING record."""
400
        handle = self._new_handle()
3✔
401

402
        if opcode == TerminalCode.TC_STRING:
3✔
403
            value = self._reader.read_utf()
3✔
NEW
404
        elif opcode == TerminalCode.TC_LONGSTRING:
×
NEW
405
            value = self._reader.read_long_utf()
×
406
        else:
NEW
407
            raise ParseError(
×
408
                f"Expected TC_STRING or TC_LONGSTRING, got 0x{opcode:02x}",
409
                self._reader.offset,
410
            )
411

412
        java_str = JavaString(handle=handle, value=value)
3✔
413
        self._set_handle(handle, java_str)
3✔
414
        return java_str
3✔
415

416
    # ------------------------------------------------------------------
417
    # Helper: read a string that may be TC_STRING, TC_LONGSTRING or TC_REFERENCE
418
    # ------------------------------------------------------------------
419

420
    def _read_new_string(self, opcode: int) -> JavaString:
3✔
421
        """
422
        Reads a string-valued token; handles TC_REFERENCE to an earlier string.
423
        """
424
        if opcode == TerminalCode.TC_REFERENCE:
3✔
425
            prev = self._do_reference()
3✔
426
            if not isinstance(prev, JavaString):
3✔
427
                raise ParseError(
3✔
428
                    "TC_REFERENCE in string context does not point to a string",
429
                    self._reader.offset,
430
                )
431
            return prev
3✔
432
        return self._do_string(opcode)
3✔
433

434
    # ------------------------------------------------------------------
435
    # TC_ENUM
436
    # ------------------------------------------------------------------
437

438
    def _do_enum(self) -> JavaEnum:
3✔
439
        """Parses a TC_ENUM record."""
440
        classdesc = self._read_classdesc()
3✔
441
        if classdesc is None:
3✔
NEW
442
            raise ParseError("TC_ENUM has null class descriptor", self._reader.offset)
×
443

444
        handle = self._new_handle()
3✔
445

446
        str_opcode = self._reader.read_ubyte()
3✔
447
        constant = self._read_new_string(str_opcode)
3✔
448
        if classdesc.name:
3✔
449
            classdesc.enum_constants.add(constant.value)
3✔
450

451
        enum_obj = JavaEnum(handle=handle, classdesc=classdesc, constant=constant)
3✔
452
        self._set_handle(handle, enum_obj)
3✔
453
        return enum_obj
3✔
454

455
    # ------------------------------------------------------------------
456
    # TC_CLASSDESC / TC_PROXYCLASSDESC
457
    # ------------------------------------------------------------------
458

459
    def _read_classdesc(self) -> JavaClassDesc | None:
3✔
460
        """
461
        Reads a type-code byte then delegates to :meth:`_do_classdesc`.
462
        Returns ``None`` for TC_NULL.
463
        """
464
        opcode = self._reader.read_ubyte()
3✔
465
        return self._do_classdesc(opcode)
3✔
466

467
    def _do_classdesc(self, opcode: int) -> JavaClassDesc | None:
3✔
468
        """Parses a class descriptor record identified by *opcode*."""
469
        if opcode == TerminalCode.TC_NULL:
3✔
470
            return None
3✔
471

472
        if opcode == TerminalCode.TC_REFERENCE:
3✔
473
            prev = self._do_reference()
3✔
474
            if not isinstance(prev, JavaClassDesc):
3✔
NEW
475
                raise ParseError(
×
476
                    "TC_REFERENCE in classdesc context does not point to a class descriptor",
477
                    self._reader.offset,
478
                )
479
            return prev
3✔
480

481
        if opcode == TerminalCode.TC_CLASSDESC:
3✔
482
            return self._read_normal_classdesc()
3✔
483

NEW
484
        if opcode == TerminalCode.TC_PROXYCLASSDESC:
×
NEW
485
            return self._read_proxy_classdesc()
×
486

NEW
487
        raise UnexpectedOpcodeError(
×
488
            (
489
                TerminalCode.TC_NULL,
490
                TerminalCode.TC_REFERENCE,
491
                TerminalCode.TC_CLASSDESC,
492
                TerminalCode.TC_PROXYCLASSDESC,
493
            ),
494
            opcode,
495
            self._reader.offset,
496
        )
497

498
    def _read_normal_classdesc(self) -> JavaClassDesc:
3✔
499
        """Parses a TC_CLASSDESC record (after the opcode byte)."""
500
        name = self._reader.read_utf()
3✔
501
        serial_version_uid = self._reader.read_long()
3✔
502
        handle = self._new_handle()
3✔
503
        desc_flags = self._reader.read_ubyte()
3✔
504

505
        nb_fields = self._reader.read_short()
3✔
506
        if nb_fields < 0:
3✔
NEW
507
            raise ParseError(f"Invalid field count {nb_fields}", self._reader.offset)
×
508

509
        fields: list[JavaField] = []
3✔
510
        for _ in range(nb_fields):
3✔
511
            field_type_byte = self._reader.read_ubyte()
3✔
512
            try:
3✔
513
                field_type = FieldType(field_type_byte)
3✔
NEW
514
            except ValueError:
×
NEW
515
                raise ParseError(
×
516
                    f"Unknown field type byte 0x{field_type_byte:02x}",
517
                    self._reader.offset,
518
                )
519
            field_name = self._reader.read_utf()
3✔
520
            class_name: str | None = None
3✔
521

522
            if field_type_byte in (TypeCode.TYPE_OBJECT, TypeCode.TYPE_ARRAY):
3✔
523
                str_opcode = self._reader.read_ubyte()
3✔
524
                class_name_str = self._read_new_string(str_opcode)
3✔
525
                class_name = class_name_str.value
3✔
526

527
            fields.append(JavaField(type=field_type, name=field_name, class_name=class_name))
3✔
528

529
        classdesc = JavaClassDesc(
3✔
530
            handle=handle,
531
            name=name,
532
            serial_version_uid=serial_version_uid,
533
            desc_flags=desc_flags,
534
            class_type=ClassDescType.NORMALCLASS,
535
            fields=fields,
536
        )
537
        self._set_handle(handle, classdesc)
3✔
538

539
        classdesc.annotations = self._read_class_annotations()
3✔
540
        classdesc.super_class = self._read_classdesc()
3✔
541
        if classdesc.super_class is not None:
3✔
542
            classdesc.super_class.is_super_class = True
3✔
543

544
        return classdesc
3✔
545

546
    def _read_proxy_classdesc(self) -> JavaClassDesc:
3✔
547
        """Parses a TC_PROXYCLASSDESC record (after the opcode byte)."""
NEW
548
        handle = self._new_handle()
×
NEW
549
        nb_interfaces = self._reader.read_int()
×
NEW
550
        interfaces = [self._reader.read_utf() for _ in range(nb_interfaces)]
×
551

552
        # Proxy classes are treated as Serializable with SC_SERIALIZABLE flag.
NEW
553
        desc_flags = ClassDescFlags.SC_SERIALIZABLE | ClassDescFlags.SC_WRITE_METHOD
×
NEW
554
        classdesc = JavaClassDesc(
×
555
            handle=handle,
556
            name="",
557
            serial_version_uid=0,
558
            desc_flags=int(desc_flags),
559
            class_type=ClassDescType.PROXYCLASS,
560
            interfaces=interfaces,
561
        )
NEW
562
        self._set_handle(handle, classdesc)
×
563

NEW
564
        classdesc.annotations = self._read_class_annotations()
×
NEW
565
        classdesc.super_class = self._read_classdesc()
×
NEW
566
        if classdesc.super_class is not None:
×
NEW
567
            classdesc.super_class.is_super_class = True
×
568

NEW
569
        return classdesc
×
570

571
    # ------------------------------------------------------------------
572
    # TC_REFERENCE
573
    # ------------------------------------------------------------------
574

575
    def _do_reference(self) -> ParsedContent:
3✔
576
        """Resolves a TC_REFERENCE to a previously parsed object."""
577
        handle = self._reader.read_int()
3✔
578
        try:
3✔
579
            return self._handles[handle]
3✔
NEW
580
        except KeyError:
×
NEW
581
            raise ParseError(f"Invalid handle 0x{handle:x}", self._reader.offset)
×
582

583
    # ------------------------------------------------------------------
584
    # TC_EXCEPTION
585
    # ------------------------------------------------------------------
586

587
    def _do_exception(self) -> JavaInstance:
3✔
588
        """
589
        Reads a TC_EXCEPTION record.
590

591
        The stream resets its state, parses one object (the exception), then
592
        resets again.
593
        """
NEW
594
        self._reset()
×
NEW
595
        opcode = self._reader.read_ubyte()
×
NEW
596
        if opcode == TerminalCode.TC_RESET:
×
NEW
597
            raise ParseError(
×
598
                "TC_RESET encountered while reading a TC_EXCEPTION",
599
                self._reader.offset,
600
            )
601

NEW
602
        content = self._read_content(opcode, block_data_allowed=False)
×
NEW
603
        if content is None:
×
NEW
604
            raise ParseError("TC_EXCEPTION contains a null object", self._reader.offset)
×
NEW
605
        if not isinstance(content, JavaInstance):
×
NEW
606
            raise ParseError("TC_EXCEPTION object is not a Java instance", self._reader.offset)
×
NEW
607
        content.is_exception = True
×
NEW
608
        self._reset()
×
NEW
609
        return content
×
610

611
    # ------------------------------------------------------------------
612
    # TC_BLOCKDATA / TC_BLOCKDATALONG
613
    # ------------------------------------------------------------------
614

615
    def _do_block_data(self, opcode: int) -> BlockData:
3✔
616
        """Reads a TC_BLOCKDATA or TC_BLOCKDATALONG record."""
617
        if opcode == TerminalCode.TC_BLOCKDATA:
3✔
618
            size = self._reader.read_ubyte()
3✔
NEW
619
        elif opcode == TerminalCode.TC_BLOCKDATALONG:
×
NEW
620
            size = self._reader.read_int()
×
621
        else:
NEW
622
            raise ParseError(
×
623
                f"Expected block-data opcode, got 0x{opcode:02x}",
624
                self._reader.offset,
625
            )
626

627
        if size < 0:
3✔
NEW
628
            raise ParseError(f"Invalid block data size {size}", self._reader.offset)
×
629

630
        data = self._reader.read_bytes(size)
3✔
631
        return BlockData(data=data)
3✔
632

633
    # ------------------------------------------------------------------
634
    # Class annotations (written by writeObject / annotateClass)
635
    # ------------------------------------------------------------------
636

637
    def _read_class_annotations(self, class_desc: JavaClassDesc | None = None) -> list[ParsedContent]:
3✔
638
        """
639
        Reads annotation objects until TC_ENDBLOCKDATA is encountered.
640

641
        :param class_desc: Optional context used for WRCLASS custom readers.
642
        :return: List of annotation content items (may be empty).
643
        """
644
        items: list[ParsedContent] = []
3✔
645
        while True:
3✔
646
            opcode = self._reader.read_ubyte()
3✔
647

648
            if opcode == TerminalCode.TC_ENDBLOCKDATA:
3✔
649
                return items
3✔
650

651
            if opcode == TerminalCode.TC_RESET:
3✔
NEW
652
                self._reset()
×
NEW
653
                continue
×
654

655
            try:
3✔
656
                item = self._read_content(
3✔
657
                    opcode,
658
                    block_data_allowed=True,
659
                    class_desc=class_desc,
660
                )
661
            except _ExceptionRead as exc:
3✔
NEW
662
                raise _ExceptionRead(exc.exception_object) from None
×
663

664
            if isinstance(item, JavaInstance) and item.is_exception:
3✔
NEW
665
                raise _ExceptionRead(item)
×
666

667
            items.append(item)
3✔
668

669
    # ------------------------------------------------------------------
670
    # Instance data (classdata)
671
    # ------------------------------------------------------------------
672

673
    def _read_class_data(self, instance: JavaInstance) -> None:
3✔
674
        """
675
        Reads all field data and annotations for *instance* according to
676
        its class hierarchy.
677
        """
678
        if instance.classdesc is None:
3✔
NEW
679
            return
×
680

681
        hierarchy = instance.classdesc.get_hierarchy()
3✔
682
        field_data: dict[JavaClassDesc, dict[JavaField, Any]] = {}
3✔
683
        annotations: dict[JavaClassDesc, list[ParsedContent]] = {}
3✔
684

685
        for cd in hierarchy:
3✔
686
            values: dict[JavaField, Any] = {}
3✔
687

688
            try:
3✔
689
                data_type = cd.data_type
3✔
NEW
690
            except ValueError:
×
691
                # Skip class descs with no serializable/externalizable flags
692
                # (e.g. proxy classes that appear in super-class chains).
NEW
693
                continue
×
694

695
            match data_type:
3✔
696
                case ClassDataType.NOWRCLASS:
3✔
697
                    for f in cd.fields:
3✔
698
                        values[f] = self._read_field_value(f.type)
3✔
699
                    field_data[cd] = values
3✔
700

701
                case ClassDataType.WRCLASS:
3✔
702
                    # Read the default serializable fields first …
703
                    for f in cd.fields:
3✔
704
                        values[f] = self._read_field_value(f.type)
3✔
705
                    field_data[cd] = values
3✔
706
                    # … then read the custom writeObject annotation block.
707
                    # load_from_instance() on the JavaInstance (or transformer
708
                    # subclass) will process these annotations afterwards.
709
                    annotations[cd] = self._read_class_annotations(cd)
3✔
710

711
                case ClassDataType.OBJECT_ANNOTATION:
3✔
712
                    # SC_EXTERNALIZABLE + SC_BLOCK_DATA
713
                    if not instance.load_from_blockdata(self, self._reader):
3✔
NEW
714
                        raise ParseError(
×
715
                            f"Externalizable class {cd.name!r} with "
716
                            "SC_BLOCK_DATA cannot be parsed: no transformer "
717
                            "handled load_from_blockdata()",
718
                            self._reader.offset,
719
                        )
720
                    annotations[cd] = self._read_class_annotations(cd)
3✔
721

NEW
722
                case ClassDataType.EXTERNAL_CONTENTS:
×
723
                    # SC_EXTERNALIZABLE without SC_BLOCK_DATA (Protocol v1).
NEW
724
                    raise UnsupportedFeatureError(
×
725
                        f"SC_EXTERNALIZABLE without SC_BLOCK_DATA "
726
                        f"(Protocol v1) is not supported for class "
727
                        f"{cd.name!r}. "
728
                        "This stream was likely produced with an old JDK."
729
                    )
730

731
        instance.field_data = field_data
3✔
732
        instance.annotations = annotations
3✔
733

734
    def _is_default_supported(self, class_name: str) -> bool:
3✔
735
        """
736
        Returns ``True`` when the :class:`DefaultObjectTransformer` (if
737
        present) recognises *class_name*.
738
        """
NEW
739
        for t in self._transformers:
×
NEW
740
            if isinstance(t, DefaultObjectTransformer):
×
NEW
741
                return t.handles(class_name)
×
NEW
742
        return False
×
743

744
    # ------------------------------------------------------------------
745
    # Field value reader
746
    # ------------------------------------------------------------------
747

748
    def _read_field_value(self, field_type: FieldType) -> Any:
3✔
749
        """Reads and returns a single field value of the given type."""
750
        match field_type:
3✔
751
            case FieldType.BYTE:
3✔
NEW
752
                return self._reader.read_byte()
×
753
            case FieldType.CHAR:
3✔
754
                return self._reader.read_char()
3✔
755
            case FieldType.DOUBLE:
3✔
NEW
756
                return self._reader.read_double()
×
757
            case FieldType.FLOAT:
3✔
758
                return self._reader.read_float()
3✔
759
            case FieldType.INTEGER:
3✔
760
                return self._reader.read_int()
3✔
761
            case FieldType.LONG:
3✔
NEW
762
                return self._reader.read_long()
×
763
            case FieldType.SHORT:
3✔
NEW
764
                return self._reader.read_short()
×
765
            case FieldType.BOOLEAN:
3✔
766
                return self._reader.read_bool()
3✔
767
            case FieldType.OBJECT | FieldType.ARRAY as obj_type:
3✔
768
                sub_opcode = self._reader.read_ubyte()
3✔
769

770
                if obj_type == FieldType.ARRAY:
3✔
771
                    if sub_opcode == TerminalCode.TC_NULL:
3✔
NEW
772
                        return None
×
773
                    if sub_opcode == TerminalCode.TC_REFERENCE:
3✔
NEW
774
                        return self._do_reference()
×
775
                    if sub_opcode != TerminalCode.TC_ARRAY:
3✔
NEW
776
                        raise ParseError(
×
777
                            f"Expected TC_ARRAY for array field, got 0x{sub_opcode:02x}",
778
                            self._reader.offset,
779
                        )
780

781
                content = self._read_content(sub_opcode, block_data_allowed=False)
3✔
782
                if isinstance(content, JavaInstance) and content.is_exception:
3✔
NEW
783
                    raise _ExceptionRead(content)
×
784
                return content
3✔
785

NEW
786
        raise ParseError(
×
787
            f"Cannot read field of unknown type {field_type!r}",
788
            self._reader.offset,
789
        )
790

791
    # ------------------------------------------------------------------
792
    # Custom writeObject dispatcher
793
    # ------------------------------------------------------------------
794

795
    def _custom_read_object(self, class_name: str) -> Any | None:
3✔
796
        """
797
        Tries each transformer's ``load_custom_writeObject`` for *class_name*.
798
        Returns ``None`` if no transformer handles it.
799
        """
NEW
800
        for t in self._transformers:
×
NEW
801
            result = t.load_custom_writeObject(self, self._reader, class_name)
×
NEW
802
            if result is not None:
×
NEW
803
                return result
×
NEW
804
        return None
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc