diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index cae8075..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,35 +0,0 @@ -version: 2 -jobs: - build: - branches: - ignore: - - gh-pages - - docker: - - image: circleci/python:3.6.1 - - working_directory: ~/repo - - steps: - - checkout - - - run: - name: install dependencies - command: | - python3 -m venv venv - . venv/bin/activate - pip3 install --upgrade pip - pip3 uninstall -y setuptools - pip3 install setuptools - pip3 install -e "." - pip3 install -e ".[dev]" - - - run: - name: run tests - command: | - . venv/bin/activate - pytest - - - store_artifacts: - path: test-reports - destination: test-reports diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..94fd905 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,148 @@ +name: Test and Release + +on: + release: + types: + - published + pull_request: + push: + branches: + - main + - develop + +jobs: + build: + name: Build Package + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4.1.6 + + - name: Set up Python + uses: actions/setup-python@v5.1.0 + with: + python-version: "3.13" + + - name: Install uv + run: pip install uv + + - name: Checking for code smells + run: uvx ruff check + + - name: Checking for formatting issues + run: uvx ruff format --check + + - name: Build package + run: uv build + + - name: Upload wheel artifact + uses: actions/upload-artifact@v4 + with: + name: wheel-package + path: dist/*.whl + retention-days: 1 + + - name: Upload source artifact + uses: actions/upload-artifact@v4 + with: + name: source-package + path: dist/*.tar.gz + retention-days: 1 + + test: + name: "${{ matrix.os }} - Py${{ matrix.python-version }}" + needs: [build] + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: true + matrix: + python-version: ["3.11", "3.12", "3.13"] + os: ["ubuntu-24.04"] + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + + steps: + - uses: actions/checkout@v4.1.6 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5.1.0 + with: + python-version: ${{ matrix.python-version }} + + - name: Installing uv + run: pip install uv + + - name: Installing dependencies (Python) + run: uv sync --all-extras + + - name: Running tests + run: | + uv run pytest -s -vvvvv --cov=lawu --cov-report=xml + + - name: Uploading coverage + uses: codecov/codecov-action@v4 + with: + env_vars: OS,PYTHON + fail_ci_if_error: true + flags: unittests + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + slug: TkTech/lawu + + docs: + name: Building Documentation + needs: [build] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4.1.6 + + - name: Set up Python + uses: actions/setup-python@v5.1.0 + with: + python-version: "3.13" + + - name: Installing uv + run: pip install uv + + - name: Installing dependencies (Python) + run: uv sync --all-extras + + - name: Building docs + run: | + cd docs && uv run make clean html + + - name: Publishing documentation + if: github.event_name == 'release' + run: | + uv run ghp-import -f -n -p docs/_build/html + + release: + name: Release to PyPI + needs: [test, docs] + runs-on: ubuntu-latest + if: github.event_name == 'release' + permissions: + contents: read + id-token: write + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + merge-multiple: true + path: dist + + - name: Set up Python + uses: actions/setup-python@v5.1.0 + with: + python-version: "3.13" + + - name: Install uv + run: pip install uv + + - name: Publishing to PyPI + run: | + uv publish \ No newline at end of file diff --git a/.gitignore b/.gitignore index b8db0ff..744e71b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,11 @@ test.py *.pyc docs/_build/ .DS_Store -*.un~ \ No newline at end of file +*.un~ +venv +.vscode +*.egg-info +dist +build/ +.venv +.idea \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 9474423..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include jawa/util/bytecode.json -include jawa/util/bytecode.yaml \ No newline at end of file diff --git a/README.md b/README.md index 9f82d0b..08a0b88 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,16 @@ -# Jawa +![lawu](lawu_small.png) -[![CircleCI](https://img.shields.io/circleci/project/github/TkTech/Jawa/master.svg?style=for-the-badge)](https://circleci.com/gh/TkTech/Jawa) -[![license](https://img.shields.io/github/license/tktech/jawa.svg?style=for-the-badge)](LICENCE) +# lawu -Jawa is a human-friendly library for assembling, disassembling, and exploring -JVM class files. It's highly suitable for automation tasks. +[![MIT](https://img.shields.io/github/license/tktech/lawu.svg?style=for-the-badge)](LICENCE) -*NOTE*: The assembler does _not_ currently implement Stack Maps, an -artificially complex requirement for ClassFiles generated for Java 7 and -above to properly verify (unless you turn it off with -XX:-UseSplitVerifier). -However, assembled files targeting Java 6 will still work with 7 and above. +Lawu is a human-friendly library for assembling, disassembling, and exploring +JVM class files. It's highly suitable for automation tasks. ## Documentation -API documentation & examples are available at http://jawa.tkte.ch +API documentation & examples are available at https://tkte.ch/lawu/ ## Licence -Jawa is available under the MIT licence. See LICENCE. +Lawu is available under the MIT licence. See LICENCE. \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 6128e89..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,155 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - touch $(BUILDDIR)/html/.nojekyll - echo "jawa.tkte.ch" > $(BUILDDIR)/html/CNAME - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/jawa.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/jawa.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/jawa" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/jawa" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index f65bc88..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,294 +0,0 @@ -# -# jawa documentation build configuration file, created by -# sphinx-quickstart on Wed Oct 31 23:00:27 2012. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys, os - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('../jawa')) - -# -- General configuration ----------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.viewcode', - 'sphinx.ext.autosummary', - 'sphinxcontrib.googleanalytics', - 'sphinx_click.ext' -] - -# Google Analytics ID -googleanalytics_id = 'UA-11145163-14' - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'jawa' -copyright = u'2019, Tyler Kennedy' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The full version, including alpha/beta/rc tags. -release = '2.2.0' -# The short X.Y version. -version = '.'.join(release.split('.')[:-1]) - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - - -# -- Options for HTML output --------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'sphinx_rtd_theme' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -html_theme_options = { -} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'jawadoc' - - -# -- Options for LaTeX output -------------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'jawa.tex', u'jawa Documentation', - u'Author', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'jawa', u'jawa Documentation', - [u'Author'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------------ - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'jawa', u'jawa Documentation', - u'Author', 'jawa', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - - -# -- Options for Epub output --------------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = u'jawa' -epub_author = u'Author' -epub_publisher = u'Author' -epub_copyright = u'2012, Author' - -# The language of the text. It defaults to the language option -# or en if the language is not set. -#epub_language = '' - -# The scheme of the identifier. Typical schemes are ISBN or URL. -#epub_scheme = '' - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -#epub_identifier = '' - -# A unique identification for the text. -#epub_uid = '' - -# A tuple containing the cover image and cover page html template filenames. -#epub_cover = () - -# HTML files that should be inserted before the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_pre_files = [] - -# HTML files shat should be inserted after the pages created by sphinx. -# The format is a list of tuples containing the path and title. -#epub_post_files = [] - -# A list of files that should not be packed into the epub file. -#epub_exclude_files = [] - -# The depth of the table of contents in toc.ncx. -#epub_tocdepth = 3 - -# Allow duplicate toc entries. -#epub_tocdup = True diff --git a/docs/examples/dism.rst b/docs/examples/dism.rst deleted file mode 100644 index fb1a17b..0000000 --- a/docs/examples/dism.rst +++ /dev/null @@ -1,31 +0,0 @@ -Disassembly - A Simple javap Clone -================================== - -`Javap`_ is the defacto Java disassembler and is included when you installed -the Oracle JDK. We can make a simple clone very easily using Jawa: - -.. literalinclude:: ../../examples/disassemble.py - -If we try this out on a HelloWorld class, our output will look like: - -.. code-block:: text - - $ python disassemble.py --class-path ../tests/data HelloWorld develop - ; ---------------------------------------------- constant pool - ; -------------------------------------------------- total: 21 - ; 0001: ) - ; 0002: ))> - ... - ; ----------------------------------------------------- fields - ; --------------------------------------------------- total: 0 - ; ---------------------------------------------------- methods - ; --------------------------------------------------- total: 1 - acc_public acc_static void main(java/lang/String[]) { - 0000 [0xB2] getstatic <- C[13] - 0003 [0x12] ldc <- C[15] - 0005 [0xB6] invokevirtual <- C[21] - 0008 [0xB1] return <- - } - - -.. _Javap: https://docs.oracle.com/javase/8/docs/technotes/tools/windows/javap.html diff --git a/docs/examples/hello_world.rst b/docs/examples/hello_world.rst deleted file mode 100644 index 7e6c8d5..0000000 --- a/docs/examples/hello_world.rst +++ /dev/null @@ -1,23 +0,0 @@ -Generating Classes From Scratch - Hello World! -============================================== - -A simple example of the classic "Hello World!" program. This example will -generate a ClassFile equivalent to this Java: - -.. code-block:: java - - class HelloWorld { - public static void main(String[] args) { - System.out.println("Hello World!"); - } - } - -.. literalinclude:: ../../examples/hello_world.py - -Our example can then be run in the standard JVM: - -.. code-block:: text - - $ python hello_world.py - $ java HelloWorld develop - Hello World! \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 5d1d603..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,73 +0,0 @@ -Jawa - JVM ClassFile Library -================================ - -Jawa_ is a human-friendly library for assembling, disassembling, and exploring -JVM class files. It's highly suitable for automation tasks. - -.. note:: This documentation is up to date as of |today|. - -Why Jawa? ---------- - -Jawa is intended to be used very differently from most projects similar to it. -Instead of trying to produce a human-readable disassembly or nearly-compilable -Java, it's intended for people that want to dive into or automate work on JVM -bytecode. For example, Jawa has been used for: - -- The automatic verification of community-uploaded plugins -- Analysis and analytics of public Android APKs (thousands at a time) -- Automatic extraction of "private" API keys embedded in Android APKs. -- Automated reverse engineering of the Minecraft server and client. - -Jawa is permissively licenced under the MIT licence. You're free to use it in -any type of project should it be commercial, closed source or open source. - - -Getting Started ---------------- - -It's recommended to use the :class:`~jawa.classloader.ClassLoader` when working -with JARs/directories, as it offers a number of conveniences. Here's an example -of loading each of the classes in the Minecraft_ server. - -.. code-block:: python - - from jawa.classloader import ClassLoader - - loader = ClassLoader('minecraft_server.jar') - for class_path in loader.classes: - cf = loader[class_path] - - -Alternatively you can create & load a :class:`~jawa.cf.ClassFile` directly. - -.. code-block:: python - - from jawa.cf import ClassFile - - with open('HelloWorld.class') as file_in: - cf = ClassFile(file_in) - - -Examples --------- - -.. toctree:: - :maxdepth: 2 - - examples/hello_world.rst - examples/dism.rst - - -Jawa API --------- - -.. toctree:: - :maxdepth: 3 - - jawa - -* :ref:`genindex` -* :ref:`search` - -.. include:: links.rst diff --git a/docs/jawa.assemble.rst b/docs/jawa.assemble.rst deleted file mode 100644 index 3459d13..0000000 --- a/docs/jawa.assemble.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.assemble module -==================== - -.. automodule:: jawa.assemble - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.attribute.rst b/docs/jawa.attribute.rst deleted file mode 100644 index 1695ef8..0000000 --- a/docs/jawa.attribute.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.attribute module -===================== - -.. automodule:: jawa.attribute - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.attributes.bootstrap.rst b/docs/jawa.attributes.bootstrap.rst deleted file mode 100644 index b1059d5..0000000 --- a/docs/jawa.attributes.bootstrap.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.attributes.bootstrap module -================================ - -.. automodule:: jawa.attributes.bootstrap - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.attributes.code.rst b/docs/jawa.attributes.code.rst deleted file mode 100644 index 6657b94..0000000 --- a/docs/jawa.attributes.code.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.attributes.code module -=========================== - -.. automodule:: jawa.attributes.code - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.attributes.constant_value.rst b/docs/jawa.attributes.constant_value.rst deleted file mode 100644 index 7ce8ae9..0000000 --- a/docs/jawa.attributes.constant_value.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.attributes.constant\_value module -====================================== - -.. automodule:: jawa.attributes.constant_value - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.attributes.deprecated.rst b/docs/jawa.attributes.deprecated.rst deleted file mode 100644 index d4e738c..0000000 --- a/docs/jawa.attributes.deprecated.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.attributes.deprecated module -================================= - -.. automodule:: jawa.attributes.deprecated - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.attributes.enclosing_method.rst b/docs/jawa.attributes.enclosing_method.rst deleted file mode 100644 index 48d3f88..0000000 --- a/docs/jawa.attributes.enclosing_method.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.attributes.enclosing\_method module -======================================== - -.. automodule:: jawa.attributes.enclosing_method - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.attributes.exceptions.rst b/docs/jawa.attributes.exceptions.rst deleted file mode 100644 index 13b07b0..0000000 --- a/docs/jawa.attributes.exceptions.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.attributes.exceptions module -================================= - -.. automodule:: jawa.attributes.exceptions - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.attributes.inner_classes.rst b/docs/jawa.attributes.inner_classes.rst deleted file mode 100644 index ef99f1a..0000000 --- a/docs/jawa.attributes.inner_classes.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.attributes.inner\_classes module -===================================== - -.. automodule:: jawa.attributes.inner_classes - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.attributes.line_number_table.rst b/docs/jawa.attributes.line_number_table.rst deleted file mode 100644 index 31ce85d..0000000 --- a/docs/jawa.attributes.line_number_table.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.attributes.line\_number\_table module -========================================== - -.. automodule:: jawa.attributes.line_number_table - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.attributes.local_variable.rst b/docs/jawa.attributes.local_variable.rst deleted file mode 100644 index a332d58..0000000 --- a/docs/jawa.attributes.local_variable.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.attributes.local\_variable module -====================================== - -.. automodule:: jawa.attributes.local_variable - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.attributes.local_variable_type.rst b/docs/jawa.attributes.local_variable_type.rst deleted file mode 100644 index 7c8ac2b..0000000 --- a/docs/jawa.attributes.local_variable_type.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.attributes.local\_variable\_type module -============================================ - -.. automodule:: jawa.attributes.local_variable_type - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.attributes.rst b/docs/jawa.attributes.rst deleted file mode 100644 index 9623f4c..0000000 --- a/docs/jawa.attributes.rst +++ /dev/null @@ -1,27 +0,0 @@ -jawa.attributes package -======================= - -.. automodule:: jawa.attributes - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -.. toctree:: - - jawa.attributes.bootstrap - jawa.attributes.code - jawa.attributes.constant_value - jawa.attributes.deprecated - jawa.attributes.exceptions - jawa.attributes.line_number_table - jawa.attributes.local_variable - jawa.attributes.signature - jawa.attributes.source_file - jawa.attributes.stack_map_table - jawa.attributes.enclosing_method - jawa.attributes.inner_classes - jawa.attributes.local_variable_type - jawa.attributes.synthetic diff --git a/docs/jawa.attributes.signature.rst b/docs/jawa.attributes.signature.rst deleted file mode 100644 index 28dc145..0000000 --- a/docs/jawa.attributes.signature.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.attributes.signature module -================================ - -.. automodule:: jawa.attributes.signature - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.attributes.source_file.rst b/docs/jawa.attributes.source_file.rst deleted file mode 100644 index 2b378b1..0000000 --- a/docs/jawa.attributes.source_file.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.attributes.source\_file module -=================================== - -.. automodule:: jawa.attributes.source_file - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.attributes.stack_map_table.rst b/docs/jawa.attributes.stack_map_table.rst deleted file mode 100644 index 72ea891..0000000 --- a/docs/jawa.attributes.stack_map_table.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.attributes.stack\_map\_table module -======================================== - -.. automodule:: jawa.attributes.stack_map_table - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.attributes.synthetic.rst b/docs/jawa.attributes.synthetic.rst deleted file mode 100644 index 0c37c7a..0000000 --- a/docs/jawa.attributes.synthetic.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.attributes.synthetic module -================================ - -.. automodule:: jawa.attributes.synthetic - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.cf.rst b/docs/jawa.cf.rst deleted file mode 100644 index 2e36130..0000000 --- a/docs/jawa.cf.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.cf module -============== - -.. automodule:: jawa.cf - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.classloader.rst b/docs/jawa.classloader.rst deleted file mode 100644 index 05cf25c..0000000 --- a/docs/jawa.classloader.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.classloader module -======================= - -.. automodule:: jawa.classloader - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.cli.rst b/docs/jawa.cli.rst deleted file mode 100644 index e960c10..0000000 --- a/docs/jawa.cli.rst +++ /dev/null @@ -1,6 +0,0 @@ -jawa.cli package -================ - -.. click:: jawa.cli:cli - :prog: jawa - :show-nested: \ No newline at end of file diff --git a/docs/jawa.constants.rst b/docs/jawa.constants.rst deleted file mode 100644 index 74bf41e..0000000 --- a/docs/jawa.constants.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.constants module -===================== - -.. automodule:: jawa.constants - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.fields.rst b/docs/jawa.fields.rst deleted file mode 100644 index c67f544..0000000 --- a/docs/jawa.fields.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.fields module -================== - -.. automodule:: jawa.fields - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.methods.rst b/docs/jawa.methods.rst deleted file mode 100644 index cc7b71d..0000000 --- a/docs/jawa.methods.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.methods module -=================== - -.. automodule:: jawa.methods - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.rst b/docs/jawa.rst deleted file mode 100644 index 3b72548..0000000 --- a/docs/jawa.rst +++ /dev/null @@ -1,30 +0,0 @@ -jawa package -============ - -.. automodule:: jawa - :members: - :undoc-members: - :show-inheritance: - -Subpackages ------------ - -.. toctree:: - - jawa.attributes - jawa.util - -Submodules ----------- - -.. toctree:: - - jawa.assemble - jawa.attribute - jawa.cf - jawa.classloader - jawa.constants - jawa.fields - jawa.methods - jawa.transforms - jawa.cli diff --git a/docs/jawa.transforms.rst b/docs/jawa.transforms.rst deleted file mode 100644 index 88f8a62..0000000 --- a/docs/jawa.transforms.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.transforms module -====================== - -.. automodule:: jawa.transforms - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.util.bytecode.rst b/docs/jawa.util.bytecode.rst deleted file mode 100644 index ca8891a..0000000 --- a/docs/jawa.util.bytecode.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.util.bytecode module -========================= - -.. automodule:: jawa.util.bytecode - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.util.descriptor.rst b/docs/jawa.util.descriptor.rst deleted file mode 100644 index eb5fdd2..0000000 --- a/docs/jawa.util.descriptor.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.util.descriptor module -=========================== - -.. automodule:: jawa.util.descriptor - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.util.flags.rst b/docs/jawa.util.flags.rst deleted file mode 100644 index 9444fd4..0000000 --- a/docs/jawa.util.flags.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.util.flags module -====================== - -.. automodule:: jawa.util.flags - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.util.rst b/docs/jawa.util.rst deleted file mode 100644 index 2847720..0000000 --- a/docs/jawa.util.rst +++ /dev/null @@ -1,22 +0,0 @@ -jawa.util package -================= - -.. automodule:: jawa.util - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -.. toctree:: - - jawa.util.bytecode - jawa.util.descriptor - jawa.util.flags - jawa.util.shell - jawa.util.stream - jawa.util.tracer - jawa.util.utf - jawa.util.verifier - diff --git a/docs/jawa.util.shell.rst b/docs/jawa.util.shell.rst deleted file mode 100644 index 585d3d3..0000000 --- a/docs/jawa.util.shell.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.util.shell module -====================== - -.. automodule:: jawa.util.shell - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.util.stream.rst b/docs/jawa.util.stream.rst deleted file mode 100644 index 2a3a817..0000000 --- a/docs/jawa.util.stream.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.util.stream module -======================= - -.. automodule:: jawa.util.stream - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.util.tracer.rst b/docs/jawa.util.tracer.rst deleted file mode 100644 index 69d9604..0000000 --- a/docs/jawa.util.tracer.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.util.tracer module -======================= - -.. automodule:: jawa.util.tracer - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.util.utf.rst b/docs/jawa.util.utf.rst deleted file mode 100644 index db5d8fd..0000000 --- a/docs/jawa.util.utf.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.util.utf module -==================== - -.. automodule:: jawa.util.utf - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/jawa.util.verifier.rst b/docs/jawa.util.verifier.rst deleted file mode 100644 index a246d5c..0000000 --- a/docs/jawa.util.verifier.rst +++ /dev/null @@ -1,7 +0,0 @@ -jawa.util.verifier module -========================= - -.. automodule:: jawa.util.verifier - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/links.rst b/docs/links.rst deleted file mode 100644 index e1b0c31..0000000 --- a/docs/links.rst +++ /dev/null @@ -1,2 +0,0 @@ -.. _Jawa: http://github.com/TkTech/Jawa -.. _Minecraft: http://minecraft.net \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 65e1cfd..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,190 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\jawa.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\jawa.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -:end diff --git a/examples/creating_fields.py b/examples/creating_fields.py index e5afbf2..f41a62e 100644 --- a/examples/creating_fields.py +++ b/examples/creating_fields.py @@ -1,20 +1,21 @@ """ An example showing how to create fields on a new class. """ -from jawa import ClassFile -if __name__ == '__main__': - cf = ClassFile.create('HelloWorld') +from lawu.cf import ClassFile +from lawu.constants import String - # Creating a field from a field name and descriptor - field = cf.fields.create('BeerCount', 'I') +if __name__ == "__main__": + cf = ClassFile(this="HelloWorld") - # A convienience shortcut for creating static fields. - field = cf.fields.create_static( - 'HelloWorld', - 'Ljava/lang/String;', - cf.constants.create_string('Hello World!') - ) + with cf: + # Creating a field from a field name and descriptor + field = cf.fields.create("BeerCount", "I") - with open('HelloWorld.class', 'wb') as fout: + # A convenience shortcut for creating static fields. + field = cf.fields.create_static( + "HelloWorld", "Ljava/lang/String;", String("Hello World!") + ) + + with open("HelloWorld.class", "wb") as fout: cf.save(fout) diff --git a/examples/disassemble.py b/examples/disassemble.py index c9e250d..3da6fe0 100644 --- a/examples/disassemble.py +++ b/examples/disassemble.py @@ -1,76 +1,73 @@ import click -from jawa.classloader import ClassLoader -from jawa.util.bytecode import OperandTypes +from lawu.classloader import ClassLoader +from lawu.util.bytecode import OperandTypes @click.command() -@click.option('--class-path', multiple=True, type=click.Path(exists=True)) -@click.argument('classes', nargs=-1) +@click.option("--class-path", multiple=True, type=click.Path(exists=True)) +@click.argument("classes", nargs=-1) def main(class_path, classes): loader = ClassLoader(*class_path) for class_ in classes: cf = loader[class_] # The constant pool. - print('; {0:->60}'.format(' constant pool')) - print('; {0:->60}'.format( - ' total: {0}'.format(len(cf.constants)) - )) + print("; {0:->60}".format(" constant pool")) + print("; {0:->60}".format(" total: {0}".format(len(cf.constants)))) for constant in cf.constants: - print('; {0:04}: {1!r}'.format(constant.index, constant)) + print("; {0:04}: {1!r}".format(constant.index, constant)) # The fields table. - print('; {0:->60}'.format(' fields')) - print('; {0:->60}'.format( - ' total: {0}'.format(len(cf.fields)) - )) + print("; {0:->60}".format(" fields")) + print("; {0:->60}".format(" total: {0}".format(len(cf.fields)))) for field in cf.fields: - print('; {0!r}'.format(field)) + print("; {0!r}".format(field)) # The methods table. - print('; {0:->60}'.format(' methods')) - print('; {0:->60}'.format( - ' total: {0}'.format(len(cf.methods)) - )) + print("; {0:->60}".format(" methods")) + print("; {0:->60}".format(" total: {0}".format(len(cf.methods)))) for method in cf.methods: # Find all enabled flags and print them out (such as acc_public # and acc_private) flags = method.access_flags.to_dict() flags = [k for k, v in flags.items() if v] - print('{0} {1} {2}({3}) {{'.format( - ' '.join(flags), - method.returns.name, - method.name.value, - ', '.join(a.name + ('[]' * a.dimensions) for a in method.args) - ).strip()) + print( + "{0} {1} {2}({3}) {{".format( + " ".join(flags), + method.returns.name, + method.name.value, + ", ".join(a.name + ("[]" * a.dimensions) for a in method.args), + ).strip() + ) - r = [] if method.code: for ins in method.code.disassemble(): line = [ - f'{ins.pos:04}', - f'[0x{ins.opcode:02X}]', - f'{ins.mnemonic:>15} <-' + f"{ins.pos:04}", + f"[0x{ins.opcode:02X}]", + f"{ins.mnemonic:>15} <-", ] for operand in ins.operands: if isinstance(operand, dict): - line.append(f'JT[{operand!r}]') + line.append(f"JT[{operand!r}]") continue - line.append({ - OperandTypes.CONSTANT_INDEX: f'C[{operand.value}]', - OperandTypes.BRANCH: f'J[{operand.value}]', - OperandTypes.LITERAL: f'#[{operand.value}]', - OperandTypes.LOCAL_INDEX: f'L[{operand.value}]', - OperandTypes.PADDING: 'P' - }[operand.op_type]) + line.append( + { + OperandTypes.CONSTANT_INDEX: f"C[{operand.value}]", + OperandTypes.BRANCH: f"J[{operand.value}]", + OperandTypes.LITERAL: f"#[{operand.value}]", + OperandTypes.LOCAL_INDEX: f"L[{operand.value}]", + OperandTypes.PADDING: "P", + }[operand.op_type] + ) - print(' ' + ' '.join(line)) - print('}') + print(" " + " ".join(line)) + print("}") -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/examples/hello_world.py b/examples/hello_world.py index d53fa37..8dbbdc3 100644 --- a/examples/hello_world.py +++ b/examples/hello_world.py @@ -1,30 +1,39 @@ """ An example showing how to create a "Hello World" class from scratch. """ -from jawa.cf import ClassFile -from jawa.assemble import assemble -cf = ClassFile.create('HelloWorld') +from lawu.cf import ClassFile +from lawu.assemble import assemble +from lawu.constants import String -main = cf.methods.create('main', '([Ljava/lang/String;)V', code=True) +cf = ClassFile(this="HelloWorld") + +main = cf.methods.create("main", "([Ljava/lang/String;)V", code=True) main.access_flags.acc_static = True main.code.max_locals = 1 main.code.max_stack = 2 -main.code.assemble(assemble([ - ('getstatic', cf.constants.create_field_ref( - 'java/lang/System', - 'out', - 'Ljava/io/PrintStream;' - )), - ('ldc', cf.constants.create_string('Hello World!')), - ('invokevirtual', cf.constants.create_method_ref( - 'java/io/PrintStream', - 'println', - '(Ljava/lang/String;)V' - )), - ('return',) -])) +with cf: + main.code.assemble( + assemble( + [ + ( + "getstatic", + cf.constants.create_field_ref( + "java/lang/System", "out", "Ljava/io/PrintStream;" + ), + ), + ("ldc", String("Hello World!")), + ( + "invokevirtual", + cf.constants.create_method_ref( + "java/io/PrintStream", "println", "(Ljava/lang/String;)V" + ), + ), + ("return",), + ] + ) + ) -with open('HelloWorld.class', 'wb') as fout: +with open("HelloWorld.class", "wb") as fout: cf.save(fout) diff --git a/jawa/__init__.py b/jawa/__init__.py deleted file mode 100644 index 40a96af..0000000 --- a/jawa/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/jawa/attributes/bootstrap.py b/jawa/attributes/bootstrap.py deleted file mode 100644 index bf6be9f..0000000 --- a/jawa/attributes/bootstrap.py +++ /dev/null @@ -1,56 +0,0 @@ -import io -from collections import namedtuple -from itertools import repeat -from struct import pack - -from jawa.attribute import Attribute - -BootstrapMethod = namedtuple( - 'BootstrapMethod', - ['method_ref', 'bootstrap_args'] -) - - -class BootstrapMethodsAttribute(Attribute): - ADDED_IN = '7' - MINIMUM_CLASS_VERSION = (51, 0) - - def __init__(self, table, name_index=None): - super(BootstrapMethodsAttribute, self).__init__( - table, - name_index or table.cf.constants.create_utf8( - 'BootstrapMethods' - ).index - ) - self.table = [] - - def __repr__(self): - return ''.format( - self=self - ) - - def pack(self): - out = io.BytesIO() - out.write(pack('>H', len(self.table))) - - for table_entry in self.table: - out.write(pack( - '>HH', - table_entry.method_ref, - len(table_entry.bootstrap_args) - )) - out.write(pack( - '>{0}H'.format(len(table_entry.bootstrap_args)), - table_entry.bootstrap_args - )) - - return out.getvalue() - - def unpack(self, info): - length = info.u2() - - for _ in repeat(None, length): - self.table.append(BootstrapMethod( - info.u2(), - info.unpack('>{0}H'.format(info.u2())) - )) diff --git a/jawa/attributes/exceptions.py b/jawa/attributes/exceptions.py deleted file mode 100644 index 9b4be39..0000000 --- a/jawa/attributes/exceptions.py +++ /dev/null @@ -1,33 +0,0 @@ -from struct import pack - -from jawa.attribute import Attribute - - -class ExceptionsAttribute(Attribute): - ADDED_IN = '1.0.2' - MINIMUM_CLASS_VERSION = (45, 3) - - def __init__(self, table, name_index=None): - super(ExceptionsAttribute, self).__init__( - table, - name_index or table.cf.constants.create_utf8( - 'Exceptions' - ).index - ) - self.exceptions = [] - - def unpack(self, info): - length = info.u2() - self.exceptions = list(info.unpack('>{0}H'.format(length))) - - def pack(self): - return pack( - '>H{0}H'.format( - len(self.exceptions) - ), - len(self.exceptions), - *self.exceptions - ) - - def __repr__(self): - return ''.format(self.exceptions) diff --git a/jawa/attributes/inner_classes.py b/jawa/attributes/inner_classes.py deleted file mode 100644 index b50089f..0000000 --- a/jawa/attributes/inner_classes.py +++ /dev/null @@ -1,38 +0,0 @@ -import io -from struct import pack -from itertools import repeat -from collections import namedtuple -from jawa.attribute import Attribute - - -InnerClass = namedtuple('InnerClass', [ - 'inner_class_info_index', - 'outer_class_info_index', - 'inner_name_index', - 'inner_class_access_flags' -]) - - -class InnerClassesAttribute(Attribute): - ADDED_IN = '1.1.0' - MINIMUM_CLASS_VERSION = (45, 3) - - def __init__(self, table, name_index): - super().__init__( - table, - name_index or table.cf.constants.create_utf8( - 'InnerClasses' - ).index - ) - self.inner_classes = [] - - def unpack(self, info): - for _ in repeat(None, info.u2()): - self.inner_classes.append(InnerClass(*info.unpack('>HHHH'))) - - def pack(self): - with io.BytesIO() as out: - out.write(pack('>H', len(self.inner_classes))) - for inner_class in self.inner_classes: - out.write(pack('>HHHH', *inner_class)) - return out.getvalue() diff --git a/jawa/attributes/line_number_table.py b/jawa/attributes/line_number_table.py deleted file mode 100644 index 60c722e..0000000 --- a/jawa/attributes/line_number_table.py +++ /dev/null @@ -1,40 +0,0 @@ -from struct import pack -from collections import namedtuple - -from jawa.attribute import Attribute - - -line_number_entry = namedtuple('line_number_entry', 'start_pc line_number') - - -class LineNumberTableAttribute(Attribute): - ADDED_IN = '1.0.2' - MINIMUM_CLASS_VERSION = (45, 3) - - def __init__(self, table, name_index=None): - super(LineNumberTableAttribute, self).__init__( - table, - name_index or table.cf.constants.create_utf8( - 'LineNumberTable' - ).index - ) - self.line_no = [] - - def unpack(self, info): - length = info.u2() - table = info.unpack('>{0}H'.format(length * 2)) - - self.line_no = [ - line_number_entry(*x) - for x in zip(*[iter(table)] * 2) - ] - - def pack(self): - return pack( - '>H{0}H'.format(len(self.line_no) * 2), - len(self.line_no), - *sum(self.line_no, ()) - ) - - def __repr__(self): - return ''.format(self.line_no) diff --git a/jawa/attributes/local_variable.py b/jawa/attributes/local_variable.py deleted file mode 100644 index a41b1da..0000000 --- a/jawa/attributes/local_variable.py +++ /dev/null @@ -1,46 +0,0 @@ -from struct import pack -from collections import namedtuple - -from jawa.attribute import Attribute - - -local_variable_entry = namedtuple('local_variable_entry', [ - 'start_pc', - 'length', - 'name_index', - 'descriptor_index', - 'index' -]) - - -class LocalVariableTableAttribute(Attribute): - ADDED_IN = '1.0.2' - MINIMUM_CLASS_VERSION = (45, 3) - - def __init__(self, table, name_index=None): - super().__init__( - table, - name_index or table.cf.constants.create_utf8( - 'LocalVariableTable' - ).index - ) - self.local_variables = [] - - def unpack(self, info): - length = info.u2() - table = info.unpack('>{0}H'.format(length * 5)) - - self.local_variables = [ - local_variable_entry(*x) - for x in zip(*[iter(table)] * 5) - ] - - def pack(self): - return pack( - '>H{0}H'.format(len(self.local_variables) * 5), - len(self.local_variables), - *sum(self.local_variables, ()) - ) - - def __repr__(self): - return f'' diff --git a/jawa/attributes/local_variable_type.py b/jawa/attributes/local_variable_type.py deleted file mode 100644 index 6568b05..0000000 --- a/jawa/attributes/local_variable_type.py +++ /dev/null @@ -1,46 +0,0 @@ -from struct import pack -from collections import namedtuple - -from jawa.attribute import Attribute - - -local_variable_type_entry = namedtuple('local_variable_type_entry', [ - 'start_pc', - 'length', - 'name_index', - 'signature_index', - 'index' -]) - - -class LocalVariableTypeTableAttribute(Attribute): - ADDED_IN = '1.0.2' - MINIMUM_CLASS_VERSION = (45, 3) - - def __init__(self, table, name_index=None): - super().__init__( - table, - name_index or table.cf.constants.create_utf8( - 'LocalVariableTypeTable' - ).index - ) - self.local_variables = [] - - def unpack(self, info): - length = info.u2() - table = info.unpack('>{0}H'.format(length * 5)) - - self.local_variables = [ - local_variable_type_entry(*x) - for x in zip(*[iter(table)] * 5) - ] - - def pack(self): - return pack( - '>H{0}H'.format(len(self.local_variables) * 5), - len(self.local_variables), - *sum(self.local_variables, ()) - ) - - def __repr__(self): - return f'' diff --git a/jawa/cf.py b/jawa/cf.py deleted file mode 100644 index 7cdb945..0000000 --- a/jawa/cf.py +++ /dev/null @@ -1,248 +0,0 @@ -""" -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 - - -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) - - -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) - - @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 - - 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'' diff --git a/jawa/cli.py b/jawa/cli.py deleted file mode 100644 index dda15cf..0000000 --- a/jawa/cli.py +++ /dev/null @@ -1,168 +0,0 @@ -import re -import json -import importlib - -import click - -from jawa.classloader import ClassLoader -from jawa.cf import ClassVersion, ClassFile -from jawa.attribute import get_attribute_classes -from jawa.util import bytecode, shell -from jawa.constants import UTF8 - - -@click.group() -def cli(): - pass - - -@cli.command() -def attributes(): - """List enabled Attributes. - - Prints a list of all enabled ClassFile Attributes. - """ - attribute_classes = get_attribute_classes() - for name, class_ in attribute_classes.items(): - click.echo( - u'{name} - Added in: {ai} ({cv})'.format( - name=click.style(name, fg='green'), - ai=click.style(class_.ADDED_IN, fg='yellow'), - cv=click.style( - ClassVersion(*class_.MINIMUM_CLASS_VERSION).human, - fg='yellow' - ) - ) - ) - - -@cli.command() -@click.argument('mnemonic') -def ins(mnemonic): - """Lookup instruction information. - - Lookup an instruction by its mnemonic. - """ - try: - opcode = bytecode.opcode_table[mnemonic] - except KeyError: - click.secho(u'No definition found.', fg='red') - return - - click.echo(u'{mnemonic} (0x{op})'.format( - mnemonic=click.style(opcode['mnemonic'], fg='green', underline=True), - op=click.style(format(opcode['op'], '02x'), fg='green') - )) - - if opcode.get('desc'): - click.secho('Description:', fg='yellow') - click.echo(opcode['desc']) - - if opcode['can_be_wide']: - click.echo(u'This instruction can be prefixed by the WIDE opcode.') - - if opcode.get('runtime'): - click.secho('Possible runtime exceptions:', fg='yellow') - for runtime_exception in opcode['runtime']: - click.echo('- {runtime_exception}'.format( - runtime_exception=click.style(runtime_exception, fg='red') - )) - - if opcode['operands']: - click.secho(u'Operand Format:', fg='yellow') - for operand_fmt, operand_type in opcode['operands']: - click.echo(u'- {ty} as a {fmt}'.format( - ty=click.style(operand_type.name, fg='yellow'), - fmt=click.style(operand_fmt.name, fg='yellow') - )) - elif opcode['op'] in (0xAB, 0xAA, 0xC4): - # lookup[table|switch] and WIDE. - click.secho(u'\nOperand Format:', fg='yellow') - click.echo( - u'This is a special-case opcode with variable operand parsing.' - ) - - -@cli.command(name='shell') -@click.option('--class-path', '-cp', multiple=True) -def shell_command(class_path): - """Drop into a debugging shell.""" - loader = ClassLoader(*class_path) - shell.start_shell(local_ns={ - 'ClassFile': ClassFile, - 'loader': loader, - 'constants': importlib.import_module('jawa.constants'), - }) - - -@cli.command(name='def2json') -@click.argument('source', type=click.File('rb')) -def definition_to_json(source): - """Convert a bytecode.yaml file into a prepared bytecode.json. - - Jawa internally uses a YAML file to define all bytecode opcodes, operands, - runtime exceptions, default transforms, etc... - - However since JSON is available in the python stdlib and YAML is not, we - process this YAML file before distribution to prevent adding an unnecessary - dependency. - """ - try: - import yaml - except ImportError: - click.echo( - 'The pyyaml module could not be found and is required' - ' to use this command.', - err=True - ) - return - - y = yaml.load(source) - - for k, v in y.items(): - # We guarantee some keys should always exist to make life easier for - # developers. - v.setdefault('operands', None) - v.setdefault('can_be_wide', False) - v.setdefault('transform', {}) - v['mnemonic'] = k - - click.echo(json.dumps(y, indent=4, sort_keys=True)) - - -@cli.command() -@click.argument('source', type=click.Path(exists=True)) -def dependencies(source): - """Output a list of all classes referenced by the given source.""" - loader = ClassLoader(source, max_cache=-1) - all_dependencies = set() - for klass in loader.classes: - new_dependencies = loader.dependencies(klass) - all_dependencies - all_dependencies.update(new_dependencies) - for new_dep in new_dependencies: - click.echo(new_dep) - - -@cli.command() -@click.argument('source', type=click.Path(exists=True)) -@click.argument('regex') -@click.option( - '--stop-on-first', - default=False, - is_flag=True, - help='Stop iteration on first matching class.' -) -def grep(source, regex, stop_on_first=False): - """Grep the constant pool of all classes in source.""" - loader = ClassLoader(source, max_cache=-1) - r = re.compile(regex) - - def _matches(constant): - return r.match(constant.value) - - for klass in loader.classes: - it = loader.search_constant_pool(path=klass, type_=UTF8, f=_matches) - if next(it, None): - print(klass) - if stop_on_first: - break diff --git a/jawa/constants.py b/jawa/constants.py deleted file mode 100644 index 2887014..0000000 --- a/jawa/constants.py +++ /dev/null @@ -1,615 +0,0 @@ -from struct import unpack, pack - -from jawa.util.utf import decode_modified_utf8, encode_modified_utf8 - - -class Constant(object): - """ - The base class for all ``Constant*`` types. - """ - __slots__ = ('pool', 'index') - - def __init__(self, pool, index): - self.pool = pool - self.index = index - - -class Number(Constant): - __slots__ = ('value',) - - def __init__(self, pool, index, value): - super().__init__(pool, index) - self.value = value - - def __repr__(self): - return ( - f'{self.__class__.__name__}(' - f'index={self.index}, value={self.value!r})' - ) - - def __eq__(self, other): - return other == self.value - - -class UTF8(Constant): - __slots__ = ('value',) - TAG = 1 - - def __init__(self, pool, index, value): - super().__init__(pool, index) - self.value = value - - def pack(self): - encoded_value = encode_modified_utf8(self.value) - return pack('>BH', self.TAG, len(encoded_value)) + encoded_value - - def __repr__(self): - return f')' - - def __eq__(self, other): - return other == self.value - - -class Integer(Number): - TAG = 3 - - def pack(self): - return pack('>Bi', self.TAG, self.value) - - -class Float(Number): - TAG = 4 - - def pack(self): - return pack('>Bf', self.TAG, self.value) - - -class Long(Number): - TAG = 5 - - def pack(self): - return pack('>Bq', self.TAG, self.value) - - -class Double(Number): - TAG = 6 - - def pack(self): - return pack('>Bd', self.TAG, self.value) - - -class ConstantClass(Constant): - __slots__ = ('name_index',) - TAG = 7 - - def __init__(self, pool, index, name_index): - super().__init__(pool, index) - self.name_index = name_index - - @property - def name(self): - return self.pool.get(self.name_index) - - def pack(self): - return pack('>BH', self.TAG, self.name_index) - - def __repr__(self): - return f'' - - -class String(Constant): - __slots__ = ('string_index',) - TAG = 8 - - def __init__(self, pool, index, string_index): - super().__init__(pool, index) - self.string_index = string_index - - @property - def string(self): - return self.pool.get(self.string_index) - - def pack(self): - return pack('>BH', self.TAG, self.string_index) - - def __repr__(self): - return f'' - - def __eq__(self, other): - return other == self.string.value - - -class Reference(Constant): - __slots__ = ('class_index', 'name_and_type_index') - TAG = None - - def __init__(self, pool, index, class_index, name_and_type_index): - super().__init__(pool, index) - self.class_index = class_index - self.name_and_type_index = name_and_type_index - - @property - def class_(self): - return self.pool.get(self.class_index) - - @property - def name_and_type(self): - return self.pool.get(self.name_and_type_index) - - def pack(self): - return pack( - '>BHH', - self.TAG, - self.class_index, - self.name_and_type_index - ) - - def __repr__(self): - return ( - f'<{self.__class__.__name__}(' - f'index={self.index},' - f'class_={self.class_!r},' - f'name_and_type={self.name_and_type!r})>' - ) - - -class FieldReference(Reference): - TAG = 9 - - -class MethodReference(Reference): - TAG = 10 - - -class InterfaceMethodRef(Reference): - TAG = 11 - - -class NameAndType(Constant): - __slots__ = ('name_index', 'descriptor_index') - TAG = 12 - - def __init__(self, pool, index, name_index, descriptor_index): - super().__init__(pool, index) - self.name_index = name_index - self.descriptor_index = descriptor_index - - @property - def name(self): - return self.pool.get(self.name_index) - - @property - def descriptor(self): - return self.pool.get(self.descriptor_index) - - def pack(self): - return pack('>BHH', self.TAG, self.name_index, self.descriptor_index) - - def __repr__(self): - return ( - f'' - ) - - -class MethodHandle(Constant): - __slots__ = ('reference_kind', 'reference_index') - TAG = 15 - - def __init__(self, pool, index, reference_kind, reference_index): - super().__init__(pool, index) - self.reference_kind = reference_kind - self.reference_index = reference_index - - @property - def reference(self): - return self.pool.get(self.reference_index) - - def pack(self): - return pack('>BBH', self.TAG, self.reference_kind, self.reference_index) - - def __repr__(self): - return ( - f'' - ) - - -class MethodType(Constant): - __slots__ = ('descriptor_index',) - TAG = 16 - - def __init__(self, pool, index, descriptor_index): - super().__init__(pool, index) - self.descriptor_index = descriptor_index - - @property - def descriptor(self): - return self.pool.get(self.descriptor_index) - - def pack(self): - return pack('>BH', self.TAG, self.descriptor_index) - - def __repr__(self): - return f'' - - -class InvokeDynamic(Constant): - __slots__ = ('bootstrap_method_attr_index', 'name_and_type_index') - TAG = 18 - - def __init__(self, pool, index, bootstrap_method_attr_index, - name_and_type_index): - super().__init__(pool, index) - self.bootstrap_method_attr_index = bootstrap_method_attr_index - self.name_and_type_index = name_and_type_index - - @property - def method_attr_index(self): - return self.bootstrap_method_attr_index - - @property - def name_and_type(self): - return self.pool[self.name_and_type_index] - - def pack(self): - return pack( - '>BHH', - self.TAG, - self.bootstrap_method_attr_index, - self.name_and_type_index - ) - - def __repr__(self): - return ( - f'' - ) - - -class Module(ConstantClass): - __slots__ = ('name_index',) - TAG = 19 - - def __repr__(self): - return f'' - - -class PackageInfo(ConstantClass): - __slots__ = ('name_index',) - TAG = 20 - - def __repr__(self): - return f'' - - -_constant_types = ( - None, - UTF8, - None, - Integer, - Float, - Long, - Double, - ConstantClass, - String, - FieldReference, - MethodReference, - InterfaceMethodRef, - NameAndType, - None, - None, - MethodHandle, - MethodType, - None, - InvokeDynamic, - Module, - PackageInfo -) - - -# The format and size-on-disk of each type of constant -# in the constant pool. -_constant_fmts = ( - None, None, None, - ('>i', 4), - ('>f', 4), - ('>q', 8), - ('>d', 8), - ('>H', 2), - ('>H', 2), - ('>HH', 4), - ('>HH', 4), - ('>HH', 4), - ('>HH', 4), - None, - None, - ('>BH', 3), - ('>H', 2), - None, - ('>HH', 4) -) - - -class ConstantPool(object): - def __init__(self): - self._pool = [None] - - def append(self, constant): - """ - Appends a new constant to the end of the pool. - """ - self._pool.append(constant) - - def __iter__(self): - for index, constant in enumerate(self._pool): - if constant is not None: - yield self.get(index) - - def get(self, index): - """ - Returns the `Constant` at `index`, raising a KeyError if it - does not exist. - """ - constant = self._pool[index] - if not isinstance(constant, Constant): - constant = _constant_types[constant[0]](self, index, *constant[1:]) - self._pool[index] = constant - return constant - - def __getitem__(self, idx): - return self.get(idx) - - def __setitem__(self, idx, value): - self._pool[idx] = value - - def find(self, type_=None, f=None): - """ - Iterates over the pool, yielding each matching ``Constant``. Calling - without any arguments is equivalent to iterating over the pool. - - :param type_: Any subclass of :class:`Constant` or ``None``. - :param f: Any callable which takes one argument (the constant). - """ - for constant in self: - if type_ is not None and not isinstance(constant, type_): - continue - - if f is not None and not f(constant): - continue - - yield constant - - def find_one(self, *args, **kwargs): - """ - Same as ``find()`` but returns only the first result, or `None` if - nothing was found. - """ - try: - return next(self.find(*args, **kwargs)) - except StopIteration: - return None - - def create_utf8(self, value): - """ - Creates a new :class:`ConstantUTF8`, adding it to the pool and - returning it. - - :param value: The value of the new UTF8 string. - """ - self.append((1, value)) - return self.get(self.raw_count - 1) - - def create_integer(self, value: int) -> Integer: - """ - Creates a new :class:`ConstantInteger`, adding it to the pool and - returning it. - - :param value: The value of the new integer. - """ - self.append((3, value)) - return self.get(self.raw_count - 1) - - def create_float(self, value: float) -> Float: - """ - Creates a new :class:`ConstantFloat`, adding it to the pool and - returning it. - - :param value: The value of the new float. - """ - self.append((4, value)) - return self.get(self.raw_count - 1) - - def create_long(self, value: int) -> Long: - """ - Creates a new :class:`ConstantLong`, adding it to the pool and - returning it. - - :param value: The value of the new long. - """ - self.append((5, value)) - self.append(None) - return self.get(self.raw_count - 2) - - def create_double(self, value: float) -> Double: - """ - Creates a new :class:`ConstantDouble`, adding it to the pool and - returning it. - - :param value: The value of the new Double. - """ - self.append((6, value)) - self.append(None) - return self.get(self.raw_count - 2) - - def create_class(self, name: str) -> ConstantClass: - """ - Creates a new :class:`ConstantClass`, adding it to the pool and - returning it. - - :param name: The name of the new class. - """ - self.append(( - 7, - self.create_utf8(name).index - )) - return self.get(self.raw_count - 1) - - def create_string(self, value: str) -> String: - """ - Creates a new :class:`ConstantString`, adding it to the pool and - returning it. - - :param value: The value of the new string as a UTF8 string. - """ - self.append(( - 8, - self.create_utf8(value).index - )) - return self.get(self.raw_count - 1) - - def create_name_and_type(self, name: str, descriptor: str) -> NameAndType: - """ - Creates a new :class:`ConstantNameAndType`, adding it to the pool and - returning it. - - :param name: The name of the class. - :param descriptor: The descriptor for `name`. - """ - self.append(( - 12, - self.create_utf8(name).index, - self.create_utf8(descriptor).index - )) - return self.get(self.raw_count - 1) - - def create_field_ref(self, class_: str, field: str, descriptor: str) \ - -> FieldReference: - """ - Creates a new :class:`ConstantFieldRef`, adding it to the pool and - returning it. - - :param class_: The name of the class to which `field` belongs. - :param field: The name of the field. - :param descriptor: The descriptor for `field`. - """ - self.append(( - 9, - self.create_class(class_).index, - self.create_name_and_type(field, descriptor).index - )) - return self.get(self.raw_count - 1) - - def create_method_ref(self, class_: str, method: str, descriptor: str) \ - -> MethodReference: - """ - Creates a new :class:`ConstantMethodRef`, adding it to the pool and - returning it. - - :param class_: The name of the class to which `method` belongs. - :param method: The name of the method. - :param descriptor: The descriptor for `method`. - """ - self.append(( - 10, - self.create_class(class_).index, - self.create_name_and_type(method, descriptor).index - )) - return self.get(self.raw_count - 1) - - def create_interface_method_ref(self, class_: str, if_method: str, - descriptor: str) -> InterfaceMethodRef: - """ - Creates a new :class:`ConstantInterfaceMethodRef`, adding it to the - pool and returning it. - - :param class_: The name of the class to which `if_method` belongs. - :param if_method: The name of the interface method. - :param descriptor: The descriptor for `if_method`. - """ - self.append(( - 11, - self.create_class(class_).index, - self.create_name_and_type(if_method, descriptor).index - )) - return self.get(self.raw_count - 1) - - def unpack(self, fio): - """ - Read the ConstantPool from the file-like object `fio`. - - .. note:: - - Advanced usage only. You will typically never need to call this - method as it will be called for you when loading a ClassFile. - - :param fio: Any file-like object providing `read()` - """ - # Reads in the ConstantPool (constant_pool in the JVM Spec) - constant_pool_count = unpack('>H', fio.read(2))[0] - - # Pull this locally so CPython doesn't do a lookup each time. - read = fio.read - - while constant_pool_count > 1: - constant_pool_count -= 1 - # The 1-byte prefix identifies the type of constant. - tag = ord(read(1)) - - if tag == 1: - # CONSTANT_Utf8_info, a length prefixed UTF-8-ish string. - # Only attempt to properly decode the MUTF8 if it fails - # regular UTF8 decoding, which overs huge time savings over - # large JARs. - utf8_str = read(unpack('>H', read(2))[0]) - try: - utf8_str = utf8_str.decode('utf8') - except UnicodeDecodeError: - utf8_str = decode_modified_utf8(utf8_str) - self.append((tag, utf8_str)) - else: - # Every other constant type is trivial. - fmt, size = _constant_fmts[tag] - self.append((tag, *unpack(fmt, read(size)))) - if tag == 5 or tag == 6: - # LONG (5) and DOUBLE (6) count as two entries in the - # pool. - self.append(None) - constant_pool_count -= 1 - - def pack(self, fout): - """ - Write the ConstantPool to the file-like object `fout`. - - .. note:: - - Advanced usage only. You will typically never need to call this - method as it will be calle=d for you when saving a ClassFile. - - :param fout: Any file-like object providing `write()` - """ - write = fout.write - write(pack('>H', self.raw_count)) - - for constant in self: - write(constant.pack()) - - def __len__(self) -> int: - """ - The number of `Constants` in the `ConstantPool`, excluding padding. - """ - count = 0 - for constant in self._pool: - if constant is not None: - count += 1 - return count - - @property - def raw_count(self) -> int: - """ - The number of `Constants` in the `ConstantPool`, including padding. - """ - return len(self._pool) diff --git a/jawa/util/flags.py b/jawa/util/flags.py deleted file mode 100644 index e847eb2..0000000 --- a/jawa/util/flags.py +++ /dev/null @@ -1,62 +0,0 @@ -__all__ = ('Flags',) -import struct - - -class Flags(object): - """ - Convenience class for handling bit flags. - """ - def __init__(self, binary_format, flags): - object.__setattr__(self, 'binary_format', binary_format) - object.__setattr__(self, 'flags', flags) - object.__setattr__(self, '_value', 0) - object.__setattr__(self, '_cache', struct.Struct(binary_format)) - - def pack(self): - """ - A shortcut for `struct.pack(flag.binary_format, flag.value)`. - """ - return self._cache.pack(self.value) - - @property - def value(self): - """ - The numeric value of the bitfield. - """ - return self._value - - def unpack(self, source): - """ - A shortcut for `struct.unpack(flag.binary_format, )`. - """ - self._value = self._cache.unpack(source)[0] - - def get(self, name): - """ - Returns the value of the field `name`. - """ - return bool(self.flags[name] & self.value) - - def set(self, name, value): - """ - Sets the value of the field `name` to `value`, which is `True` or - `False`. - """ - flag = self.flags[name] - self._value = (self.value | flag) if value else (self.value & ~flag) - - def __getattr__(self, attr): - if attr not in self.flags: - return object.__getattr__(self, attr) - return self.get(attr) - - def __setattr__(self, attr, value): - if attr not in self.flags: - return object.__setattr__(self, attr, value) - self.set(attr, value) - - def to_dict(self): - """ - Returns this `Flags` object's fields as a dictionary. - """ - return dict((k, self.get(k)) for k in self.flags.keys()) diff --git a/jawa/util/stream.py b/jawa/util/stream.py deleted file mode 100644 index 77c3897..0000000 --- a/jawa/util/stream.py +++ /dev/null @@ -1,43 +0,0 @@ -from struct import unpack_from, calcsize - - -class BufferStreamReader(object): - """Stream-like reader over a buffer mimicing the JVM spec types. - """ - def __init__(self, buff, starting_offset=0): - self.pos = starting_offset - self.buff = buff - - def u1(self): - r = unpack_from('B', self.buff, offset=self.pos) - self.pos += 1 - return r[0] - - def u2(self): - r = unpack_from('>H', self.buff, offset=self.pos) - self.pos += 2 - return r[0] - - def u4(self): - r = unpack_from('>I', self.buff, offset=self.pos) - self.pos += 4 - return r[0] - - def unpack(self, fmt): - size = calcsize(fmt) - r = unpack_from(fmt, self.buff, offset=self.pos) - self.pos += size - return r - - def seek(self, pos): - self.pos = pos - - def read(self, length=None): - if length is None: - r = self.buff[self.pos:] - self.pos = len(self.buff) - return r - - r = self.buff[self.pos:self.pos+length] - self.pos += length - return r diff --git a/jawa/util/tracer.py b/jawa/util/tracer.py deleted file mode 100644 index e69de29..0000000 diff --git a/jawa/util/utf.py b/jawa/util/utf.py deleted file mode 100644 index 15d27bc..0000000 --- a/jawa/util/utf.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -Utility methods for handling oddities in character encoding encountered -when parsing and writing JVM ClassFiles or object serialization archives. - -.. note:: - - http://bugs.python.org/issue2857 was an attempt in 2008 to get support - for MUTF-8/CESU-8 into the python core. -""" - - -def decode_modified_utf8(s: bytes) -> str: - """ - Decodes a bytestring containing modified UTF-8 as defined in section - 4.4.7 of the JVM specification. - - :param s: bytestring to be converted. - :returns: A unicode representation of the original string. - """ - s = bytearray(s) - buff = [] - buffer_append = buff.append - ix = 0 - while ix < len(s): - x = s[ix] - ix += 1 - - if x >> 7 == 0: - # Just an ASCII character, nothing else to do. - pass - elif x >> 6 == 6: - y = s[ix] - ix += 1 - x = ((x & 0x1F) << 6) + (y & 0x3F) - elif x >> 4 == 14: - y, z = s[ix:ix+2] - ix += 2 - x = ((x & 0xF) << 12) + ((y & 0x3F) << 6) + (z & 0x3F) - elif x == 0xED: - v, w, x, y, z = s[ix:ix+6] - ix += 5 - x = 0x10000 + ( - ((v & 0x0F) << 16) + - ((w & 0x3F) << 10) + - ((y & 0x0F) << 6) + - (z & 0x3F) - ) - elif x == 0xC0 and s[ix] == 0x80: - ix += 1 - x = 0 - buffer_append(x) - return u''.join(chr(b) for b in buff) - - -def encode_modified_utf8(u: str) -> bytearray: - """ - Encodes a unicode string as modified UTF-8 as defined in section 4.4.7 - of the JVM specification. - - :param u: unicode string to be converted. - :returns: A decoded bytearray. - """ - final_string = bytearray() - - for c in [ord(char) for char in u]: - if c == 0x00 or (0x80 < c < 0x7FF): - final_string.extend([ - (0xC0 | (0x1F & (c >> 6))), - (0x80 | (0x3F & c))] - ) - elif c < 0x7F: - final_string.append(c) - elif 0x800 < c < 0xFFFF: - final_string.extend([ - (0xE0 | (0x0F & (c >> 12))), - (0x80 | (0x3F & (c >> 6))), - (0x80 | (0x3F & c))] - ) - - return final_string diff --git a/lawu.png b/lawu.png new file mode 100644 index 0000000..a948a07 Binary files /dev/null and b/lawu.png differ diff --git a/lawu/__init__.py b/lawu/__init__.py new file mode 100644 index 0000000..13f0358 --- /dev/null +++ b/lawu/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +def instructions(): + return diff --git a/jawa/assemble.py b/lawu/assemble.py similarity index 80% rename from jawa/assemble.py rename to lawu/assemble.py index 9f5b102..dbbc5d4 100644 --- a/jawa/assemble.py +++ b/lawu/assemble.py @@ -1,15 +1,10 @@ from collections import namedtuple -from jawa.constants import Constant -from jawa.util.bytecode import ( - Operand, - OperandTypes, - Instruction, - opcode_table -) +from lawu.constants import Constant +from lawu.util.bytecode import Operand, OperandTypes, Instruction, opcode_table -Label = namedtuple('Label', ['name']) +Label = namedtuple("Label", ["name"]) def assemble(code): @@ -18,12 +13,12 @@ def assemble(code): A convienience over constructing individual Instruction and Operand objects, the output of this function can be directly piped to - :class:`~jawa.attributes.code.CodeAttribute.assemble()` to produce + :class:`~lawu.attributes.code.CodeAttribute.assemble()` to produce executable bytecode. As a simple example, lets produce an infinite loop: - >>> from jawa.assemble import assemble, Label + >>> from lawu.assemble import assemble, Label >>> print(list(assemble(( ... Label('start'), ... ('goto', Label('start')) @@ -45,7 +40,7 @@ def assemble(code): continue mnemonic, operands = line[0], line[1:] - operand_fmts = opcode_table[mnemonic]['operands'] + operand_fmts = opcode_table[mnemonic]["operands"] # We need to coerce each opcodes operands into their # final `Operand` form. @@ -56,10 +51,9 @@ def assemble(code): final_operands.append(operand) elif isinstance(operand, Constant): # Convert constants into CONSTANT_INDEX'es - final_operands.append(Operand( - OperandTypes.CONSTANT_INDEX, - operand.index - )) + final_operands.append( + Operand(OperandTypes.CONSTANT_INDEX, operand.index) + ) elif isinstance(operand, dict): # lookupswitch's operand is a dict as # a special usability case. @@ -69,10 +63,7 @@ def assemble(code): else: # For anything else, lookup that opcode's operand # type from its definition. - final_operands.append(Operand( - operand_fmts[i][1], - operand - )) + final_operands.append(Operand(operand_fmts[i][1], operand)) # Build the final, immutable `Instruction`. final.append(Instruction.create(mnemonic, final_operands)) @@ -105,10 +96,7 @@ def assemble(code): if isinstance(v, Label): operand[k] = Operand(40, label_pcs[v.name] - current_pc) elif isinstance(operand, Label): - ins.operands[i] = Operand( - 40, - label_pcs[operand.name] - current_pc - ) + ins.operands[i] = Operand(40, label_pcs[operand.name] - current_pc) current_pc += ins.size_on_disk(current_pc) diff --git a/jawa/attribute.py b/lawu/attribute.py similarity index 79% rename from jawa/attribute.py rename to lawu/attribute.py index 62e5ce5..29ededb 100644 --- a/jawa/attribute.py +++ b/lawu/attribute.py @@ -1,19 +1,19 @@ import inspect import pkgutil import importlib -from typing import IO, Callable, Iterator, Union, Dict, Any, Tuple +from typing import BinaryIO, Callable, Iterator, Union, Dict, Any, Tuple from struct import unpack, pack from itertools import repeat -from jawa.constants import UTF8 -from jawa.util.stream import BufferStreamReader +from lawu.constants import UTF8 +from lawu.util.stream import BufferStreamReader class Attribute(object): ADDED_IN: int = None MINIMUM_CLASS_VERSION: Tuple[int, int] = None - def __init__(self, parent: 'AttributeTable', name_index: int): + def __init__(self, parent: "AttributeTable", name_index: int): self.parent = parent self.name_index = name_index @@ -45,11 +45,11 @@ def pack(self) -> bytes: class UnknownAttribute(Attribute): - def __init__(self, parent: 'AttributeTable', name_index: int): + def __init__(self, parent: "AttributeTable", name_index: int): super().__init__(parent, name_index) self.info = None - def unpack(self, info: Union[bytes, BufferStreamReader]): + def unpack(self, info: BufferStreamReader): self.info = info def pack(self) -> bytes: @@ -57,14 +57,14 @@ def pack(self) -> bytes: class AttributeTable(object): - def __init__(self, cf, parent: Attribute=None): + def __init__(self, cf, parent: Attribute = None): #: The ClassFile that ultimately owns this AttributeTable. self.cf = cf #: The parent Attribute, if one exists. self.parent = parent self._table = [] - def unpack(self, source: IO): + def unpack(self, source: BinaryIO): """ Read the ConstantPool from the file-like object `source`. @@ -75,9 +75,9 @@ def unpack(self, source: IO): :param source: Any file-like object providing `read()` """ - count = unpack('>H', source.read(2))[0] + count = unpack(">H", source.read(2))[0] for _ in repeat(None, count): - name_index, length = unpack('>HI', source.read(6)) + name_index, length = unpack(">HI", source.read(6)) info_blob = source.read(length) self._table.append((name_index, info_blob)) @@ -90,17 +90,14 @@ def __getitem__(self, key): attribute_type = ATTRIBUTE_CLASSES.get(name, UnknownAttribute) self._table[key] = attr = attribute_type(self, name_index) - if attribute_type is UnknownAttribute: - attr.unpack(info) - else: - attr.unpack(BufferStreamReader(info)) + attr.unpack(BufferStreamReader(info)) return attr def __len__(self): return len(self._table) - def pack(self, out: IO): + def pack(self, out: BinaryIO): """ Write the AttributeTable to the file-like object `out`. @@ -111,14 +108,10 @@ def pack(self, out: IO): :param out: Any file-like object providing `write()` """ - out.write(pack('>H', len(self._table))) + out.write(pack(">H", len(self._table))) for attribute in self: info = attribute.pack() - out.write(pack( - '>HI', - attribute.name.index, - len(info) - )) + out.write(pack(">HI", attribute.name.index, len(info))) out.write(info) def create(self, type_, *args, **kwargs) -> Any: @@ -130,7 +123,7 @@ def create(self, type_, *args, **kwargs) -> Any: self._table.append(attribute) return attribute - def find(self, *, name: str=None, f: Callable=None) -> Iterator[Any]: + def find(self, *, name: str = None, f: Callable = None) -> Iterator[Any]: for idx, attribute in enumerate(self._table): if name is not None: # Optimization to filter solely on name without causing @@ -163,8 +156,7 @@ def get_attribute_classes() -> Dict[str, Attribute]: Lookup all builtin Attribute subclasses, load them, and return a dict """ attribute_children = pkgutil.iter_modules( - importlib.import_module('jawa.attributes').__path__, - prefix='jawa.attributes.' + importlib.import_module("lawu.attributes").__path__, prefix="lawu.attributes." ) result = {} @@ -172,13 +164,12 @@ def get_attribute_classes() -> Dict[str, Attribute]: classes = inspect.getmembers( importlib.import_module(name), lambda c: ( - inspect.isclass(c) and issubclass(c, Attribute) and - c is not Attribute - ) + inspect.isclass(c) and issubclass(c, Attribute) and c is not Attribute + ), ) for class_name, class_ in classes: - attribute_name = getattr(class_, 'ATTRIBUTE_NAME', class_name[:-9]) + attribute_name = getattr(class_, "ATTRIBUTE_NAME", class_name[:-9]) result[attribute_name] = class_ return result diff --git a/jawa/attributes/__init__.py b/lawu/attributes/__init__.py similarity index 73% rename from jawa/attributes/__init__.py rename to lawu/attributes/__init__.py index 871304d..20555e9 100644 --- a/jawa/attributes/__init__.py +++ b/lawu/attributes/__init__.py @@ -3,12 +3,12 @@ ====================== In addition to standard JVM attributes various compilers, debuggers, and other -tools may insert additional unknown attributes into a :class:`~jawa.cf +tools may insert additional unknown attributes into a :class:`~lawu.cf .ClassFile`. Not all standard attributes are currently implemented. When an unknown -attribute is encountered, an :class:`~jawa.attribute.UnknownAttribute` object +attribute is encountered, an :class:`~lawu.attribute.UnknownAttribute` object is created instead. This UnknownAttribute retains the name and content of the original attribute, allowing you to parse it yourself or to simply pass it through. -""" \ No newline at end of file +""" diff --git a/lawu/attributes/bootstrap.py b/lawu/attributes/bootstrap.py new file mode 100644 index 0000000..c75fede --- /dev/null +++ b/lawu/attributes/bootstrap.py @@ -0,0 +1,48 @@ +import io +from collections import namedtuple +from itertools import repeat +from struct import pack + +from lawu.attribute import Attribute + +BootstrapMethod = namedtuple("BootstrapMethod", ["method_ref", "bootstrap_args"]) + + +class BootstrapMethodsAttribute(Attribute): + ADDED_IN = "7" + MINIMUM_CLASS_VERSION = (51, 0) + + def __init__(self, table, name_index=None): + super(BootstrapMethodsAttribute, self).__init__( + table, + name_index or table.cf.constants.create_utf8("BootstrapMethods").index, + ) + self.table = [] + + def __repr__(self): + return "".format(self=self) + + def pack(self): + out = io.BytesIO() + out.write(pack(">H", len(self.table))) + + for table_entry in self.table: + out.write( + pack(">HH", table_entry.method_ref, len(table_entry.bootstrap_args)) + ) + out.write( + pack( + ">{0}H".format(len(table_entry.bootstrap_args)), + table_entry.bootstrap_args, + ) + ) + + return out.getvalue() + + def unpack(self, info): + length = info.u2() + + for _ in repeat(None, length): + self.table.append( + BootstrapMethod(info.u2(), info.unpack(">{0}H".format(info.u2()))) + ) diff --git a/jawa/attributes/code.py b/lawu/attributes/code.py similarity index 70% rename from jawa/attributes/code.py rename to lawu/attributes/code.py index 8801fdb..16e6dea 100644 --- a/jawa/attributes/code.py +++ b/lawu/attributes/code.py @@ -6,16 +6,13 @@ from itertools import repeat from collections import namedtuple -from jawa.attribute import Attribute, AttributeTable -from jawa.util.bytecode import ( - read_instruction, - write_instruction, - Instruction -) +from lawu.constants import UTF8 +from lawu.attribute import Attribute, AttributeTable +from lawu.util.bytecode import read_instruction, write_instruction, Instruction -CodeException = namedtuple('CodeException', [ - 'start_pc', 'end_pc', 'handler_pc', 'catch_type' -]) +CodeException = namedtuple( + "CodeException", ["start_pc", "end_pc", "handler_pc", "catch_type"] +) class CodeAttribute(Attribute): @@ -27,17 +24,17 @@ class CodeAttribute(Attribute): .. code-block:: python - from jawa import ClassFile - from jawa.util.bytecode import Instruction + from lawu import ClassFile + from lawu.util.bytecode import Instruction - cf = ClassFile.create('HelloWorld') + cf = ClassFile(name='HelloWorld') main = cf.methods.create( # The name of the method 'main', # The signature of the method '([Ljava/lang/String;)V', - # Tell Jawa to automatically create an empty CodeAttribute for + # Tell Lawu to automatically create an empty CodeAttribute for # us to use. code=True ) @@ -51,21 +48,19 @@ class CodeAttribute(Attribute): with open('HelloWorld.class', 'wb') as fout: cf.save(fout) """ - ADDED_IN = '1.0.2' + + ADDED_IN = "1.0.2" MINIMUM_CLASS_VERSION = (45, 3) def __init__(self, table, name_index=None): - super(CodeAttribute, self).__init__( - table, - name_index or table.cf.constants.create_utf8( - 'Code' - ).index + super().__init__( + table, name_index or UTF8(pool=table.cf.constants, value="Code").index ) self.max_stack = 0 self.max_locals = 0 self.exception_table = [] self.attributes = AttributeTable(table.cf, parent=self) - self._code = '' + self._code = b"" def unpack(self, info): """ @@ -78,15 +73,14 @@ def unpack(self, info): :param info: A byte string containing an unparsed CodeAttribute. """ - self.max_stack, self.max_locals, c_len = info.unpack('>HHI') + self.max_stack, self.max_locals, c_len = info.unpack(">HHI") self._code = info.read(c_len) # The exception table ex_table_len = info.u2() for _ in repeat(None, ex_table_len): - self.exception_table.append(CodeException( - *info.unpack('>HHHH') - )) + self.exception_table.append(CodeException(*info.unpack(">HHHH"))) + self.attributes = AttributeTable(self.cf, parent=self) self.attributes.unpack(info) @@ -95,24 +89,21 @@ def pack(self): The `CodeAttribute` in packed byte string form. """ with io.BytesIO() as file_out: - file_out.write(pack( - '>HHI', - self.max_stack, - self.max_locals, - len(self._code) - )) + file_out.write( + pack(">HHI", self.max_stack, self.max_locals, len(self._code)) + ) file_out.write(self._code) - file_out.write(pack('>H', len(self.exception_table))) + file_out.write(pack(">H", len(self.exception_table))) for exception in self.exception_table: - file_out.write(pack('>HHHH', *exception)) + file_out.write(pack(">HHHH", *exception)) self.attributes.pack(file_out) return file_out.getvalue() def assemble(self, code): """ - Assembles an iterable of :class:`~jawa.util.bytecode.Instruction` + Assembles an iterable of :class:`~lawu.util.bytecode.Instruction` objects into a method's code body. """ with io.BytesIO() as code_out: @@ -123,7 +114,7 @@ def assemble(self, code): def disassemble(self, *, transforms=None) -> Iterator[Instruction]: """ Disassembles this method, yielding an iterable of - :class:`~jawa.util.bytecode.Instruction` objects. + :class:`~lawu.util.bytecode.Instruction` objects. """ if transforms is None: if self.cf.classloader: @@ -144,8 +135,9 @@ def _bind_transform(self, transform): sig = inspect.signature(transform, follow_wrapped=True) return functools.partial( transform, - **{k: v for k, v in { - 'cf': self.cf, - 'attribute': self - }.items() if k in sig.parameters} + **{ + k: v + for k, v in {"cf": self.cf, "attribute": self}.items() + if k in sig.parameters + }, ) diff --git a/jawa/attributes/constant_value.py b/lawu/attributes/constant_value.py similarity index 67% rename from jawa/attributes/constant_value.py rename to lawu/attributes/constant_value.py index e564f08..1ade4dd 100644 --- a/jawa/attributes/constant_value.py +++ b/lawu/attributes/constant_value.py @@ -1,17 +1,14 @@ from struct import pack -from jawa.attribute import Attribute +from lawu.attribute import Attribute class ConstantValueAttribute(Attribute): - ADDED_IN = '1.0.2' + ADDED_IN = "1.0.2" MINIMUM_CLASS_VERSION = (45, 3) def __init__(self, table, value=None, name_index=None): super(ConstantValueAttribute, self).__init__( - table, - name_index or table.cf.constants.create_utf8( - 'ConstantValue' - ).index + table, name_index or table.cf.constants.create_utf8("ConstantValue").index ) self._constant_value_index = value.index if value else None @@ -19,7 +16,7 @@ def unpack(self, info): self._constant_value_index = info.u2() def pack(self): - return pack('>H', self._constant_value_index) + return pack(">H", self._constant_value_index) @property def constant_value(self): diff --git a/jawa/attributes/deprecated.py b/lawu/attributes/deprecated.py similarity index 55% rename from jawa/attributes/deprecated.py rename to lawu/attributes/deprecated.py index 29f8cbb..47c0f99 100644 --- a/jawa/attributes/deprecated.py +++ b/lawu/attributes/deprecated.py @@ -1,20 +1,17 @@ -from jawa.attribute import Attribute +from lawu.attribute import Attribute class DeprecatedAttribute(Attribute): - ADDED_IN = '1.1.0' + ADDED_IN = "1.1.0" MINIMUM_CLASS_VERSION = (45, 3) def __init__(self, table, name_index=None): super(DeprecatedAttribute, self).__init__( - table, - name_index or table.cf.constants.create_utf8( - 'Deprecated' - ).index + table, name_index or table.cf.constants.create_utf8("Deprecated").index ) def __repr__(self): - return '' + return "" def pack(self): pass diff --git a/jawa/attributes/enclosing_method.py b/lawu/attributes/enclosing_method.py similarity index 59% rename from jawa/attributes/enclosing_method.py rename to lawu/attributes/enclosing_method.py index 0c989a5..0bb6b5d 100644 --- a/jawa/attributes/enclosing_method.py +++ b/lawu/attributes/enclosing_method.py @@ -1,24 +1,21 @@ from struct import pack -from jawa.attribute import Attribute +from lawu.attribute import Attribute class EnclosingMethodAttribute(Attribute): - ADDED_IN = '5.0.0' + ADDED_IN = "5.0.0" MINIMUM_CLASS_VERSION = (49, 0) def __init__(self, table, name_index=None): super().__init__( - table, - name_index or table.cf.constants.create_utf8( - 'EnclosingMethod' - ).index + table, name_index or table.cf.constants.create_utf8("EnclosingMethod").index ) self.class_index = None self.method_index = None def pack(self): - return pack('>HH', self.class_index, self.method_index) + return pack(">HH", self.class_index, self.method_index) def unpack(self, info): self.class_index = info.u2() diff --git a/lawu/attributes/exceptions.py b/lawu/attributes/exceptions.py new file mode 100644 index 0000000..2d6607c --- /dev/null +++ b/lawu/attributes/exceptions.py @@ -0,0 +1,28 @@ +from struct import pack + +from lawu.attribute import Attribute + + +class ExceptionsAttribute(Attribute): + ADDED_IN = "1.0.2" + MINIMUM_CLASS_VERSION = (45, 3) + + def __init__(self, table, name_index=None): + super(ExceptionsAttribute, self).__init__( + table, name_index or table.cf.constants.create_utf8("Exceptions").index + ) + self.exceptions = [] + + def unpack(self, info): + length = info.u2() + self.exceptions = list(info.unpack(">{0}H".format(length))) + + def pack(self): + return pack( + ">H{0}H".format(len(self.exceptions)), + len(self.exceptions), + *self.exceptions, + ) + + def __repr__(self): + return "".format(self.exceptions) diff --git a/lawu/attributes/inner_classes.py b/lawu/attributes/inner_classes.py new file mode 100644 index 0000000..6dcb05c --- /dev/null +++ b/lawu/attributes/inner_classes.py @@ -0,0 +1,38 @@ +import io +from struct import pack +from itertools import repeat +from collections import namedtuple +from lawu.attribute import Attribute + + +InnerClass = namedtuple( + "InnerClass", + [ + "inner_class_info_index", + "outer_class_info_index", + "inner_name_index", + "inner_class_access_flags", + ], +) + + +class InnerClassesAttribute(Attribute): + ADDED_IN = "1.1.0" + MINIMUM_CLASS_VERSION = (45, 3) + + def __init__(self, table, name_index): + super().__init__( + table, name_index or table.cf.constants.create_utf8("InnerClasses").index + ) + self.inner_classes = [] + + def unpack(self, info): + for _ in repeat(None, info.u2()): + self.inner_classes.append(InnerClass(*info.unpack(">HHHH"))) + + def pack(self): + with io.BytesIO() as out: + out.write(pack(">H", len(self.inner_classes))) + for inner_class in self.inner_classes: + out.write(pack(">HHHH", *inner_class)) + return out.getvalue() diff --git a/lawu/attributes/line_number_table.py b/lawu/attributes/line_number_table.py new file mode 100644 index 0000000..077689d --- /dev/null +++ b/lawu/attributes/line_number_table.py @@ -0,0 +1,34 @@ +from struct import pack +from collections import namedtuple + +from lawu.attribute import Attribute + + +line_number_entry = namedtuple("line_number_entry", "start_pc line_number") + + +class LineNumberTableAttribute(Attribute): + ADDED_IN = "1.0.2" + MINIMUM_CLASS_VERSION = (45, 3) + + def __init__(self, table, name_index=None): + super(LineNumberTableAttribute, self).__init__( + table, name_index or table.cf.constants.create_utf8("LineNumberTable").index + ) + self.line_no = [] + + def unpack(self, info): + length = info.u2() + table = info.unpack(">{0}H".format(length * 2)) + + self.line_no = [line_number_entry(*x) for x in zip(*[iter(table)] * 2)] + + def pack(self): + return pack( + ">H{0}H".format(len(self.line_no) * 2), + len(self.line_no), + *sum(self.line_no, ()), + ) + + def __repr__(self): + return "".format(self.line_no) diff --git a/lawu/attributes/local_variable.py b/lawu/attributes/local_variable.py new file mode 100644 index 0000000..fc0748d --- /dev/null +++ b/lawu/attributes/local_variable.py @@ -0,0 +1,40 @@ +from struct import pack +from collections import namedtuple + +from lawu.attribute import Attribute + + +local_variable_entry = namedtuple( + "local_variable_entry", + ["start_pc", "length", "name_index", "descriptor_index", "index"], +) + + +class LocalVariableTableAttribute(Attribute): + ADDED_IN = "1.0.2" + MINIMUM_CLASS_VERSION = (45, 3) + + def __init__(self, table, name_index=None): + super().__init__( + table, + name_index or table.cf.constants.create_utf8("LocalVariableTable").index, + ) + self.local_variables = [] + + def unpack(self, info): + length = info.u2() + table = info.unpack(">{0}H".format(length * 5)) + + self.local_variables = [ + local_variable_entry(*x) for x in zip(*[iter(table)] * 5) + ] + + def pack(self): + return pack( + ">H{0}H".format(len(self.local_variables) * 5), + len(self.local_variables), + *sum(self.local_variables, ()), + ) + + def __repr__(self): + return f"" diff --git a/lawu/attributes/local_variable_type.py b/lawu/attributes/local_variable_type.py new file mode 100644 index 0000000..0c755b7 --- /dev/null +++ b/lawu/attributes/local_variable_type.py @@ -0,0 +1,41 @@ +from struct import pack +from collections import namedtuple + +from lawu.attribute import Attribute + + +local_variable_type_entry = namedtuple( + "local_variable_type_entry", + ["start_pc", "length", "name_index", "signature_index", "index"], +) + + +class LocalVariableTypeTableAttribute(Attribute): + ADDED_IN = "1.0.2" + MINIMUM_CLASS_VERSION = (45, 3) + + def __init__(self, table, name_index=None): + super().__init__( + table, + name_index + or table.cf.constants.create_utf8("LocalVariableTypeTable").index, + ) + self.local_variables = [] + + def unpack(self, info): + length = info.u2() + table = info.unpack(">{0}H".format(length * 5)) + + self.local_variables = [ + local_variable_type_entry(*x) for x in zip(*[iter(table)] * 5) + ] + + def pack(self): + return pack( + ">H{0}H".format(len(self.local_variables) * 5), + len(self.local_variables), + *sum(self.local_variables, ()), + ) + + def __repr__(self): + return f"" diff --git a/jawa/attributes/signature.py b/lawu/attributes/signature.py similarity index 69% rename from jawa/attributes/signature.py rename to lawu/attributes/signature.py index 4b05754..cc9cbb4 100644 --- a/jawa/attributes/signature.py +++ b/lawu/attributes/signature.py @@ -1,17 +1,14 @@ from struct import pack -from jawa.attribute import Attribute +from lawu.attribute import Attribute class SignatureAttribute(Attribute): - ADDED_IN = '5.0.0' + ADDED_IN = "5.0.0" MINIMUM_CLASS_VERSION = (49, 0) def __init__(self, table, name_index): super(SignatureAttribute, self).__init__( - table, - name_index or table.cf.constants.create_utf8( - 'Signature' - ).index + table, name_index or table.cf.constants.create_utf8("Signature").index ) self._signature_index = None @@ -19,7 +16,7 @@ def unpack(self, info): self._signature_index = info.u2() def pack(self): - return pack('>H', self._signature_index) + return pack(">H", self._signature_index) @property def signature(self): diff --git a/jawa/attributes/source_file.py b/lawu/attributes/source_file.py similarity index 69% rename from jawa/attributes/source_file.py rename to lawu/attributes/source_file.py index 55d60f8..31f3c25 100644 --- a/jawa/attributes/source_file.py +++ b/lawu/attributes/source_file.py @@ -1,17 +1,16 @@ from struct import pack -from jawa.attribute import Attribute + +from lawu.attribute import Attribute +from lawu.constants import UTF8 class SourceFileAttribute(Attribute): - ADDED_IN = '1.0.2' + ADDED_IN = "1.0.2" MINIMUM_CLASS_VERSION = (45, 3) def __init__(self, table, name_index=None): super(SourceFileAttribute, self).__init__( - table, - name_index or table.cf.constants.create_utf8( - u'SourceFile' - ).index + table, name_index or UTF8(pool=table.cf.constants, value="SourceFile").index ) self.source_file_index = None @@ -19,7 +18,7 @@ def unpack(self, info): self.source_file_index = info.u2() def pack(self): - return pack('>H', self.source_file_index) + return pack(">H", self.source_file_index) @property def source_file(self): diff --git a/jawa/attributes/stack_map_table.py b/lawu/attributes/stack_map_table.py similarity index 71% rename from jawa/attributes/stack_map_table.py rename to lawu/attributes/stack_map_table.py index 411fa81..3597900 100644 --- a/jawa/attributes/stack_map_table.py +++ b/lawu/attributes/stack_map_table.py @@ -1,22 +1,14 @@ from itertools import repeat -from jawa.attribute import Attribute -from jawa.util.verifier import VerificationTypes +from lawu.attribute import Attribute +from lawu.util.verifier import VerificationTypes # These types are followed by an additional u2. -TYPES_WITH_EXTRA = ( - VerificationTypes.ITEM_Object, - VerificationTypes.ITEM_Uninitialized -) +TYPES_WITH_EXTRA = (VerificationTypes.ITEM_Object, VerificationTypes.ITEM_Uninitialized) class StackMapFrame(object): - __slots__ = ( - 'frame_type', - 'frame_offset', - 'frame_locals', - 'frame_stack' - ) + __slots__ = ("frame_type", "frame_offset", "frame_locals", "frame_stack") def __init__(self, frame_type): self.frame_type = frame_type @@ -26,10 +18,10 @@ def __init__(self, frame_type): def __repr__(self): return ( - u'' + "" ).format(s=self) @@ -42,15 +34,13 @@ class StackMapTableAttribute(Attribute): generation of a StackMapTableAttribute requires a complete class hierarchy among other things. """ - ADDED_IN = '6.0.0' + + ADDED_IN = "6.0.0" MINIMUM_CLASS_VERSION = (50, 0) def __init__(self, table, name_index=None): super(StackMapTableAttribute, self).__init__( - table, - name_index or table.cf.constants.create_utf8( - 'StackMapTable' - ).index + table, name_index or table.cf.constants.create_utf8("StackMapTable").index ) self.frames = [] @@ -67,8 +57,7 @@ def unpack(self, info): if i == 0: frame.frame_offset = frame_type else: - frame.frame_offset = previous_frame.frame_offset + \ - frame_type + 1 + frame.frame_offset = previous_frame.frame_offset + frame_type + 1 frame.frame_locals = previous_frame.frame_locals self.frames.append(frame) @@ -79,13 +68,10 @@ def unpack(self, info): if i == 0: frame.frame_offset = frame_type - 64 else: - frame.frame_offset = previous_frame.frame_offset + \ - frame_type - 63 + frame.frame_offset = previous_frame.frame_offset + frame_type - 63 frame.frame_locals = previous_frame.frame_locals - frame.frame_stack = list( - self._unpack_verification_type_info(info, 1) - ) + frame.frame_stack = list(self._unpack_verification_type_info(info, 1)) self.frames.append(frame) previous_frame = frame @@ -103,63 +89,48 @@ def unpack(self, info): if i == 0: frame.frame_offset = frame_offset else: - frame.frame_offset = previous_frame.frame_offset + \ - frame_offset + 1 + frame.frame_offset = previous_frame.frame_offset + frame_offset + 1 frame.frame_locals = previous_frame.frame_locals - frame.frame_stack = list( - self._unpack_verification_type_info( - info, - 1 - ) - ) + frame.frame_stack = list(self._unpack_verification_type_info(info, 1)) elif frame_type < 251: # CHOP if i == 0: frame.frame_offset = frame_offset else: - frame.frame_offset = previous_frame.frame_offset + \ - frame_offset + 1 + frame.frame_offset = previous_frame.frame_offset + frame_offset + 1 frame.frame_locals = previous_frame.frame_locals[ - 0:251 - frame_type + 0 : 251 - frame_type ] elif frame_type == 251: # SAME_FRAME_EXTENDED if i == 0: frame.frame_offset = frame_offset else: - frame.frame_offset = previous_frame.frame_offset + \ - frame_offset + 1 + frame.frame_offset = previous_frame.frame_offset + frame_offset + 1 frame.frame_locals = previous_frame.frame_locals elif frame_type < 255: # APPEND if i == 0: frame.frame_offset = frame_offset else: - frame.frame_offset = previous_frame.frame_offset + \ - frame_offset + 1 + frame.frame_offset = previous_frame.frame_offset + frame_offset + 1 frame.frame_locals = previous_frame.frame_locals + list( - self._unpack_verification_type_info( - info, - frame_type - 251 - ) + self._unpack_verification_type_info(info, frame_type - 251) ) elif frame_type == 255: # FULL_FRAME if i == 0: frame.frame_offset = frame_offset else: - frame.frame_offset = previous_frame.frame_offset + \ - frame_offset + 1 - - frame.frame_locals = list(self._unpack_verification_type_info( - info, - info.u2() - )) - frame.frame_stack = list(self._unpack_verification_type_info( - info, - info.u2() - )) + frame.frame_offset = previous_frame.frame_offset + frame_offset + 1 + + frame.frame_locals = list( + self._unpack_verification_type_info(info, info.u2()) + ) + frame.frame_stack = list( + self._unpack_verification_type_info(info, info.u2()) + ) self.frames.append(frame) previous_frame = frame @@ -171,7 +142,7 @@ def _unpack_verification_type_info(info, count): for _ in repeat(None, count): tag = info.u1() if tag in TYPES_WITH_EXTRA: - yield (tag, info.u2()) + yield tag, info.u2() else: yield (tag,) diff --git a/jawa/attributes/synthetic.py b/lawu/attributes/synthetic.py similarity index 56% rename from jawa/attributes/synthetic.py rename to lawu/attributes/synthetic.py index 6cd2f6c..0f54089 100644 --- a/jawa/attributes/synthetic.py +++ b/lawu/attributes/synthetic.py @@ -1,16 +1,13 @@ -from jawa.attribute import Attribute +from lawu.attribute import Attribute class SyntheticAttribute(Attribute): - ADDED_IN = '5.0.0' + ADDED_IN = "5.0.0" MINIMUM_CLASS_VERSION = (49, 0) def __init__(self, table, name_index=None): super().__init__( - table, - name_index or table.cf.constants.create_utf8( - 'Synthetic' - ).index + table, name_index or table.cf.constants.create_utf8("Synthetic").index ) def pack(self): diff --git a/lawu/cf.py b/lawu/cf.py new file mode 100644 index 0000000..f28b2a1 --- /dev/null +++ b/lawu/cf.py @@ -0,0 +1,252 @@ +""" +ClassFile reader & writer. + +The :mod:`lawu.cf` module provides tools for working with JVM ``.class`` +ClassFiles. +""" + +from typing import BinaryIO, Iterable, Union, Sequence, Optional +from struct import pack, unpack +from collections import namedtuple +from enum import IntFlag + +from lawu.constants import ConstantPool, ConstantClass, UTF8 +from lawu.fields import FieldTable +from lawu.methods import MethodTable +from lawu.attribute import AttributeTable, ATTRIBUTE_CLASSES +from lawu.attributes.bootstrap import BootstrapMethod +from lawu.context import class_context + + +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) + + +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() + >>> with open('HelloWorld.class', 'wb') as out: + ... cf.save(out) + + :meth:`~ClassFile()` 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 + + class AccessFlags(IntFlag): + """ + Possible values for the ClassFile.access_flags field. + """ + + PUBLIC = 0x0001 + FINAL = 0x0010 + SUPER = 0x0020 + INTERFACE = 0x0200 + ABSTRACT = 0x0400 + SYNTHETIC = 0x1000 + ANNOTATION = 0x2000 + ENUM = 0x4000 + MODULE = 0x8000 + + def __init__( + self, + source: Optional[BinaryIO] = None, + *, + this: str = "HelloWorld", + super_: str = "java/lang/Object", + ): + # Default to J2SE_7 + self._version = ClassVersion(0x32, 0) + self.constants = ConstantPool() + self.access_flags = ClassFile.AccessFlags.PUBLIC | ClassFile.AccessFlags.SUPER + self.this = self.constants.add( + ConstantClass(pool=self.constants, name=self.constants.add(UTF8(this))) + ) + self.super_ = self.constants.add( + ConstantClass(pool=self.constants, name=self.constants.add(UTF8(super_))) + ) + self._interfaces = [] + self.fields = FieldTable(self) + self.methods = MethodTable(self) + self.attributes = AttributeTable(self) + #: The ClassLoader instance bound to this ClassFile, if any. + self.classloader = None + + if source: + self._from_io(source) + + def push_context(self): + """Push this ClassFile to the top of the class context. + + It's generally better to use the ClassFile as a context manager using + 'with' then to manage the stack manually:: + + >>> cf = ClassFile() + >>> with cf: + ... print('Context is automatically managed.') + """ + class_context().append(self) + + def pop_context(self): + """Pop this ClassFile off the top of the class context. + + It is a RuntimeError if the ClassFile popped off the top of the stack + is not _this_ ClassFile. + """ + cont = class_context() + if cont: + ctx = cont.pop() + if ctx is not self: + # This should never happen unless a user is manually managing + # the context stack instead of using the ClassFile as a context + # manager. + raise RuntimeError( + "A ClassFile tried to pop a context which was not itself." + ) + + def __enter__(self): + self.push_context() + + def __exit__(self, _, __, ___): + self.pop_context() + + def save(self, source: BinaryIO): + """ + 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(pack(">H", int(self.access_flags))) + write( + pack( + f">HHH{len(self._interfaces)}H", + self.this.index, + self.super_.index, + len(self._interfaces), + *self._interfaces, + ) + ) + + self.fields.pack(source) + self.methods.pack(source) + self.attributes.pack(source) + + def _from_io(self, source: BinaryIO): + """ + 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] + + # We created some default values when the class was constructed, just + # purge them. + self.constants.clear() + self.constants.unpack(source) + + # ClassFile access_flags, see section #4.1 of the JVM specs. + self.access_flags = unpack(">H", read(2)) + + # The CONSTANT_Class indexes for "this" class and its superclass. + # Interfaces are a simple list of CONSTANT_Class indexes. + this_, super_, interfaces_count = unpack(">HHH", read(6)) + self.this = self.constants[this_] + self.super_ = self.constants[super_] + + 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:`~lawu.cf.ClassVersion` for this class. + + Example:: + + >>> cf = ClassFile(this='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 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"" diff --git a/jawa/classloader.py b/lawu/classloader.py similarity index 86% rename from jawa/classloader.py rename to lawu/classloader.py index 04c556f..3962610 100644 --- a/jawa/classloader.py +++ b/lawu/classloader.py @@ -7,8 +7,8 @@ from collections import OrderedDict from contextlib import contextmanager -from jawa.cf import ClassFile -from jawa.constants import ConstantPool, ConstantClass +from lawu.cf import ClassFile +from lawu.constants import ConstantPool, ConstantClass def _walk(path, follow_links=False, maximum_depth=None): @@ -38,8 +38,14 @@ class ClassLoader(object): :param bytecode_transforms: Default transforms to apply when disassembling a method. """ - def __init__(self, *sources, max_cache: int=50, klass=ClassFile, - bytecode_transforms: Iterable[Callable]=None): + + def __init__( + self, + *sources, + max_cache: int = 50, + klass=ClassFile, + bytecode_transforms: Iterable[Callable] = None, + ): self.path_map = {} self.max_cache = max_cache self.class_cache = OrderedDict() @@ -55,12 +61,11 @@ def __getitem__(self, path: str) -> ClassFile: def __contains__(self, path: str) -> bool: if path in self.path_map: return True - elif path + '.class' in self.path_map: + elif path + ".class" in self.path_map: return True return False - def update(self, *sources, follow_symlinks: bool=False, - maximum_depth: int=20): + def update(self, *sources, follow_symlinks: bool = False, maximum_depth: int = 20): """Add one or more ClassFile sources to the class loader. If a given source is a directory path, it is traversed up to the @@ -89,14 +94,12 @@ def update(self, *sources, follow_symlinks: bool=False, # Explicit cast to str to support Path objects. source = str(source) - if source.lower().endswith(('.zip', '.jar')): - zf = ZipFile(source, 'r') + if source.lower().endswith((".zip", ".jar")): + zf = ZipFile(source, "r") self.path_map.update(zip(zf.namelist(), repeat(zf))) elif os.path.isdir(source): walker = _walk( - source, - follow_links=follow_symlinks, - maximum_depth=maximum_depth + source, follow_links=follow_symlinks, maximum_depth=maximum_depth ) for root, dirs, files in walker: for file_ in files: @@ -105,7 +108,7 @@ def update(self, *sources, follow_symlinks: bool=False, self.path_map[path_suffix] = path_full @contextmanager - def open(self, path: str, mode: str='r') -> IO: + def open(self, path: str, mode: str = "r") -> IO: """Open an IO-like object for `path`. .. note:: @@ -121,7 +124,7 @@ def open(self, path: str, mode: str='r') -> IO: raise FileNotFoundError() if isinstance(entry, str): - with open(entry, 'rb' if mode == 'r' else mode) as source: + with open(entry, "rb" if mode == "r" else mode) as source: yield source elif isinstance(entry, ZipFile): yield io.BytesIO(entry.read(path)) @@ -141,7 +144,7 @@ def load(self, path: str) -> ClassFile: try: r = self.class_cache.pop(path) except KeyError: - with self.open(f'{path}.class') as source: + with self.open(f"{path}.class") as source: r = self.klass(source) r.classloader = self @@ -172,10 +175,10 @@ def dependencies(self, path: str) -> Set[str]: :param path: Fully-qualified path to a ClassFile. """ - return set(c.name.value for c in self.search_constant_pool( - path=path, - type_=ConstantClass - )) + return set( + c.name.value + for c in self.search_constant_pool(path=path, type_=ConstantClass) + ) def search_constant_pool(self, *, path: str, **options): """Partially load the class at `path`, yield all matching constants @@ -187,7 +190,7 @@ def search_constant_pool(self, *, path: str, **options): :param path: Fully-qualified path to a ClassFile. :param options: A list of options to pass into `ConstantPool.find()` """ - with self.open(f'{path}.class') as source: + with self.open(f"{path}.class") as source: # Skip over the magic, minor, and major version. source.read(8) pool = ConstantPool() @@ -197,7 +200,4 @@ def search_constant_pool(self, *, path: str, **options): @property def classes(self) -> Iterator[str]: """Yield the name of all classes discovered in the path map.""" - yield from ( - c[:-6] - for c in self.path_map.keys() if c.endswith('.class') - ) + yield from (c[:-6] for c in self.path_map.keys() if c.endswith(".class")) diff --git a/lawu/constants.py b/lawu/constants.py new file mode 100644 index 0000000..5cbc1a8 --- /dev/null +++ b/lawu/constants.py @@ -0,0 +1,621 @@ +""" +Utilities for working with the ConstantPool found in JVM ClassFiles. +""" + +from typing import Dict, Any, Deque, BinaryIO, Union +from collections import deque +from struct import unpack, pack + +from mutf8 import decode_modified_utf8, encode_modified_utf8 + +from lawu import context + + +def _missing_elements(lst, start, end): + """Sublinear solution for finding gaps in a list of integers.""" + # From Lie Ryan, Stackoverflow. + if end - start <= 1: + if lst[end] - lst[start] > 1: + yield from range(lst[start] + 1, lst[end]) + return + + index = start + (end - start) // 2 + + # is the lower half consecutive? + consecutive_low = lst[index] == lst[start] + (index - start) + if not consecutive_low: + yield from _missing_elements(lst, start, index) + + # is the upper part consecutive? + consecutive_high = lst[index] == lst[end] - (end - index) + if not consecutive_high: + yield from _missing_elements(lst, index, end) + + +class Constant(object): + """ + The base class for all ``Constant*`` types. + """ + + __slots__ = ("pool", "index") + + #: The "tag" or leading byte of a constant that identifies its type. + TAG: int = None + + def __init__(self, *, pool=None, index=None): + #: The constants index in the pool that owns it. + self.index = index + #: The ConstantPool that owns this constant. + self.pool = pool + + # If no pool was specified, try to see if one is already pushed + # on the stack. + if pool is None: + cf = context.current_class_context() + if cf: + self.pool = cf.constants + + if self.pool: + self.pool.add(self, index=index) + + def pack(self) -> bytes: + """ + Pack the constant into a binary string, minus the tag. + """ + raise NotImplementedError() + + def unpack(self, source: BinaryIO): + """ + Unpack the constant from `source`, minus the tag. + """ + raise NotImplementedError() + + +class Number(Constant): + """ + The base class for all numeric constant types. + """ + + __slots__ = ("value",) + + def __init__(self, *, pool=None, index=None, value=0): + super().__init__(pool=pool, index=index) + self.value = value + + def __repr__(self): + return f"{self.__class__.__name__}(index={self.index}, value={self.value!r})" + + def __eq__(self, other): + if isinstance(other, Number): + return other.value == self.value + return other == self.value + + def pack(self): + raise NotImplementedError() + + def unpack(self, source: BinaryIO): + raise NotImplementedError() + + +class UTF8(Constant): + __slots__ = ("value",) + TAG = 1 + + def __init__(self, value=None, *, pool=None, index=None): + super().__init__(pool=pool, index=index) + self.value = value + + def pack(self): + encoded_value = encode_modified_utf8(self.value) + return pack(">H", len(encoded_value)) + encoded_value + + def unpack(self, source: BinaryIO): + self.value = decode_modified_utf8(source.read(unpack(">H", source.read(2))[0])) + + def __repr__(self): + return f")" + + def __eq__(self, other): + if isinstance(other, UTF8): + return other.value == self.value + return other == self.value + + +class Integer(Number): + TAG = 3 + + def pack(self): + return pack(">i", self.value) + + def unpack(self, source: BinaryIO): + self.value = unpack(">i", source.read(4))[0] + + +class Float(Number): + TAG = 4 + + def pack(self): + return pack(">f", self.value) + + def unpack(self, source: BinaryIO): + self.value = unpack(">f", source.read(4))[0] + + +class Long(Number): + TAG = 5 + + def pack(self): + return pack(">q", self.value) + + def unpack(self, source: BinaryIO): + self.value = unpack(">q", source.read(8))[0] + + +class Double(Number): + TAG = 6 + + def pack(self): + return pack(">d", self.value) + + def unpack(self, source: BinaryIO): + self.value = unpack(">d", source.read(8))[0] + + +class ConstantClass(Constant): + __slots__ = ("name_index",) + TAG = 7 + + def __init__(self, *, pool=None, index=None, name=None): + super().__init__(pool=pool, index=index) + self.name_index = 0 + if name is not None: + self.name = name + + @property + def name(self): + return self.pool[self.name_index] + + @name.setter + def name(self, value: Union[str, UTF8]): + if isinstance(value, UTF8): + self.name_index = self.pool.add(value).index + else: + self.name_index = UTF8(pool=self.pool, value=value).index + + def pack(self): + return pack(">H", self.name_index) + + def unpack(self, source: BinaryIO): + self.name_index = unpack(">H", source.read(2))[0] + + def __repr__(self): + return f"" + + +class String(Constant): + __slots__ = ("string_index",) + TAG = 8 + + def __init__(self, string=None, *, pool=None, index=None): + super().__init__(pool=pool, index=index) + self.string_index = None + if string is not None: + self.string = string + + @property + def string(self): + return self.pool[self.string_index] + + @string.setter + def string(self, value): + if isinstance(value, UTF8): + self.string_index = self.pool.add(value).index + else: + self.string_index = UTF8(value, pool=self.pool).index + + def pack(self): + return pack(">H", self.string_index) + + def unpack(self, source: BinaryIO): + self.string_index = unpack(">H", source.read(2))[0] + + def __repr__(self): + return f"" + + +class Reference(Constant): + __slots__ = ("class_index", "name_and_type_index") + TAG = None + + def __init__(self, *, pool=None, index=None): + super().__init__(pool=pool, index=index) + self.class_index = 0 + self.name_and_type_index = 0 + + @property + def class_(self): + return self.pool[self.class_index] + + @property + def name_and_type(self): + return self.pool[self.name_and_type_index] + + def pack(self): + return pack(">HH", self.class_index, self.name_and_type_index) + + def unpack(self, source: BinaryIO): + self.class_index, self.name_and_type_index = unpack(">HH", source.read(4)) + + def __repr__(self): + return ( + f"<{self.__class__.__name__}(" + f"index={self.index}," + f"class_={self.class_!r}," + f"name_and_type={self.name_and_type!r})>" + ) + + +class FieldReference(Reference): + TAG = 9 + + +class MethodReference(Reference): + TAG = 10 + + +class InterfaceMethodRef(Reference): + TAG = 11 + + +class NameAndType(Constant): + __slots__ = ("name_index", "descriptor_index") + TAG = 12 + + def __init__(self, *, pool=None, index=None): + super().__init__(pool=pool, index=index) + self.name_index = 0 + self.descriptor_index = 0 + + @property + def name(self): + return self.pool[self.name_index] + + @property + def descriptor(self): + return self.pool[self.descriptor_index] + + def pack(self): + return pack(">HH", self.name_index, self.descriptor_index) + + def unpack(self, source: BinaryIO): + self.name_index, self.descriptor_index = unpack(">HH", source.read(4)) + + def __repr__(self): + return ( + f"" + ) + + +class MethodHandle(Constant): + __slots__ = ("reference_kind", "reference_index") + TAG = 15 + + def __init__(self, *, pool=None, index=None): + super().__init__(pool=pool, index=index) + self.reference_kind = None + self.reference_index = 0 + + @property + def reference(self): + return self.pool.get(self.reference_index) + + def pack(self): + return pack(">BH", self.reference_kind, self.reference_index) + + def unpack(self, source: BinaryIO): + self.reference_kind, self.reference_index = unpack(">BH", source.read(3)) + + def __repr__(self): + return f"" + + +class MethodType(Constant): + __slots__ = ("descriptor_index",) + TAG = 16 + + def __init__(self, *, pool=None, index=None): + super().__init__(pool=pool, index=index) + self.descriptor_index = 0 + + @property + def descriptor(self): + return self.pool.get(self.descriptor_index) + + def pack(self): + return pack(">H", self.descriptor_index) + + def unpack(self, source: BinaryIO): + self.descriptor_index = unpack(">H", source.read(2))[0] + + def __repr__(self): + return f"" + + +class Dynamic(Constant): + __slots__ = ("bootstrap_method_attr_index", "name_and_type_index") + TAG = 17 + + def __init__(self, *, pool=None, index=None): + super().__init__(pool=pool, index=index) + self.bootstrap_method_attr_index = 0 + self.name_and_type_index = 0 + + @property + def method_attr_index(self): + return self.bootstrap_method_attr_index + + @property + def name_and_type(self): + return self.pool[self.name_and_type_index] + + def pack(self): + return pack(">HH", self.bootstrap_method_attr_index, self.name_and_type_index) + + def unpack(self, source: BinaryIO): + self.bootstrap_method_attr_index, self.name_and_type_index = unpack( + ">HH", source.read(4) + ) + + def __repr__(self): + return ( + f"" + ) + + +class InvokeDynamic(Dynamic): + __slots__ = ("bootstrap_method_attr_index", "name_and_type_index") + TAG = 18 + + def __repr__(self): + return ( + f"" + ) + + +class Module(ConstantClass): + __slots__ = ("name_index",) + TAG = 19 + + def __repr__(self): + return f"" + + +class PackageInfo(ConstantClass): + __slots__ = ("name_index",) + TAG = 20 + + def __repr__(self): + return f"" + + +CONSTANTS = { + 1: UTF8, + 3: Integer, + 4: Float, + 5: Long, + 6: Double, + 7: ConstantClass, + 8: String, + 9: FieldReference, + 10: MethodReference, + 11: InterfaceMethodRef, + 12: NameAndType, + 15: MethodHandle, + 16: MethodType, + 17: Dynamic, + 18: InvokeDynamic, + 19: Module, + 20: PackageInfo, +} + + +# The size (in bytes) of each type of Constant in the pool, except the UTF8 +# type which must be handled dynamically. +SIZE = (None, None, None, 4, 4, 8, 8, 2, 2, 4, 4, 4, 4, None, None, 3, 2, None, 4) + + +class ConstantPool(object): + """ + This class can be used to read, modify, and write the JVM ClassFile + constant pool with a high-level interface. + """ + + def __init__(self, *, source: BinaryIO = None): + # We use a dict as our basic pool container because the pool can be + # built out-of-order. For example when loading a Jasmin file, it's + # possible to explicitly set the position of constants in the pool, + # even if earlier elements don't exist yet. In general pool operations + # are not thread-safe. + + #: The internal constant pool. It's not recommended using this + #: directly. + self.pool: Dict[int, Any] = {} + #: A list of free indexes in the constant pool where gaps occur. + self.sparse_map: Deque[int] = deque() + + if source is not None: + self.unpack(source) + + def clear(self): + """Completely erase the ConstantPool. + + Any existing Constants will be disassociated from this pool, and may + still be referenced from elsewhere. + """ + # Disassociate any existing Constants in the pool. + for constant in self.pool.values(): + if constant is not None: + constant.pool = None + + self.pool.clear() + self.sparse_map.clear() + + def unpack(self, source: BinaryIO): + """Unpack a constant pool from a ClassFile.""" + read = source.read + constant_pool_count = unpack(">H", read(2))[0] + + index_iter = iter(range(1, constant_pool_count)) + for index in index_iter: + tag = ord(read(1)) + c = CONSTANTS[tag]() + c.unpack(source) + c.index = index + c.pool = self + self.pool[index] = c + if tag == 5 or tag == 6: + self.pool[index + 1] = None + next(index_iter) + + def pack(self, out: BinaryIO): + """Write the ConstantPool to the file-like object `out`.""" + write = out.write + write(pack(">H", len(self) + 1)) + + for index, constant in sorted(self.pool.items()): + # Skip over double-width padding (Doubles & Longs) + if constant is None: + continue + write(constant.TAG.to_bytes(1, byteorder="big")) + write(constant.pack()) + + def update_trackers(self): + """Update the internal tracking lists and counters to update free + indexes. + + .. note:: + + This is a fairly expensive operation, you should avoid calling it + except when necessary. If you are not updating the pool manually + you should never need to use this method. + """ + if not self.pool: + self.sparse_map = deque() + return + + self.sparse_map = deque( + _missing_elements(sorted(self.pool.keys()), 0, len(self.pool) - 1) + ) + + @property + def highest_unused_index(self) -> int: + return max(self.pool.keys(), default=0) + 1 + + def add(self, constant, index: int = None) -> int: + """Add a new entry to the constant pool. + + If no index is provided, this method will first attempt to fill in any + gaps in the constant pool. If no room is found, it'll instead append to + the end of the pool. + + :param constant: The constant to be added to the pool. + :param index: Optionally, an explicit index to use for the + new constant. [default: None] + :returns: The index used for the constant. + """ + if index is None and self.sparse_map: + # No explicit spot in the pool was requested, so lets try to find + # room for it. + desired_index = self.sparse_map.popleft() + if constant.TAG in (5, 6): + # ... however, we want to add a LONG or DOUBLE, so we + # really need two adjacent slots. + if desired_index + 1 not in self.pool: + index = desired_index + # Make sure it's removed from the sparse map if the + # neighbouring index happened to also be free. + try: + self.sparse_map.remove(desired_index + 1) + except ValueError: + pass + else: + # We can't use this slot, add it back to the sparse map, + # so it can be reused. + self.sparse_map.appendleft(desired_index) + else: + # Single-slot constant, we can put it anywhere. + index = desired_index + + # Still no usable index, append it to the pool instead. + if index is None: + index = self.highest_unused_index + + self.pool[index] = constant + constant.index = index + constant.pool = self + if constant.TAG in (5, 6): + self.pool[index + 1] = None + + return constant + + def remove(self, index: int): + """Remove the constant at `index` from the pool. + + ..note:: + + If the constant being removed at `index` is a LONG or DOUBLE, the + adjacent padding constant will also be removed. + + :param index: Index in the pool to be removed. + """ + const = self.pool.pop(index) + if const.TAG in (5, 6): + # If this was a double-width LONG or DOUBLE cleanup the adjacent + # padding. + del self.pool[index + 1] + self.update_trackers() + + def __iter__(self): + yield from ((k, v) for k, v in sorted(self.pool.items()) if v is not None) + + def __getitem__(self, index): + return self.pool[index] + + def __len__(self): + return sum(1 for v in self.pool.values() if v is not None) + + def find(self, type_=None, f=None): + """ + Iterates over the pool, yielding each matching ``Constant``. Calling + without any arguments is equivalent to iterating over the pool. + + :param type_: Any subclass of :class:`Constant` or ``None``. + :param f: Any callable which takes one argument (the constant). + """ + for index, constant in self: + if type_ is not None and not isinstance(constant, type_): + continue + + if f is not None and not f(constant): + continue + + yield constant + + def find_one(self, *args, **kwargs): + """ + Same as ``find()`` but returns only the first result, or `None` if + nothing was found. + """ + try: + return next(self.find(*args, **kwargs)) + except StopIteration: + return None diff --git a/lawu/context.py b/lawu/context.py new file mode 100644 index 0000000..5d17c4c --- /dev/null +++ b/lawu/context.py @@ -0,0 +1,18 @@ +from contextvars import ContextVar + +_class_context: ContextVar = ContextVar("class_context") + + +def class_context(): + class_stack = _class_context.get(None) + if class_stack is None: + lst = [] + _class_context.set(lst) + return lst + return class_stack + + +def current_class_context(): + class_stack = class_context() + if class_stack: + return class_stack.pop() diff --git a/jawa/fields.py b/lawu/fields.py similarity index 75% rename from jawa/fields.py rename to lawu/fields.py index d9d1289..d853eb9 100644 --- a/jawa/fields.py +++ b/lawu/fields.py @@ -1,28 +1,29 @@ -from typing import IO, Callable, Iterator, Optional +from typing import BinaryIO, Callable, Iterator, Optional from struct import unpack, pack from itertools import repeat +from enum import IntFlag -from jawa.util.flags import Flags -from jawa.attribute import AttributeTable -from jawa.constants import Constant, UTF8 -from jawa.attributes.constant_value import ConstantValueAttribute -from jawa.util.descriptor import field_descriptor +from lawu.attribute import AttributeTable +from lawu.constants import Constant, UTF8 +from lawu.attributes.constant_value import ConstantValueAttribute +from lawu.util.descriptor import field_descriptor class Field(object): + class AccessFlags(IntFlag): + PUBLIC = 0x0001 + PRIVATE = 0x0002 + PROTECTED = 0x0004 + STATIC = 0x0008 + FINAL = 0x0010 + VOLATILE = 0x0040 + TRANSIENT = 0x0080 + SYNTHETIC = 0x1000 + ENUM = 0x4000 + def __init__(self, cf): self._cf = cf - self.access_flags = Flags('>H', { - 'acc_public': 0x0001, - 'acc_private': 0x0002, - 'acc_protected': 0x0004, - 'acc_static': 0x0008, - 'acc_final': 0x0010, - 'acc_volatile': 0x0040, - 'acc_transient': 0x0080, - 'acc_synthetic': 0x1000, - 'acc_enum': 0x4000 - }) + self.access_flags = Field.AccessFlags(0) self._name_index = 0 self._descriptor_index = 0 self.attributes = AttributeTable(cf) @@ -37,7 +38,7 @@ def descriptor(self) -> UTF8: @property def type(self): """ - A :class:`~jawa.util.descriptor.JVMType` representing the field's + A :class:`~lawu.util.descriptor.JVMType` representing the field's type. """ return field_descriptor(self.descriptor.value) @@ -54,9 +55,9 @@ def value(self) -> ConstantValueAttribute: """ A shortcut for the field's ConstantValue attribute, should one exist. """ - return self.attributes.find_one(name='ConstantValue') + return self.attributes.find_one(name="ConstantValue") - def unpack(self, source: IO): + def unpack(self, source: BinaryIO): """ Read the Field from the file-like object `fio`. @@ -67,11 +68,11 @@ def unpack(self, source: IO): :param source: Any file-like object providing `read()` """ - self.access_flags.unpack(source.read(2)) - self._name_index, self._descriptor_index = unpack('>HH', source.read(4)) + self.access_flags = Field.AccessFlags(unpack(">H", source.read(2))[0]) + self._name_index, self._descriptor_index = unpack(">HH", source.read(4)) self.attributes.unpack(source) - def pack(self, out: IO): + def pack(self, out: BinaryIO): """ Write the Field to the file-like object `out`. @@ -82,8 +83,8 @@ def pack(self, out: IO): :param out: Any file-like object providing `write()` """ - out.write(self.access_flags.pack()) - out.write(pack('>HH', self._name_index, self._descriptor_index)) + out.write(pack(">H", int(self.access_flags))) + out.write(pack(">HH", self._name_index, self._descriptor_index)) self.attributes.pack(out) @@ -107,18 +108,18 @@ def remove(self, field: Field): """ self._table = [fld for fld in self._table if fld is not field] - def create(self, name: str, descriptor: str, value: Constant=None) -> Field: + def create(self, name: str, descriptor: str, value: Constant = None) -> Field: """ Creates a new field from `name` and `descriptor`. For example:: - >>> from jawa.cf import ClassFile - >>> cf = ClassFile.create('BeerCounter') + >>> from lawu.cf import ClassFile + >>> cf = ClassFile(this='BeerCounter') >>> field = cf.fields.create('BeerCount', 'I') To automatically create a static field, pass a value:: - >>> from jawa.cf import ClassFile - >>> cf = ClassFile.create('BeerCounter') + >>> from lawu.cf import ClassFile + >>> cf = ClassFile(this='BeerCounter') >>> field = cf.fields.create( ... 'MaxBeer', ... 'I', @@ -134,11 +135,11 @@ def create(self, name: str, descriptor: str, value: Constant=None) -> Field: descriptor = self._cf.constants.create_utf8(descriptor) field._name_index = name.index field._descriptor_index = descriptor.index - field.access_flags.acc_public = True + field.access_flags.PUBLIC = True if value is not None: field.attributes.create(ConstantValueAttribute, value) - field.access_flags.acc_static = True + field.access_flags.STATIC = True self.append(field) return field @@ -147,7 +148,7 @@ def __iter__(self): for field in self._table: yield field - def unpack(self, source: IO): + def unpack(self, source: BinaryIO): """ Read the FieldTable from the file-like object `source`. @@ -158,13 +159,13 @@ def unpack(self, source: IO): :param source: Any file-like object providing `read()` """ - field_count = unpack('>H', source.read(2))[0] + field_count = unpack(">H", source.read(2))[0] for _ in repeat(None, field_count): field = Field(self._cf) field.unpack(source) self.append(field) - def pack(self, out: IO): + def pack(self, out: BinaryIO): """ Write the FieldTable to the file-like object `out`. @@ -175,15 +176,20 @@ def pack(self, out: IO): :param out: Any file-like object providing `write()` """ - out.write(pack('>H', len(self))) + out.write(pack(">H", len(self))) for field in self._table: field.pack(out) def __len__(self): return len(self._table) - def find(self, *, name: str=None, type_: str=None, - f: Callable=None) -> Iterator[Field]: + def find( + self, + *, + name: Optional[str] = None, + type_: Optional[str] = None, + f: Optional[Callable] = None, + ) -> Iterator[Field]: """ Iterates over the fields table, yielding each matching method. Calling without any arguments is equivalent to iterating over the table. diff --git a/jawa/methods.py b/lawu/methods.py similarity index 70% rename from jawa/methods.py rename to lawu/methods.py index d53d2db..d7ccc11 100644 --- a/jawa/methods.py +++ b/lawu/methods.py @@ -1,31 +1,32 @@ -from typing import Optional, Callable, Iterator, IO, List +from typing import Optional, Callable, Iterator, BinaryIO, List from struct import unpack, pack from itertools import repeat +from enum import IntFlag -from jawa.constants import UTF8 -from jawa.util.flags import Flags -from jawa.util.descriptor import method_descriptor, JVMType -from jawa.attribute import AttributeTable -from jawa.attributes.code import CodeAttribute +from lawu.constants import UTF8 +from lawu.util.descriptor import method_descriptor, JVMType +from lawu.attribute import AttributeTable +from lawu.attributes.code import CodeAttribute class Method(object): + class AccessFlags(IntFlag): + PUBLIC = 0x0001 + PRIVATE = 0x0002 + PROTECTED = 0x0004 + STATIC = 0x0008 + FINAL = 0x0010 + SYNCHRONIZED = 0x0020 + BRIDGE = 0x0040 + VARARGS = 0x0080 + NATIVE = 0x0100 + ABSTRACT = 0x0400 + STRICT = 0x0800 + SYNTHETIC = 0x1000 + def __init__(self, cf): self._cf = cf - self.access_flags = Flags('>H', { - 'acc_public': 0x0001, - 'acc_private': 0x0002, - 'acc_protected': 0x0004, - 'acc_static': 0x0008, - 'acc_final': 0x0010, - 'acc_synchronized': 0x0020, - 'acc_bridge': 0x0040, - 'acc_varargs': 0x0080, - 'acc_native': 0x0100, - 'acc_abstract': 0x0400, - 'acc_strict': 0x0800, - 'acc_synthetic': 0x1000 - }) + self.access_flags = Method.AccessFlags(0) self._name_index = 0 self._descriptor_index = 0 self.attributes = AttributeTable(cf) @@ -47,7 +48,7 @@ def name(self) -> UTF8: @property def returns(self) -> JVMType: """ - A :class:`~jawa.util.descriptor.JVMType` representing the method's + A :class:`~lawu.util.descriptor.JVMType` representing the method's return type. """ return method_descriptor(self.descriptor.value).returns @@ -55,7 +56,7 @@ def returns(self) -> JVMType: @property def args(self) -> List[JVMType]: """ - A list of :class:`~jawa.util.descriptor.JVMType` representing the + A list of :class:`~lawu.util.descriptor.JVMType` representing the method's argument list. """ return method_descriptor(self.descriptor.value).args @@ -65,12 +66,12 @@ def code(self) -> CodeAttribute: """ A shortcut for :code:`method.attributes.find_one(name='Code')`. """ - return self.attributes.find_one(name='Code') + return self.attributes.find_one(name="Code") def __repr__(self): - return f'' + return f"" - def unpack(self, source: IO): + def unpack(self, source: BinaryIO): """ Read the Method from the file-like object `fio`. @@ -81,11 +82,11 @@ def unpack(self, source: IO): :param source: Any file-like object providing `read()` """ - self.access_flags.unpack(source.read(2)) - self._name_index, self._descriptor_index = unpack('>HH', source.read(4)) + self.access_flags = Method.AccessFlags(unpack(">H", source.read(2))[0]) + self._name_index, self._descriptor_index = unpack(">HH", source.read(4)) self.attributes.unpack(source) - def pack(self, out: IO): + def pack(self, out: BinaryIO): """ Write the Method to the file-like object `out`. @@ -96,12 +97,8 @@ def pack(self, out: IO): :param out: Any file-like object providing `write()` """ - out.write(self.access_flags.pack()) - out.write(pack( - '>HH', - self._name_index, - self._descriptor_index - )) + out.write(pack(">H", int(self.access_flags))) + out.write(pack(">HH", self._name_index, self._descriptor_index)) self.attributes.pack(out) @@ -125,18 +122,17 @@ def remove(self, method: Method): """ self._table = [fld for fld in self._table if fld is not method] - def create(self, name: str, descriptor: str, - code: CodeAttribute=None) -> Method: + def create(self, name: str, descriptor: str, code: CodeAttribute = None) -> Method: """ Creates a new method from `name` and `descriptor`. If `code` is not ``None``, add a `Code` attribute to this method. """ method = Method(self._cf) - name = self._cf.constants.create_utf8(name) - descriptor = self._cf.constants.create_utf8(descriptor) - method._name_index = name.index - method._descriptor_index = descriptor.index - method.access_flags.acc_public = True + with self._cf: + method._name_index = UTF8(value=name).index + method._descriptor_index = UTF8(value=descriptor).index + + method.access_flags.PUBLIC = True if code is not None: method.attributes.create(CodeAttribute) @@ -148,7 +144,7 @@ def __iter__(self): for method in self._table: yield method - def unpack(self, source: IO): + def unpack(self, source: BinaryIO): """ Read the MethodTable from the file-like object `source`. @@ -159,13 +155,13 @@ def unpack(self, source: IO): :param source: Any file-like object providing `read()` """ - method_count = unpack('>H', source.read(2))[0] + method_count = unpack(">H", source.read(2))[0] for _ in repeat(None, method_count): method = Method(self._cf) method.unpack(source) self.append(method) - def pack(self, out: IO): + def pack(self, out: BinaryIO): """ Write the MethodTable to the file-like object `out`. @@ -176,12 +172,18 @@ def pack(self, out: IO): :param out: Any file-like object providing `write()` """ - out.write(pack('>H', len(self))) + out.write(pack(">H", len(self))) for method in self._table: method.pack(out) - def find(self, *, name: str=None, args: str=None, returns: str=None, - f: Callable=None) -> Iterator[Method]: + def find( + self, + *, + name: Optional[str] = None, + args: Optional[str] = None, + returns: Optional[str] = None, + f: Optional[Callable] = None, + ) -> Iterator[Method]: """ Iterates over the methods table, yielding each matching method. Calling without any arguments is equivalent to iterating over the table. For @@ -197,8 +199,8 @@ def find(self, *, name: str=None, args: str=None, returns: str=None, print method.name.value :param name: The name of the method(s) to find. - :param args: The arguments descriptor (ex: ``III``) - :param returns: The returns descriptor (Ex: ``V``) + :param args: The argument descriptor (ex: ``III``) + :param returns: The return descriptor (Ex: ``V``) :param f: Any callable which takes one argument (the method). """ for method in self._table: @@ -206,13 +208,13 @@ def find(self, *, name: str=None, args: str=None, returns: str=None, continue descriptor = method.descriptor.value - end_para = descriptor.find(')') + end_para = descriptor.find(")") m_args = descriptor[1:end_para] if args is not None and args != m_args: continue - m_returns = descriptor[end_para + 1:] + m_returns = descriptor[end_para + 1 :] if returns is not None and returns != m_returns: continue diff --git a/jawa/transforms.py b/lawu/transforms.py similarity index 72% rename from jawa/transforms.py rename to lawu/transforms.py index 8c39a74..f0c801f 100644 --- a/jawa/transforms.py +++ b/lawu/transforms.py @@ -1,9 +1,10 @@ """ Transforms are simple Instruction modifiers that can be called on each -Instruction by the :func:`~jawa.attributes.code.CodeAttribute.disassemble` +Instruction by the :func:`~lawu.attributes.code.CodeAttribute.disassemble` function. """ -from jawa.util.bytecode import Instruction, Operand, OperandTypes, opcode_table + +from lawu.util.bytecode import Instruction, Operand, OperandTypes, opcode_table def expand_constants(ins: Instruction, *, cf) -> Instruction: @@ -34,18 +35,18 @@ def simple_swap(ins: Instruction) -> Instruction: :return: Potentially modified instruction. """ try: - rule = ins.details['transform']['simple_swap'] + rule = ins.details["transform"]["simple_swap"] except KeyError: return ins - replacement_ins = opcode_table[rule['op']] + replacement_ins = opcode_table[rule["op"]] return Instruction( - replacement_ins['mnemonic'], - replacement_ins['op'], - [Operand( - replacement_ins['operands'][i][1], - r - ) for i, r in enumerate(rule['operands'])], - ins.pos + replacement_ins["mnemonic"], + replacement_ins["op"], + [ + Operand(replacement_ins["operands"][i][1], r) + for i, r in enumerate(rule["operands"]) + ], + ins.pos, ) diff --git a/jawa/util/__init__.py b/lawu/util/__init__.py similarity index 88% rename from jawa/util/__init__.py rename to lawu/util/__init__.py index 109e049..aee5c8e 100644 --- a/jawa/util/__init__.py +++ b/lawu/util/__init__.py @@ -1,4 +1,4 @@ """ -Standalone utility modules that don't necessarily require the rest of Jawa to +Standalone utility modules that don't necessarily require the rest of Lawu to function. Generally they can be copy-pasted into other projects for reuse. -""" \ No newline at end of file +""" diff --git a/jawa/util/bytecode.json b/lawu/util/bytecode.json similarity index 100% rename from jawa/util/bytecode.json rename to lawu/util/bytecode.json diff --git a/jawa/util/bytecode.py b/lawu/util/bytecode.py similarity index 73% rename from jawa/util/bytecode.py rename to lawu/util/bytecode.py index f7989be..70dd7ee 100644 --- a/jawa/util/bytecode.py +++ b/lawu/util/bytecode.py @@ -1,6 +1,7 @@ """ Utilities for reading & writing JVM method bytecode. """ + import json import enum import pkgutil @@ -8,13 +9,8 @@ from itertools import repeat from collections import namedtuple -Operand = namedtuple('Operand', ['op_type', 'value']) -_Instruction = namedtuple('Instruction', [ - 'mnemonic', - 'opcode', - 'operands', - 'pos' -]) +Operand = namedtuple("Operand", ["op_type", "value"]) +_Instruction = namedtuple("Instruction", ["mnemonic", "opcode", "operands", "pos"]) class Instruction(_Instruction): @@ -22,6 +18,7 @@ class Instruction(_Instruction): Represents a single JVM instruction, consisting of an opcode and its potential operands. """ + __slots__ = () def size_on_disk(self, start_pos=0): @@ -32,7 +29,7 @@ def size_on_disk(self, start_pos=0): """ # All instructions are at least 1 byte (the opcode itself) size = 1 - fmts = opcode_table[self.opcode]['operands'] + fmts = opcode_table[self.opcode]["operands"] if self.wide: size += 2 @@ -63,7 +60,7 @@ def wide(self): ``True`` if this instruction needs to be prefixed by the WIDE opcode. """ - if not opcode_table[self.opcode].get('can_be_wide'): + if not opcode_table[self.opcode].get("can_be_wide"): return False if self.operands[0].value >= 255: @@ -89,12 +86,7 @@ def details(self): def create(cls, mnemonic_or_op, operands=None): op = opcode_table[mnemonic_or_op] - return cls( - op['mnemonic'], - op['op'], - operands or [], - 0 - ) + return cls(op["mnemonic"], op["op"], operands or [], 0) def __eq__(self, other): return other == self.mnemonic or super().__eq__(other) @@ -105,6 +97,7 @@ class OperandTypes(enum.IntEnum): Constants used to determine the "type" of operand on an opcode, such as a BRANCH [offset] or a LITERAL [value]. """ + LITERAL = 10 LOCAL_INDEX = 20 CONSTANT_INDEX = 30 @@ -113,11 +106,11 @@ class OperandTypes(enum.IntEnum): class OperandFmts(enum.Enum): - UBYTE = Struct('>B') - BYTE = Struct('>b') - USHORT = Struct('>H') - SHORT = Struct('>h') - INTEGER = Struct('>i') + UBYTE = Struct(">B") + BYTE = Struct(">b") + USHORT = Struct(">H") + SHORT = Struct(">h") + INTEGER = Struct(">i") def write_instruction(fout, start_pos, ins): @@ -129,24 +122,24 @@ def write_instruction(fout, start_pos, ins): :param ins: The `Instruction` to write. """ opcode, operands = ins.opcode, ins.operands - fmt_operands = opcode_table[opcode]['operands'] + fmt_operands = opcode_table[opcode]["operands"] if ins.wide: # The "WIDE" prefix - fout.write(pack('>B', 0xC4)) + fout.write(pack(">B", 0xC4)) # The real opcode. - fout.write(pack('>B', opcode)) - fout.write(pack('>H', operands[0].value)) + fout.write(pack(">B", opcode)) + fout.write(pack(">H", operands[0].value)) if opcode == 0x84: - fout.write(pack('>h', operands[1].value)) + fout.write(pack(">h", operands[1].value)) elif fmt_operands: # A normal simple opcode with simple operands. - fout.write(pack('>B', opcode)) + fout.write(pack(">B", opcode)) for i, (fmt, _) in enumerate(fmt_operands): fout.write(fmt.value.pack(operands[i].value)) elif opcode == 0xAB: # Special case for lookupswitch. - fout.write(pack('>B', opcode)) + fout.write(pack(">B", opcode)) # assemble([ # ('lookupswitch', { # 2: -3, @@ -155,27 +148,29 @@ def write_instruction(fout, start_pos, ins): # ]) padding = 4 - (start_pos + 1) % 4 padding = padding if padding != 4 else 0 - fout.write(pack(f'{padding}x')) - fout.write(pack('>ii', operands[1].value, len(operands[0]))) + fout.write(pack(f"{padding}x")) + fout.write(pack(">ii", operands[1].value, len(operands[0]))) for key in sorted(operands[0].keys()): - fout.write(pack('>ii', key, operands[0][key])) + fout.write(pack(">ii", key, operands[0][key])) elif opcode == 0xAA: # Special case for table switch. - fout.write(pack('>B', opcode)) + fout.write(pack(">B", opcode)) padding = 4 - (start_pos + 1) % 4 padding = padding if padding != 4 else 0 - fout.write(pack(f'{padding}x')) - fout.write(pack( - f'>iii{len(operands) - 3}i', - # Default branch offset - operands[0].value, - operands[1].value, - operands[2].value, - *(o.value for o in operands[3:]) - )) + fout.write(pack(f"{padding}x")) + fout.write( + pack( + f">iii{len(operands) - 3}i", + # Default branch offset + operands[0].value, + operands[1].value, + operands[2].value, + *(o.value for o in operands[3:]), + ) + ) else: # opcode with no operands. - fout.write(pack('>B', opcode)) + fout.write(pack(">B", opcode)) def read_instruction(fio, start_pos): @@ -194,18 +189,15 @@ def read_instruction(fio, start_pos): op = ord(op) ins = opcode_table[op] - operands = ins['operands'] - name = ins['mnemonic'] + operands = ins["operands"] + name = ins["mnemonic"] final_operands = [] # Most opcodes have simple operands. if operands: for fmt, type_ in operands: final_operands.append( - Operand( - type_, - fmt.value.unpack(fio.read(fmt.value.size))[0] - ) + Operand(type_, fmt.value.unpack(fio.read(fmt.value.size))[0]) ) # Special case for lookupswitch. elif op == 0xAB: @@ -215,11 +207,11 @@ def read_instruction(fio, start_pos): fio.read(padding) # Default branch address and branch count. - default, npairs = unpack('>ii', fio.read(8)) + default, npairs = unpack(">ii", fio.read(8)) pairs = {} for _ in repeat(None, npairs): - match, offset = unpack('>ii', fio.read(8)) + match, offset = unpack(">ii", fio.read(8)) pairs[match] = offset final_operands.append(pairs) @@ -231,30 +223,28 @@ def read_instruction(fio, start_pos): padding = padding if padding != 4 else 0 fio.read(padding) - default, low, high = unpack('>iii', fio.read(12)) + default, low, high = unpack(">iii", fio.read(12)) final_operands.append(Operand(OperandTypes.BRANCH, default)) final_operands.append(Operand(OperandTypes.LITERAL, low)) final_operands.append(Operand(OperandTypes.LITERAL, high)) for _ in repeat(None, high - low + 1): - offset = unpack('>i', fio.read(4))[0] + offset = unpack(">i", fio.read(4))[0] final_operands.append(Operand(OperandTypes.BRANCH, offset)) # Special case for the wide prefix elif op == 0xC4: - real_op = unpack('>B', fio.read(1))[0] + real_op = unpack(">B", fio.read(1))[0] ins = opcode_table[real_op] - name = ins['mnemonic'] + name = ins["mnemonic"] - final_operands.append(Operand( - OperandTypes.LOCAL_INDEX, - unpack('>H', fio.read(2))[0] - )) + final_operands.append( + Operand(OperandTypes.LOCAL_INDEX, unpack(">H", fio.read(2))[0]) + ) # Further special case for iinc. if real_op == 0x84: - final_operands.append(Operand( - OperandTypes.LITERAL, - unpack('>H', fio.read(2))[0] - )) + final_operands.append( + Operand(OperandTypes.LITERAL, unpack(">H", fio.read(2))[0]) + ) return Instruction(name, op, final_operands, start_pos) @@ -268,11 +258,11 @@ def load_bytecode_definitions(*, path=None) -> dict: bytecode definitions. """ if path is not None: - with open(path, 'rb') as file_in: + with open(path, "rb") as file_in: j = json.load(file_in) else: try: - j = json.loads(pkgutil.get_data('jawa.util', 'bytecode.json')) + j = json.loads(pkgutil.get_data("lawu.util", "bytecode.json")) except json.JSONDecodeError: # Unfortunately our best way to handle missing/malformed/empty # bytecode.json files since it may not actually be backed by a @@ -282,15 +272,14 @@ def load_bytecode_definitions(*, path=None) -> dict: for definition in j.values(): # If the entry has any operands take the text labels and convert # them into pre-cached struct objects and operand types. - operands = definition['operands'] + operands = definition["operands"] if operands: - definition['operands'] = [ - [getattr(OperandFmts, oo[0]), OperandTypes[oo[1]]] - for oo in operands + definition["operands"] = [ + [getattr(OperandFmts, oo[0]), OperandTypes[oo[1]]] for oo in operands ] # Return one dict that contains both mnemonic keys and opcode keys. - return {**j, **{v['op']: v for v in j.values()}} + return {**j, **{v["op"]: v for v in j.values()}} opcode_table = load_bytecode_definitions() diff --git a/jawa/util/bytecode.yaml b/lawu/util/bytecode.yaml similarity index 100% rename from jawa/util/bytecode.yaml rename to lawu/util/bytecode.yaml diff --git a/jawa/util/descriptor.py b/lawu/util/descriptor.py similarity index 58% rename from jawa/util/descriptor.py rename to lawu/util/descriptor.py index 0db372a..258b5af 100644 --- a/jawa/util/descriptor.py +++ b/lawu/util/descriptor.py @@ -1,22 +1,16 @@ """ Methods for parsing standard JVM type descriptors for fields and methods. """ + from collections import namedtuple -JVMType = namedtuple('JVMType', [ - 'base_type', - 'dimensions', - 'name' -]) +JVMType = namedtuple("JVMType", ["base_type", "dimensions", "name"]) -MethodDescriptor = namedtuple('MethodDescriptor', [ - 'returns', - 'args', - 'returns_descriptor', - 'args_descriptor', - 'descriptor' -]) +MethodDescriptor = namedtuple( + "MethodDescriptor", + ["returns", "args", "returns_descriptor", "args_descriptor", "descriptor"], +) def method_descriptor(descriptor: str) -> MethodDescriptor: @@ -24,16 +18,12 @@ def method_descriptor(descriptor: str) -> MethodDescriptor: Parses a Method descriptor as described in section 4.3.3 of the JVM specification. """ - end_para = descriptor.find(')') - returns = descriptor[end_para + 1:] + end_para = descriptor.find(")") + returns = descriptor[end_para + 1 :] args = descriptor[1:end_para] return MethodDescriptor( - parse_descriptor(returns)[0], - parse_descriptor(args), - returns, - args, - descriptor + parse_descriptor(returns)[0], parse_descriptor(args), returns, args, descriptor ) @@ -48,24 +38,24 @@ def field_descriptor(descriptor: str) -> str: # JVM Descriptor "BaseType" characters to their # full simple type. _HUMAN_NAMES = { - 'L': 'reference', - 'B': 'byte', - 'C': 'char', - 'D': 'double', - 'F': 'float', - 'I': 'int', - 'J': 'long', - 'S': 'short', - 'Z': 'boolean', - 'V': 'void' + "L": "reference", + "B": "byte", + "C": "char", + "D": "double", + "F": "float", + "I": "int", + "J": "long", + "S": "short", + "Z": "boolean", + "V": "void", } def parse_descriptor(descriptor: str) -> list: """ Uses a tiny state machine to parse JVM descriptors. To get useful wrappers - around the results, use :py:func:`jawa.core.descriptor.method_descriptor` - or :py:func:`jawa.core.descriptor.field_descriptor`. + around the results, use :py:func:`lawu.core.descriptor.method_descriptor` + or :py:func:`lawu.core.descriptor.field_descriptor`. """ # States: # 10 == NORMAL, @@ -75,15 +65,15 @@ def parse_descriptor(descriptor: str) -> list: token = [] dimensions = 0 for char in descriptor: - if state == 10 and char == 'L': + if state == 10 and char == "L": state = 20 - elif state == 10 and char == '[': + elif state == 10 and char == "[": dimensions += 1 elif state == 10: tokens.append(JVMType(char, dimensions, _HUMAN_NAMES[char])) dimensions = 0 - elif state == 20 and char == ';': - tokens.append(JVMType('L', dimensions, ''.join(token))) + elif state == 20 and char == ";": + tokens.append(JVMType("L", dimensions, "".join(token))) dimensions = 0 state = 10 del token[:] diff --git a/jawa/util/shell.py b/lawu/util/shell.py similarity index 93% rename from jawa/util/shell.py rename to lawu/util/shell.py index 8737934..bfdac54 100644 --- a/jawa/util/shell.py +++ b/lawu/util/shell.py @@ -10,7 +10,7 @@ IPYTHON_SHELL_AVAILABLE = True -def start_shell(local_ns: Dict=None, banner: str=''): +def start_shell(local_ns: Dict = None, banner: str = ""): """Create and immediately drop into a Python shell. If IPython version 5 or greater is available it will be used instead diff --git a/lawu/util/stream.py b/lawu/util/stream.py new file mode 100644 index 0000000..d943090 --- /dev/null +++ b/lawu/util/stream.py @@ -0,0 +1,18 @@ +from io import BytesIO +from struct import unpack, calcsize + + +class BufferStreamReader(BytesIO): + """Stream-like reader over a buffer mimicking the JVM spec types.""" + + def u1(self): + return unpack(">B", self.read(1))[0] + + def u2(self): + return unpack(">H", self.read(2))[0] + + def u4(self): + return unpack(">I", self.read(4))[0] + + def unpack(self, fmt): + return unpack(fmt, self.read(calcsize(fmt))) diff --git a/jawa/util/verifier.py b/lawu/util/verifier.py similarity index 99% rename from jawa/util/verifier.py rename to lawu/util/verifier.py index 26f18ea..a743183 100644 --- a/jawa/util/verifier.py +++ b/lawu/util/verifier.py @@ -1,5 +1,3 @@ - - class VerificationTypes(object): ITEM_Top = 0 ITEM_Integer = 1 diff --git a/lawu_small.png b/lawu_small.png new file mode 100644 index 0000000..6004c66 Binary files /dev/null and b/lawu_small.png differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..52b136f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,58 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "lawu" +version = "4.0.0" +description = "Doing fun stuff with JVM ClassFiles." +readme = "README.md" +authors = [ + { name = "Tyler Kennedy", email = "tk@tkte.ch" } +] +license = { text = "MIT" } +requires-python = ">=3.11" +classifiers = [ + "Programming Language :: Python", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Topic :: Software Development :: Disassemblers", +] +keywords = ["java", "disassembly", "disassembler", "assembly"] +dependencies = [ + "mutf8", +] + +[project.urls] +Homepage = "https://tkte.ch/lawu/" + +[project.optional-dependencies] +dev = [ + "pytest", + "pytest-cov", + "sphinx", + "sphinxcontrib-googleanalytics", + "sphinx-click", + "furo", + "ghp-import", + "pyyaml", + "twine", + "wheel", +] +cli = [ + "click", + "rich", +] + +[project.scripts] +lawu = "lawu.cli:cli" + +[tool.pytest.ini_options] +addopts = "--cov=lawu --cov-report=term-missing" + +[tool.coverage.run] +omit = [ + "lawu/util/shell.py", +] \ No newline at end of file diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 224a779..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -description-file = README.md \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 3fd21a0..0000000 --- a/setup.py +++ /dev/null @@ -1,56 +0,0 @@ - -from setuptools import setup, find_packages - -setup( - name='jawa', - packages=find_packages(), - version='2.2.0', - python_requires='>=3.6', - description='Doing fun stuff with JVM ClassFiles.', - long_description=open('README.md', 'r').read(), - long_description_content_type='text/markdown', - author='Tyler Kennedy', - author_email='tk@tkte.ch', - url='http://github.com/TkTech/Jawa', - keywords=[ - 'java', - 'disassembly', - 'disassembler', - 'assembly' - ], - include_package_data=True, - classifiers=[ - 'Programming Language :: Python', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Disassemblers', - 'Topic :: Software Development :: Assemblers' - ], - install_requires=[ - 'click>=5.0' - ], - tests_require=[ - 'pytest>=2.10', - ], - extras_require={ - 'dev': [ - 'pytest', - 'sphinx', - 'sphinxcontrib-googleanalytics', - 'sphinx_rtd_theme', - 'sphinx-click', - 'ghp-import', - 'pyyaml', - 'ipython', - 'twine', - 'wheel', - 'bumpversion' - ] - }, - entry_points=''' - [console_scripts] - jawa=jawa.cli:cli - ''' -) diff --git a/tests/attributes/test_enclosing_method.py b/tests/attributes/test_enclosing_method.py index 4276412..9e8a57f 100644 --- a/tests/attributes/test_enclosing_method.py +++ b/tests/attributes/test_enclosing_method.py @@ -1,11 +1,11 @@ def test_enclosing_method_read(loader): - cf = loader['EnclosingMethod$1EnclosedClass'] - a = cf.attributes.find_one(name='EnclosingMethod') - assert cf.constants[a.method_index].name.value == 'main' - assert cf.constants[a.class_index].name.value == 'EnclosingMethod' + cf = loader["EnclosingMethod$1EnclosedClass"] + a = cf.attributes.find_one(name="EnclosingMethod") + assert cf.constants[a.method_index].name.value == "main" + assert cf.constants[a.class_index].name.value == "EnclosingMethod" def test_exceptions_write(loader): - cf = loader['EnclosingMethod$1EnclosedClass'] - a = cf.attributes.find_one(name='EnclosingMethod') - assert a.pack() == b'\x00\x0b\x00\x0c' + cf = loader["EnclosingMethod$1EnclosedClass"] + a = cf.attributes.find_one(name="EnclosingMethod") + assert a.pack() == b"\x00\x0b\x00\x0c" diff --git a/tests/attributes/test_exceptions_attribute.py b/tests/attributes/test_exceptions_attribute.py index 6b6d002..7d9efcb 100644 --- a/tests/attributes/test_exceptions_attribute.py +++ b/tests/attributes/test_exceptions_attribute.py @@ -1,27 +1,28 @@ +from lawu.constants import ConstantClass + + def test_exceptions_read(loader): - cf = loader['ExceptionsTest'] + cf = loader["ExceptionsTest"] - m = cf.methods.find_one(name='test') - a = m.attributes.find_one(name='Exceptions') + m = cf.methods.find_one(name="test") + a = m.attributes.find_one(name="Exceptions") assert len(a.exceptions) == 1 - assert cf.constants[a.exceptions[0]].name.value == \ - u'java/lang/IndexOutOfBoundsException' + assert ( + cf.constants[a.exceptions[0]].name.value + == "java/lang/IndexOutOfBoundsException" + ) def test_exceptions_write(loader): - cf = loader['ExceptionsTest'] + cf = loader["ExceptionsTest"] + with cf: + m = cf.methods.find_one(name="test") + a = m.attributes.find_one(name="Exceptions") - m = cf.methods.find_one(name='test') - a = m.attributes.find_one(name='Exceptions') + assert a.pack() == b"\x00\x01\x00\x0a" - assert a.pack() == b'\x00\x01\x00\x0A' - - a.exceptions.append( - cf.constants.create_class( - name=u'java/lang/TestException' - ).index - ) + a.exceptions.append(ConstantClass(name="java/lang/TestException").index) - assert a.pack() == b'\x00\x02\x00\x0A\x00\x12' + assert a.pack() == b"\x00\x02\x00\x0a\x00\x11" diff --git a/tests/attributes/test_general_attributes.py b/tests/attributes/test_general_attributes.py index 47fbc5c..6fc0c16 100644 --- a/tests/attributes/test_general_attributes.py +++ b/tests/attributes/test_general_attributes.py @@ -1,24 +1,21 @@ -from jawa.attribute import get_attribute_classes +from lawu.attribute import get_attribute_classes def test_mandatory_attributes(): - required_properties = ['ADDED_IN', 'MINIMUM_CLASS_VERSION'] + required_properties = ["ADDED_IN", "MINIMUM_CLASS_VERSION"] for name, class_ in get_attribute_classes().items(): for p in required_properties: assert hasattr(class_, p), ( - '{name} parser missing mandatory {p} property'.format( - name=name, - p=p - ) + "{name} parser missing mandatory {p} property".format(name=name, p=p) ) def test_attribute_naming(): for name, class_ in get_attribute_classes().items(): - if hasattr(class_, 'ATTRIBUTE_NAME'): + if hasattr(class_, "ATTRIBUTE_NAME"): continue - assert class_.__name__.endswith('Attribute'), ( - '{name} parser does not follow naming convention and does' - ' not explicitly set it.'.format(name=name) + assert class_.__name__.endswith("Attribute"), ( + "{name} parser does not follow naming convention and does" + " not explicitly set it.".format(name=name) ) diff --git a/tests/attributes/test_inner_classes.py b/tests/attributes/test_inner_classes.py index 4e4ec4e..60bb6d9 100644 --- a/tests/attributes/test_inner_classes.py +++ b/tests/attributes/test_inner_classes.py @@ -1,20 +1,20 @@ -from jawa.attributes.inner_classes import InnerClass +from lawu.attributes.inner_classes import InnerClass def test_inner_classes_read(loader): - cf = loader['InnerClasses'] - a = cf.attributes.find_one(name='InnerClasses') + cf = loader["InnerClasses"] + a = cf.attributes.find_one(name="InnerClasses") assert a.inner_classes == [ InnerClass( inner_class_info_index=4, outer_class_info_index=2, inner_name_index=5, - inner_class_access_flags=2 + inner_class_access_flags=2, ) ] def test_exceptions_write(loader): - cf = loader['InnerClasses'] - a = cf.attributes.find_one(name='InnerClasses') - assert a.pack() == b'\x00\x01\x00\x04\x00\x02\x00\x05\x00\x02' + cf = loader["InnerClasses"] + a = cf.attributes.find_one(name="InnerClasses") + assert a.pack() == b"\x00\x01\x00\x04\x00\x02\x00\x05\x00\x02" diff --git a/tests/attributes/test_line_number_attribute.py b/tests/attributes/test_line_number_attribute.py index 0513c61..67b9c84 100644 --- a/tests/attributes/test_line_number_attribute.py +++ b/tests/attributes/test_line_number_attribute.py @@ -1,7 +1,7 @@ def test_exceptions_read(loader): - cf = loader['HelloWorldDebug'] - m = cf.methods.find_one(name='main') - a = m.code.attributes.find_one(name='LineNumberTable') + cf = loader["HelloWorldDebug"] + m = cf.methods.find_one(name="main") + a = m.code.attributes.find_one(name="LineNumberTable") assert len(a.line_no) == 2 @@ -10,8 +10,8 @@ def test_exceptions_read(loader): def test_exceptions_write(loader): - cf = loader['HelloWorldDebug'] - m = cf.methods.find_one(name='main') - a = m.code.attributes.find_one(name='LineNumberTable') + cf = loader["HelloWorldDebug"] + m = cf.methods.find_one(name="main") + a = m.code.attributes.find_one(name="LineNumberTable") - assert a.pack() == b'\x00\x02\x00\x00\x00\x03\x00\x08\x00\x04' + assert a.pack() == b"\x00\x02\x00\x00\x00\x03\x00\x08\x00\x04" diff --git a/tests/attributes/test_sourcefile_attribute.py b/tests/attributes/test_sourcefile_attribute.py index 4c5f35e..5d3bc1d 100644 --- a/tests/attributes/test_sourcefile_attribute.py +++ b/tests/attributes/test_sourcefile_attribute.py @@ -1,26 +1,28 @@ from io import BytesIO -from jawa.cf import ClassFile -from jawa.attributes.source_file import SourceFileAttribute +from lawu.cf import ClassFile +from lawu.attributes.source_file import SourceFileAttribute +from lawu.constants import UTF8 def test_sourcefile_read(loader): """ Ensure we can read a SourceFileAttribute generated by javac. """ - cf = loader['HelloWorldDebug'] - source_file = cf.attributes.find_one(name='SourceFile') - assert(source_file.source_file.value == 'HelloWorldDebug.java') + cf = loader["HelloWorldDebug"] + source_file = cf.attributes.find_one(name="SourceFile") + assert source_file.source_file.value == "HelloWorldDebug.java" def test_sourcefile_write(): """ Ensure SourceFileAttribute can be written and read back. """ - cf_one = ClassFile.create(u'SourceFileTest') + cf_one = ClassFile(this="SourceFileTest") - sfa = cf_one.attributes.create(SourceFileAttribute) - sfa.source_file = cf_one.constants.create_utf8(u'SourceFileTest.java') + with cf_one: + sfa = cf_one.attributes.create(SourceFileAttribute) + sfa.source_file = UTF8(value="SourceFileTest.java") fout = BytesIO() cf_one.save(fout) @@ -28,5 +30,5 @@ def test_sourcefile_write(): fin = BytesIO(fout.getvalue()) cf_two = ClassFile(fin) - source_file = cf_two.attributes.find_one(name=u'SourceFile') - assert(source_file.source_file.value == u'SourceFileTest.java') + source_file = cf_two.attributes.find_one(name="SourceFile") + assert source_file.source_file.value == "SourceFileTest.java" diff --git a/tests/attributes/test_stack_map_attribute.py b/tests/attributes/test_stack_map_attribute.py index 069b9e6..f1814a5 100644 --- a/tests/attributes/test_stack_map_attribute.py +++ b/tests/attributes/test_stack_map_attribute.py @@ -1,24 +1,22 @@ -from jawa.util.verifier import VerificationTypes +from lawu.util.verifier import VerificationTypes def test_stack_map_table_read(loader): - cf = loader['ArrayTest'] - m = cf.methods.find_one(name='addOne') - a = m.code.attributes.find_one(name='StackMapTable') + cf = loader["ArrayTest"] + m = cf.methods.find_one(name="addOne") + a = m.code.attributes.find_one(name="StackMapTable") assert len(a.frames) == 2 assert a.frames[0].frame_type == 76 - assert a.frames[0].frame_stack == [ - (VerificationTypes.ITEM_Integer,) - ] + assert a.frames[0].frame_stack == [(VerificationTypes.ITEM_Integer,)] assert a.frames[1].frame_type == 255 assert a.frames[1].frame_stack == [ (VerificationTypes.ITEM_Integer,), - (VerificationTypes.ITEM_Integer,) + (VerificationTypes.ITEM_Integer,), ] assert a.frames[1].frame_locals == [ (VerificationTypes.ITEM_Object, 17), - (VerificationTypes.ITEM_Integer,) + (VerificationTypes.ITEM_Integer,), ] diff --git a/tests/conftest.py b/tests/conftest.py index bb965e8..7caee27 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,9 +2,9 @@ import pytest -from jawa.classloader import ClassLoader +from lawu.classloader import ClassLoader -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def loader() -> ClassLoader: - return ClassLoader(Path(__file__).parent / 'data', max_cache=-1) + return ClassLoader(Path(__file__).parent / "data", max_cache=-1) diff --git a/tests/test_bytecode.py b/tests/test_bytecode.py index f7fb224..c9829e8 100644 --- a/tests/test_bytecode.py +++ b/tests/test_bytecode.py @@ -1,42 +1,49 @@ -from jawa.util.bytecode import Instruction, Operand, OperandTypes +from lawu.util.bytecode import Instruction, Operand, OperandTypes GOOD_TABLE_SWITCH = [ - Instruction(mnemonic='iconst_1', opcode=4, operands=[], pos=0), - Instruction(mnemonic='tableswitch', opcode=170, operands=[ - # DEFAULT - Operand(OperandTypes.BRANCH, value=30), - # LOW - Operand(OperandTypes.LITERAL, value=1), - # HIGH - Operand(OperandTypes.LITERAL, value=3), - # TABLE - Operand(OperandTypes.BRANCH, value=27), - Operand(OperandTypes.BRANCH, value=28), - Operand(OperandTypes.BRANCH, value=29) - ], pos=1), - Instruction(mnemonic='return', opcode=177, operands=[], pos=28), - Instruction(mnemonic='return', opcode=177, operands=[], pos=29), - Instruction(mnemonic='return', opcode=177, operands=[], pos=30), - Instruction(mnemonic='return', opcode=177, operands=[], pos=31) + Instruction(mnemonic="iconst_1", opcode=4, operands=[], pos=0), + Instruction( + mnemonic="tableswitch", + opcode=170, + operands=[ + # DEFAULT + Operand(OperandTypes.BRANCH, value=30), + # LOW + Operand(OperandTypes.LITERAL, value=1), + # HIGH + Operand(OperandTypes.LITERAL, value=3), + # TABLE + Operand(OperandTypes.BRANCH, value=27), + Operand(OperandTypes.BRANCH, value=28), + Operand(OperandTypes.BRANCH, value=29), + ], + pos=1, + ), + Instruction(mnemonic="return", opcode=177, operands=[], pos=28), + Instruction(mnemonic="return", opcode=177, operands=[], pos=29), + Instruction(mnemonic="return", opcode=177, operands=[], pos=30), + Instruction(mnemonic="return", opcode=177, operands=[], pos=31), ] GOOD_LOOKUP_SWITCH = [ - Instruction(mnemonic='iconst_1', opcode=4, operands=[], pos=0), - Instruction(mnemonic='lookupswitch', opcode=171, operands=[ - {1: 27, 3: 28}, - Operand(op_type=OperandTypes.BRANCH, value=29) - ], pos=1), - Instruction(mnemonic='return', opcode=177, operands=[], pos=28), - Instruction(mnemonic='return', opcode=177, operands=[], pos=29), - Instruction(mnemonic='return', opcode=177, operands=[], pos=30) + Instruction(mnemonic="iconst_1", opcode=4, operands=[], pos=0), + Instruction( + mnemonic="lookupswitch", + opcode=171, + operands=[{1: 27, 3: 28}, Operand(op_type=OperandTypes.BRANCH, value=29)], + pos=1, + ), + Instruction(mnemonic="return", opcode=177, operands=[], pos=28), + Instruction(mnemonic="return", opcode=177, operands=[], pos=29), + Instruction(mnemonic="return", opcode=177, operands=[], pos=30), ] def test_table_switch(loader): # Ensure we can both read and write table switch opcodes. - cf = loader['TableSwitch'] - main = cf.methods.find_one(name='main') + cf = loader["TableSwitch"] + main = cf.methods.find_one(name="main") instructions = list(main.code.disassemble()) assert instructions == GOOD_TABLE_SWITCH @@ -49,8 +56,8 @@ def test_table_switch(loader): def test_lookup_switch(loader): # Ensure we can both read and write lookup switch opcodes. - cf = loader['LookupSwitch'] - main = cf.methods.find_one(name='main') + cf = loader["LookupSwitch"] + main = cf.methods.find_one(name="main") instructions = list(main.code.disassemble()) assert instructions == GOOD_LOOKUP_SWITCH @@ -62,7 +69,7 @@ def test_lookup_switch(loader): def test_compare(): - ins = Instruction.create('return') - assert ins == 'return' + ins = Instruction.create("return") + assert ins == "return" assert ins == ins - assert ins != 'not_return' + assert ins != "not_return" diff --git a/tests/test_classloader.py b/tests/test_classloader.py index 2799e4b..55929fc 100644 --- a/tests/test_classloader.py +++ b/tests/test_classloader.py @@ -3,101 +3,88 @@ import tempfile import zipfile -from jawa.cf import ClassFile -from jawa.classloader import ClassLoader -from jawa.transforms import simple_swap -from jawa.assemble import assemble +from lawu.cf import ClassFile +from lawu.classloader import ClassLoader +from lawu.transforms import simple_swap +from lawu.assemble import assemble def test_load_from_class(): """Ensure we can add ClassFile's directly to the ClassLoader.""" cl = ClassLoader() - cf = ClassFile.create('TestClass') + cf = ClassFile(this="TestClass") cl.update(cf) - assert cl.load('TestClass') is cf + assert cl.load("TestClass") is cf def test_default_bytecode_transforms(): cl = ClassLoader(bytecode_transforms=[simple_swap]) - cf = ClassFile.create('TestClass') + cf = ClassFile(this="TestClass") cl.update(cf) - test_method = cf.methods.create('test', '(V)V;', code=True) + test_method = cf.methods.create("test", "(V)V;", code=True) test_method.code.max_stack = 2 test_method.code.max_locals = 0 - test_method.code.assemble(assemble([ - ('iconst_0',), - ('pop',), - ('return',) - ])) + test_method.code.assemble(assemble([("iconst_0",), ("pop",), ("return",)])) # Load from the ClassLoader to bind to it. - cf = cl.load('TestClass') + cf = cl.load("TestClass") # Ensure the defaults apply. ins_iter = test_method.code.disassemble() ins = next(ins_iter) - assert ins.mnemonic == 'bipush' + assert ins.mnemonic == "bipush" assert len(ins.operands) == 1 assert ins.operands[0].value == 0 # Ensure we can override the default. ins_iter = test_method.code.disassemble(transforms=[]) ins = next(ins_iter) - assert ins.mnemonic == 'iconst_0' + assert ins.mnemonic == "iconst_0" assert len(ins.operands) == 0 def test_load_from_directory(): """Ensure we can load a ClassFile from a simple directory.""" - with tempfile.TemporaryDirectory() as dir: + with tempfile.TemporaryDirectory() as directory: shutil.copy( - os.path.join( - os.path.dirname(__file__), - 'data', - 'HelloWorld.class' - ), - dir + os.path.join(os.path.dirname(__file__), "data", "HelloWorld.class"), + directory, ) cl = ClassLoader() - cl.update(dir) + cl.update(directory) - assert isinstance(cl.load('HelloWorld'), cl.klass) + assert isinstance(cl.load("HelloWorld"), cl.klass) def test_load_from_zipfile(): """Ensure we can load a ClassFile from a ZipFile.""" - with tempfile.NamedTemporaryFile(suffix='.jar') as tmp: - with zipfile.ZipFile(tmp, 'w') as zf: + with tempfile.NamedTemporaryFile(suffix=".jar") as tmp: + with zipfile.ZipFile(tmp, "w") as zf: zf.write( - os.path.join( - os.path.dirname(__file__), - 'data', - 'HelloWorld.class' - ), - arcname='HelloWorld.class' - + os.path.join(os.path.dirname(__file__), "data", "HelloWorld.class"), + arcname="HelloWorld.class", ) cl = ClassLoader() cl.update(tmp.name) - assert isinstance(cl.load('HelloWorld'), cl.klass) + assert isinstance(cl.load("HelloWorld"), cl.klass) def test_contains(loader): - assert 'HelloWorld' in loader + assert "HelloWorld" in loader def test_dependencies(loader): - assert loader.dependencies('HelloWorld') == { - 'java/lang/Object', - 'java/io/PrintStream', - 'HelloWorld', - 'java/lang/System' + assert loader.dependencies("HelloWorld") == { + "java/lang/Object", + "java/io/PrintStream", + "HelloWorld", + "java/lang/System", } diff --git a/tests/test_expand_constants.py b/tests/test_expand_constants.py index 5d7c238..8e15499 100644 --- a/tests/test_expand_constants.py +++ b/tests/test_expand_constants.py @@ -1,9 +1,9 @@ -from jawa.constants import FieldReference -from jawa.transforms import expand_constants +from lawu.constants import FieldReference +from lawu.transforms import expand_constants def test_expand_constants(loader): - cf = loader['HelloWorld'] - main = cf.methods.find_one(name='main') + cf = loader["HelloWorld"] + main = cf.methods.find_one(name="main") ins = list(main.code.disassemble(transforms=[expand_constants])) assert isinstance(ins[0].operands[0], FieldReference) diff --git a/tests/test_hello_world.py b/tests/test_hello_world.py index d59d59d..13cbc4d 100644 --- a/tests/test_hello_world.py +++ b/tests/test_hello_world.py @@ -1,4 +1,4 @@ -from jawa.util.bytecode import Instruction, Operand +from lawu.util.bytecode import Instruction, Operand def test_hello_world(loader): @@ -16,14 +16,14 @@ class HelloWorld { } } """ - cf = loader['HelloWorld'] + cf = loader["HelloWorld"] assert len(cf.constants) == 21 assert len(cf.attributes) == 0 assert len(cf.fields) == 0 assert len(cf.methods) == 1 - main_method = cf.methods.find_one(name='main') + main_method = cf.methods.find_one(name="main") assert main_method is not None # 0x08 for ACC_STATIC, 0x01 for ACC_PUBLIC @@ -33,14 +33,20 @@ class HelloWorld { instruction_list = list(main_method.code.disassemble()) assert instruction_list == [ - Instruction(mnemonic='getstatic', opcode=178, operands=[ - Operand(op_type=30, value=13) - ], pos=0), - Instruction(mnemonic='ldc', opcode=18, operands=[ - Operand(op_type=30, value=15) - ], pos=3), - Instruction(mnemonic='invokevirtual', opcode=182, operands=[ - Operand(op_type=30, value=21) - ], pos=5), - Instruction(mnemonic='return', opcode=177, operands=[], pos=8) + Instruction( + mnemonic="getstatic", + opcode=178, + operands=[Operand(op_type=30, value=13)], + pos=0, + ), + Instruction( + mnemonic="ldc", opcode=18, operands=[Operand(op_type=30, value=15)], pos=3 + ), + Instruction( + mnemonic="invokevirtual", + opcode=182, + operands=[Operand(op_type=30, value=21)], + pos=5, + ), + Instruction(mnemonic="return", opcode=177, operands=[], pos=8), ] diff --git a/tests/test_modified_utf8.py b/tests/test_modified_utf8.py deleted file mode 100644 index b8fc07c..0000000 --- a/tests/test_modified_utf8.py +++ /dev/null @@ -1,47 +0,0 @@ -from jawa.util.utf import encode_modified_utf8, decode_modified_utf8 - - -def test_decode_modified_utf8(loader): - """ - JVM ClassFile's use a "modified" form of UTF8 which cannot always be - parsed by python's UTF-8 decoder. - - We simply need to make sure no encoding exceptions are raised - when we parse the sample ClassFile. - """ - loader.load('ModifiedUTF8') - - -def test_encode_utf8_1(): - """ - Tests encoding of some special cases: - 1 - byte 00 must be encoded as 'c080' - 2 - supplementary characters (represented by the two surrogate code - units of their UTF-16 representation): each surrogate must be encoded - by three bytes. This means supplementary characters are represented by - six bytes then U+10400 (represented as \uD801\uDC00) will be encoded as - 'eda081edb080' - """ - pairs = ( - (u'1\x002', b'\x31\xc0\x80\x32'), - (u'\uD801\uDC00', b'\xed\xa0\x81\xed\xb0\x80') - ) - for original, encoded in pairs: - assert encode_modified_utf8(original) == encoded - - -def test_decode_utf8_1(): - """ - Counterpart of test_encode_utf8_1. - - Tests decoding of some special cases: - 1 - c080 must be decoded as byte 00 - 2 - eda081edb080 must be decoded as \uD801\uDC00 (representing U+10400) - """ - pairs = ( - (b'\x31\xc0\x80\x32', '1\x002'), - (b'\xed\xa0\x81\xed\xb0\x80', '\uD801\uDC00') - ) - - for original, decoded in pairs: - assert decode_modified_utf8(original) == decoded diff --git a/tests/test_printable.py b/tests/test_printable.py deleted file mode 100644 index e0f58bb..0000000 --- a/tests/test_printable.py +++ /dev/null @@ -1,28 +0,0 @@ -from jawa.cf import ClassFile -from jawa.constants import ConstantPool - - -def test_printable_constants(): - # Ensure we can successfully repr valid constants without crashing. - pool = ConstantPool() - repr(pool.create_utf8('HelloWorld')) - repr(pool.create_class('HelloWorld')) - repr(pool.create_double(1)) - repr(pool.create_float(1)) - repr(pool.create_integer(1)) - repr(pool.create_long(1)) - repr(pool.create_name_and_type('HelloWorld', 'I')) - repr(pool.create_field_ref('HelloWorld', 'test', 'I')) - repr(pool.create_method_ref('HelloWorld', 'test', 'I)V')) - repr(pool.create_interface_method_ref( - 'HelloWorld', - 'test', - 'I)V' - )) - repr(pool.create_string('HelloWorld')) - - -def test_printable_classes(): - cf = ClassFile.create('HelloWorld') - assert repr(cf) == '' - assert repr(cf.version) == 'ClassVersion(major=50, minor=0)' diff --git a/tests/test_tracer.py b/tests/test_tracer.py deleted file mode 100644 index e69de29..0000000