Source code for jawa.cf

"""
ClassFile reader & writer.

The :mod:`jawa.cf` module provides tools for working with JVM ``.class``
ClassFiles.
"""
from typing import IO, Iterable, Union, Sequence
from struct import pack, unpack
from collections import namedtuple

from jawa.constants import ConstantPool, ConstantClass
from jawa.fields import FieldTable
from jawa.methods import MethodTable
from jawa.attribute import AttributeTable, ATTRIBUTE_CLASSES
from jawa.util.flags import Flags
from jawa.attributes.bootstrap import BootstrapMethod


[docs]class ClassVersion(namedtuple('ClassVersion', ['major', 'minor'])): """ClassFile file format version.""" __slots__ = () @property def human(self) -> str: """ A human-readable string identifying this version. If the version is unknown, `None` is returned instead. """ return { 0x33: 'J2SE_7', 0x32: 'J2SE_6', 0x31: 'J2SE_5', 0x30: 'JDK1_4', 0x2F: 'JDK1_3', 0x2E: 'JDK1_2', 0x2D: 'JDK1_1', }.get(self.major, None)
[docs]class ClassFile(object): """ Implements the JVM ClassFile (files typically ending in ``.class``). To open an existing ClassFile:: >>> with open('HelloWorld.class', 'rb') as fin: ... cf = ClassFile(fin) To save a newly created or modified ClassFile:: >>> cf = ClassFile.create('HelloWorld') >>> with open('HelloWorld.class', 'wb') as out: ... cf.save(out) :meth:`~ClassFile.create` sets up some reasonable defaults equivalent to: .. code-block:: java public class HelloWorld extends java.lang.Object{ } :param source: any file-like object providing ``.read()``. """ #: The JVM ClassFile magic number. MAGIC = 0xCAFEBABE def __init__(self, source: IO=None): # Default to J2SE_7 self._version = ClassVersion(0x32, 0) self._constants = ConstantPool() self.access_flags = Flags('>H', { 'acc_public': 0x0001, 'acc_final': 0x0010, 'acc_super': 0x0020, 'acc_interface': 0x0200, 'acc_abstract': 0x0400, 'acc_synthetic': 0x1000, 'acc_annotation': 0x2000, 'acc_enum': 0x4000 }) self._this = 0 self._super = 0 self._interfaces = [] self.fields = FieldTable(self) self.methods = MethodTable(self) self.attributes = AttributeTable(self) #: The ClassLoader bound to this ClassFile, if any. self.classloader = None if source: self._from_io(source)
[docs] @classmethod def create(cls, this: str, super_: str=u'java/lang/Object') -> 'ClassFile': """ A utility which sets up reasonable defaults for a new public class. :param this: The name of this class. :param super_: The name of this class's superclass. """ cf = ClassFile() cf.access_flags.acc_public = True cf.access_flags.acc_super = True cf.this = cf.constants.create_class(this) cf.super_ = cf.constants.create_class(super_) return cf
[docs] def save(self, source: IO): """ Saves the class to the file-like object `source`. :param source: Any file-like object providing write(). """ write = source.write write(pack( '>IHH', ClassFile.MAGIC, self.version.minor, self.version.major )) self._constants.pack(source) write(self.access_flags.pack()) write(pack( f'>HHH{len(self._interfaces)}H', self._this, self._super, len(self._interfaces), *self._interfaces )) self.fields.pack(source) self.methods.pack(source) self.attributes.pack(source)
def _from_io(self, source: IO): """ Loads an existing JVM ClassFile from any file-like object. """ read = source.read if unpack('>I', source.read(4))[0] != ClassFile.MAGIC: raise ValueError('invalid magic number') # The version is swapped on disk to (minor, major), so swap it back. self.version = unpack('>HH', source.read(4))[::-1] self._constants.unpack(source) # ClassFile access_flags, see section #4.1 of the JVM specs. self.access_flags.unpack(read(2)) # The CONSTANT_Class indexes for "this" class and its superclass. # Interfaces are a simple list of CONSTANT_Class indexes. self._this, self._super, interfaces_count = unpack('>HHH', read(6)) self._interfaces = unpack( f'>{interfaces_count}H', read(2 * interfaces_count) ) self.fields.unpack(source) self.methods.unpack(source) self.attributes.unpack(source) @property def version(self) -> ClassVersion: """ The :class:`~jawa.cf.ClassVersion` for this class. Example:: >>> cf = ClassFile.create('HelloWorld') >>> cf.version = 51, 0 >>> print(cf.version) ClassVersion(major=51, minor=0) >>> print(cf.version.major) 51 """ return self._version @version.setter def version(self, major_minor: Union[ClassVersion, Sequence]): self._version = ClassVersion(*major_minor) @property def constants(self) -> ConstantPool: """ The :class:`~jawa.cp.ConstantPool` for this class. """ return self._constants @property def this(self) -> ConstantClass: """ The :class:`~jawa.constants.ConstantClass` which represents this class. """ return self.constants.get(self._this) @this.setter def this(self, value): self._this = value.index @property def super_(self) -> ConstantClass: """ The :class:`~jawa.constants.ConstantClass` which represents this class's superclass. """ return self.constants.get(self._super) @super_.setter def super_(self, value: ConstantClass): self._super = value.index @property def interfaces(self) -> Iterable[ConstantClass]: """ A list of direct superinterfaces of this class as indexes into the constant pool, in left-to-right order. """ return [self._constants[idx] for idx in self._interfaces] @property def bootstrap_methods(self) -> BootstrapMethod: """ Returns the bootstrap methods table from the BootstrapMethods attribute, if one exists. If it does not, one will be created. :returns: Table of `BootstrapMethod` objects. """ bootstrap = self.attributes.find_one(name='BootstrapMethods') if bootstrap is None: bootstrap = self.attributes.create( ATTRIBUTE_CLASSES['BootstrapMethods'] ) return bootstrap.table def __repr__(self): return f'<ClassFile(this={self.this.name.value!r})>'