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

tcalmant / python-javaobj / 26725838468

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

Pull #63

github

web-flow
Merge ad1ebc8ca into 519fc2167
Pull Request #63: Addition of a v3 package

808 of 1023 new or added lines in 7 files covered. (78.98%)

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

83.17
/javaobj/v3/beans.py
1
#!/usr/bin/env python3
2
"""
3
Definition of the beans used to represent the parsed objects (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 warnings
3✔
29
from dataclasses import dataclass, field
3✔
30
from enum import IntEnum
3✔
31
from typing import Any
3✔
32

33
# Javaobj
34
from ..constants import ClassDescFlags, TypeCode
3✔
35

36
# ------------------------------------------------------------------------------
37

38
# Module version
39
__version_info__ = (0, 5, 0)
3✔
40
__version__ = ".".join(str(x) for x in __version_info__)
3✔
41

42
# Documentation strings format
43
__docformat__ = "restructuredtext en"
3✔
44

45
# ------------------------------------------------------------------------------
46

47
__all__ = [
3✔
48
    "FieldType",
49
    "ClassDescType",
50
    "ClassDataType",
51
    "JavaField",
52
    "JavaClassDesc",
53
    "JavaInstance",
54
    "JavaArray",
55
    "JavaString",
56
    "JavaEnum",
57
    "JavaClass",
58
    "BlockData",
59
    "ExceptionState",
60
    "ParsedContent",
61
]
62

63

64
# ------------------------------------------------------------------------------
65
# Enumerations
66
# ------------------------------------------------------------------------------
67

68

69
class FieldType(IntEnum):
3✔
70
    """
71
    Java type codes as used in class-descriptor field entries.
72

73
    Values match the single-character ASCII type codes defined by the
74
    Java Object Serialization Protocol (e.g. ``B`` → byte, ``I`` → int …).
75
    """
76

77
    BYTE = TypeCode.TYPE_BYTE.value  # 'B' – signed byte
3✔
78
    CHAR = TypeCode.TYPE_CHAR.value  # 'C' – UTF-16 code unit (2 bytes)
3✔
79
    DOUBLE = TypeCode.TYPE_DOUBLE.value  # 'D' – IEEE-754 double
3✔
80
    FLOAT = TypeCode.TYPE_FLOAT.value  # 'F' – IEEE-754 float
3✔
81
    INTEGER = TypeCode.TYPE_INTEGER.value  # 'I' – 32-bit signed int
3✔
82
    LONG = TypeCode.TYPE_LONG.value  # 'J' – 64-bit signed long
3✔
83
    SHORT = TypeCode.TYPE_SHORT.value  # 'S' – 16-bit signed short
3✔
84
    BOOLEAN = TypeCode.TYPE_BOOLEAN.value  # 'Z' – boolean
3✔
85
    ARRAY = TypeCode.TYPE_ARRAY.value  # '[' – array reference
3✔
86
    OBJECT = TypeCode.TYPE_OBJECT.value  # 'L' – object reference
3✔
87

88

89
class ClassDescType(IntEnum):
3✔
90
    """Whether a class descriptor represents a normal class or a proxy."""
91

92
    NORMALCLASS = 0
3✔
93
    PROXYCLASS = 1
3✔
94

95

96
class ClassDataType(IntEnum):
3✔
97
    """
98
    How an instance's data is laid out in the stream.
99

100
    Derived from the ``desc_flags`` byte of its :class:`JavaClassDesc`.
101
    """
102

103
    NOWRCLASS = 0  # SC_SERIALIZABLE, no writeObject
3✔
104
    WRCLASS = 1  # SC_SERIALIZABLE + SC_WRITE_METHOD
3✔
105
    EXTERNAL_CONTENTS = 2  # SC_EXTERNALIZABLE, no SC_BLOCK_DATA
3✔
106
    OBJECT_ANNOTATION = 3  # SC_EXTERNALIZABLE + SC_BLOCK_DATA
3✔
107

108

109
# ------------------------------------------------------------------------------
110
# Field descriptor
111
# ------------------------------------------------------------------------------
112

113

114
@dataclass(slots=True, eq=False)
3✔
115
class JavaField:
3✔
116
    """
117
    A single field entry in a :class:`JavaClassDesc`.
118

119
    Equality and hashing use **object identity** (like plain Python classes)
120
    so that ``JavaField`` instances can be used as dict keys and compared
121
    across the same parsing session.
122
    """
123

124
    type: FieldType
2✔
125
    name: str
2✔
126
    # For OBJECT / ARRAY fields this holds the binary class name
127
    # (e.g. ``Ljava/lang/String;`` or ``[B``).
128
    class_name: str | None = None
3✔
129

130

131
# ------------------------------------------------------------------------------
132
# Class descriptor
133
# ------------------------------------------------------------------------------
134

135

136
@dataclass(slots=True, eq=False)
3✔
137
class JavaClassDesc:
3✔
138
    """
139
    Full description of a Java class as parsed from a TC_CLASSDESC or
140
    TC_PROXYCLASSDESC record.
141

142
    Equality and hashing use **object identity** so that ``JavaClassDesc``
143
    instances can be used as dict keys when building ``field_data`` and
144
    ``annotations`` maps.
145
    """
146

147
    handle: int
2✔
148
    name: str
2✔
149
    serial_version_uid: int
2✔
150
    desc_flags: int
2✔
151
    class_type: ClassDescType = ClassDescType.NORMALCLASS
3✔
152
    fields: list[JavaField] = field(default_factory=list)
3✔
153
    super_class: "JavaClassDesc | None" = None
3✔
154
    # Interface names (only for proxy classes)
155
    interfaces: list[str] = field(default_factory=list)
3✔
156
    # Class annotations (blockdata / objects written by annotateClass)
157
    annotations: list[Any] = field(default_factory=list)
3✔
158
    # Enum constant names observed in this stream
159
    enum_constants: set[str] = field(default_factory=set)
3✔
160
    # True when this descriptor is a super-class of another descriptor
161
    is_super_class: bool = False
3✔
162

163
    # ------------------------------------------------------------------
164
    # v1 / v2 compatibility aliases
165
    # ------------------------------------------------------------------
166

167
    @property
3✔
168
    def serialVersionUID(self) -> int:
3✔
169
        """Alias for ``serial_version_uid`` (v1/v2 API compatibility)."""
170
        return self.serial_version_uid
3✔
171

172
    @property
3✔
173
    def flags(self) -> int:
3✔
174
        """Alias for ``desc_flags`` (v1/v2 API compatibility)."""
175
        return self.desc_flags
3✔
176

177
    @property
3✔
178
    def fields_names(self) -> list[str]:
3✔
179
        """Returns the ordered list of field names."""
180
        return [f.name for f in self.fields]
3✔
181

182
    @property
3✔
183
    def fields_types(self) -> list[FieldType]:
3✔
184
        """Returns the ordered list of field types."""
185
        return [f.type for f in self.fields]
3✔
186

187
    # ------------------------------------------------------------------
188
    # Computed properties
189
    # ------------------------------------------------------------------
190

191
    @property
3✔
192
    def data_type(self) -> ClassDataType:
3✔
193
        """
194
        Derives the :class:`ClassDataType` from the descriptor flags.
195

196
        :raises ValueError: If the flags combination is unsupported.
197
        """
198
        if ClassDescFlags.SC_SERIALIZABLE & self.desc_flags:
3✔
199
            return (
3✔
200
                ClassDataType.WRCLASS
201
                if (ClassDescFlags.SC_WRITE_METHOD & self.desc_flags)
202
                else ClassDataType.NOWRCLASS
203
            )
204
        if ClassDescFlags.SC_EXTERNALIZABLE & self.desc_flags:
3✔
205
            return (
3✔
206
                ClassDataType.OBJECT_ANNOTATION
207
                if (ClassDescFlags.SC_BLOCK_DATA & self.desc_flags)
208
                else ClassDataType.EXTERNAL_CONTENTS
209
            )
NEW
210
        raise ValueError(f"Cannot derive data type from desc_flags 0x{self.desc_flags:02x}")
×
211

212
    def get_hierarchy(self) -> "list[JavaClassDesc]":
3✔
213
        """
214
        Returns the class hierarchy from the topmost ancestor to ``self``,
215
        in the order used by the Java serialization protocol.
216
        """
217
        classes: list[JavaClassDesc] = []
3✔
218
        if self.super_class is not None:
3✔
219
            classes.extend(self.super_class.get_hierarchy())
3✔
220
        classes.append(self)
3✔
221
        return classes
3✔
222

223
    def validate(self) -> None:
3✔
224
        """
225
        Checks that the descriptor is internally consistent.
226

227
        :raises ValueError: If the descriptor is malformed.
228
        """
NEW
229
        serial_or_extern = ClassDescFlags.SC_SERIALIZABLE | ClassDescFlags.SC_EXTERNALIZABLE
×
NEW
230
        if (self.desc_flags & serial_or_extern) == 0 and self.fields:
×
NEW
231
            raise ValueError("Non-serializable, non-externalizable class has fields")
×
NEW
232
        if (self.desc_flags & serial_or_extern) == serial_or_extern:
×
NEW
233
            raise ValueError("Class is both serializable and externalizable")
×
NEW
234
        if self.desc_flags & ClassDescFlags.SC_ENUM:
×
NEW
235
            if self.fields or self.interfaces:
×
NEW
236
                raise ValueError("Enum class must not have non-constant fields or interfaces")
×
237
        else:
NEW
238
            if self.enum_constants:
×
NEW
239
                raise ValueError("Non-enum class must not have enum constants")
×
240

241
    def __str__(self) -> str:
3✔
NEW
242
        return f"[classdesc 0x{self.handle:x}: name={self.name!r}, uid={self.serial_version_uid}]"
×
243

244
    __repr__ = __str__
3✔
245

246

247
# ------------------------------------------------------------------------------
248
# Instance
249
# ------------------------------------------------------------------------------
250

251

252
@dataclass
3✔
253
class JavaInstance:
3✔
254
    """
255
    A deserialized Java object instance (TC_OBJECT).
256

257
    ``field_data`` maps each :class:`JavaClassDesc` in the class hierarchy to
258
    a ``{JavaField: value}`` dict.  ``annotations`` maps each class descriptor
259
    to the list of :data:`ParsedContent` items written by ``writeObject``.
260

261
    .. note::
262
        This class intentionally does **not** use ``slots=True`` so that
263
        transformer subclasses can use multiple inheritance with built-in
264
        types such as :class:`list`, :class:`dict`, or :class:`set`.
265
        All fields have defaults so that ``JavaInstance()`` can be called
266
        with no arguments during construction (the parser sets them after).
267
    """
268

269
    handle: int = 0
3✔
270
    classdesc: JavaClassDesc | None = None  # set by the parser after creation
3✔
271
    field_data: dict[JavaClassDesc, dict[JavaField, Any]] = field(default_factory=dict)
3✔
272
    annotations: dict[JavaClassDesc, list[Any]] = field(default_factory=dict)
3✔
273
    is_exception: bool = False
3✔
274

275
    # ------------------------------------------------------------------
276
    # Field access helpers
277
    # ------------------------------------------------------------------
278

279
    def get_field(
3✔
280
        self,
281
        name: str,
282
        class_desc: JavaClassDesc | None = None,
283
    ) -> Any:
284
        """
285
        Returns the value of a field by name.
286

287
        If *class_desc* is provided the search is restricted to that class,
288
        which avoids the ambiguity that can arise when two classes in the
289
        hierarchy declare a field with the same name.
290

291
        :raises AttributeError: If the field is not found.
292
        """
293
        search = {class_desc: self.field_data[class_desc]} if class_desc is not None else self.field_data
3✔
294
        for cd_fields in search.values():
3✔
295
            for f, v in cd_fields.items():
3✔
296
                if f.name == name:
3✔
297
                    return v
3✔
NEW
298
        raise AttributeError(name)
×
299

300
    def __getattr__(self, name: str) -> Any:
3✔
301
        """
302
        Flat attribute access to instance fields (v1/v2 API compatibility).
303

304
        When multiple classes in the hierarchy define a field with the same
305
        name, a :class:`UserWarning` is emitted and the first match is
306
        returned.  Use :meth:`get_field` with an explicit *class_desc* to
307
        resolve ambiguity.
308
        """
309
        # Note: __getattr__ is only called when normal attribute lookup fails,
310
        # so there is no risk of infinite recursion here.
311
        matches: list[Any] = [
3✔
312
            v for cd_fields in self.field_data.values() for f, v in cd_fields.items() if f.name == name
313
        ]
314
        if len(matches) == 1:
3✔
315
            return matches[0]
3✔
NEW
316
        if len(matches) > 1:
×
NEW
317
            warnings.warn(
×
318
                f"Ambiguous field '{name}': found in {len(matches)} classes "
319
                "in the hierarchy. Use get_field(name, class_desc) for "
320
                "unambiguous access.",
321
                stacklevel=2,
322
            )
NEW
323
            return matches[0]
×
NEW
324
        raise AttributeError(name)
×
325

326
    def get_class(self) -> JavaClassDesc | None:
3✔
327
        """Returns the class descriptor of this instance."""
328
        return self.classdesc
3✔
329

330
    def load_from_instance(self) -> bool:
3✔
331
        """
332
        Post-processing hook called after parsing.
333

334
        Transformer subclasses can override this to convert parsed field data
335
        into a more convenient Python representation.
336

337
        :return: ``True`` if post-processing succeeded, ``False`` otherwise.
338
        """
339
        return False
3✔
340

341
    def load_from_blockdata(self, parser: Any, reader: Any) -> bool:
3✔
342
        """
343
        Hook for ``SC_EXTERNALIZABLE + SC_BLOCK_DATA`` classes.
344

345
        Transformer subclasses should override this to decode the raw block
346
        data written by the Java ``writeExternal`` method.
347

348
        :return: ``True`` if decoding succeeded, ``False`` otherwise.
349
        """
NEW
350
        return False
×
351

352
    def __str__(self) -> str:
3✔
NEW
353
        name = self.classdesc.name if self.classdesc else "<no class>"
×
NEW
354
        return f"[instance 0x{self.handle:x}: type={name!r}]"
×
355

356
    __repr__ = __str__
3✔
357

358

359
# ------------------------------------------------------------------------------
360
# Array
361
# ------------------------------------------------------------------------------
362

363

364
@dataclass(slots=True)
3✔
365
class JavaArray:
3✔
366
    """
367
    A deserialized Java array (TC_ARRAY).
368

369
    For ``TYPE_BYTE`` arrays ``data`` holds a :class:`bytes` object.
370
    For all other element types ``data`` is a :class:`list`.
371
    """
372

373
    handle: int
2✔
374
    classdesc: JavaClassDesc
2✔
375
    element_type: FieldType
2✔
376
    data: bytes | list[Any]
2✔
377

378
    def __len__(self) -> int:
3✔
379
        return len(self.data)
3✔
380

381
    def __iter__(self):
3✔
382
        return iter(self.data)
3✔
383

384
    def __getitem__(self, idx: int) -> Any:
3✔
385
        return self.data[idx]  # type: ignore[index]
3✔
386

387
    def __str__(self) -> str:
3✔
NEW
388
        return f"[array 0x{self.handle:x}: type={self.element_type.name}, len={len(self.data)}]"
×
389

390
    __repr__ = __str__
3✔
391

392

393
# ------------------------------------------------------------------------------
394
# String
395
# ------------------------------------------------------------------------------
396

397

398
@dataclass(slots=True)
3✔
399
class JavaString:
3✔
400
    """A Java string decoded from TC_STRING or TC_LONGSTRING."""
401

402
    handle: int
2✔
403
    value: str
2✔
404

405
    def __str__(self) -> str:
3✔
406
        return self.value
3✔
407

408
    def __repr__(self) -> str:
3✔
NEW
409
        return repr(self.value)
×
410

411
    def __hash__(self) -> int:
3✔
412
        return hash(self.value)
3✔
413

414
    def __eq__(self, other: object) -> bool:
3✔
415
        if isinstance(other, JavaString):
3✔
416
            return self.value == other.value
3✔
417
        if isinstance(other, str):
3✔
418
            return self.value == other
3✔
NEW
419
        return NotImplemented
×
420

421

422
# ------------------------------------------------------------------------------
423
# Enum
424
# ------------------------------------------------------------------------------
425

426

427
@dataclass(slots=True)
3✔
428
class JavaEnum:
3✔
429
    """A Java enum constant (TC_ENUM)."""
430

431
    handle: int
2✔
432
    classdesc: JavaClassDesc
2✔
433
    constant: JavaString
2✔
434

435
    @property
3✔
436
    def name(self) -> str:
3✔
437
        """The binary class name of the enum type."""
NEW
438
        return self.classdesc.name
×
439

440
    def __str__(self) -> str:
3✔
NEW
441
        return f"[enum {self.classdesc.name}.{self.constant.value}]"
×
442

443
    __repr__ = __str__
3✔
444

445
    def __hash__(self) -> int:
3✔
NEW
446
        return hash((self.classdesc.name, self.constant.value))
×
447

448
    def __eq__(self, other: object) -> bool:
3✔
NEW
449
        if isinstance(other, JavaEnum):
×
NEW
450
            return self.classdesc.name == other.classdesc.name and self.constant.value == other.constant.value
×
NEW
451
        return NotImplemented
×
452

453

454
# ------------------------------------------------------------------------------
455
# Class reference
456
# ------------------------------------------------------------------------------
457

458

459
@dataclass(slots=True)
3✔
460
class JavaClass:
3✔
461
    """Represents a ``java.lang.Class`` token (TC_CLASS)."""
462

463
    handle: int
2✔
464
    classdesc: JavaClassDesc
2✔
465

466
    @property
3✔
467
    def name(self) -> str:
3✔
468
        """The binary name of the represented class."""
469
        return self.classdesc.name
3✔
470

471
    def __str__(self) -> str:
3✔
NEW
472
        return f"[class {self.classdesc.name!r}]"
×
473

474
    __repr__ = __str__
3✔
475

476

477
# ------------------------------------------------------------------------------
478
# Block data
479
# ------------------------------------------------------------------------------
480

481

482
@dataclass(slots=True, eq=False)
3✔
483
class BlockData:
3✔
484
    """Raw bytes from a TC_BLOCKDATA / TC_BLOCKDATALONG record."""
485

486
    data: bytes
2✔
487
    handle: int = 0
3✔
488

489
    def __eq__(self, other: object) -> bool:
3✔
490
        """
491
        Compares block data with other ``BlockData`` instances or with
492
        ``bytes`` / ``str`` directly (v1/v2 API compatibility).
493
        """
494
        if isinstance(other, BlockData):
3✔
495
            return self.data == other.data
3✔
496
        if isinstance(other, (bytes, bytearray)):
3✔
497
            return self.data == bytes(other)
3✔
498
        if isinstance(other, str):
3✔
499
            return self.data == other.encode("latin-1")
3✔
NEW
500
        return NotImplemented
×
501

502
    def __hash__(self) -> int:
3✔
NEW
503
        return hash(self.data)
×
504

505
    def __str__(self) -> str:
3✔
NEW
506
        return f"[blockdata len={len(self.data)}]"
×
507

508
    __repr__ = __str__
3✔
509

510

511
# ------------------------------------------------------------------------------
512
# Exception state
513
# ------------------------------------------------------------------------------
514

515

516
@dataclass(slots=True)
3✔
517
class ExceptionState:
3✔
518
    """
519
    Wrapper produced when a TC_EXCEPTION record is encountered.
520

521
    The ``exception_object`` holds the parsed Java exception instance and
522
    ``stream_data`` preserves the raw bytes for diagnostic purposes.
523
    """
524

525
    exception_object: JavaInstance
2✔
526
    stream_data: bytes
2✔
527
    handle: int = 0
3✔
528
    is_exception: bool = True
3✔
529

530
    def __str__(self) -> str:
3✔
NEW
531
        return f"[ExceptionState 0x{self.handle:x}]"
×
532

533
    __repr__ = __str__
3✔
534

535

536
# ------------------------------------------------------------------------------
537
# Union type alias
538
# ------------------------------------------------------------------------------
539

540
type ParsedContent = (
3✔
541
    JavaInstance
542
    | JavaArray
543
    | JavaString
544
    | JavaEnum
545
    | JavaClass
546
    | JavaClassDesc
547
    | BlockData
548
    | ExceptionState
549
    | None
550
)
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