`__
110 |
111 | Contact
112 | -------
113 |
114 | Author: Christopher O’Brien obriencj@gmail.com
115 |
116 | If you’re interested in my other projects, feel free to visit `my
117 | blog `__.
118 |
119 | Original Git Repository: https://github.com/obriencj/python-javatools
120 |
121 | License
122 | -------
123 |
124 | This library is free software; you can redistribute it and/or modify it
125 | under the terms of the GNU Lesser General Public License as published by
126 | the Free Software Foundation; either version 3 of the License, or (at
127 | your option) any later version.
128 |
129 | This library is distributed in the hope that it will be useful, but
130 | WITHOUT ANY WARRANTY; without even the implied warranty of
131 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
132 | General Public License for more details.
133 |
134 | You should have received a copy of the GNU Lesser General Public License
135 | along with this library; if not, see http://www.gnu.org/licenses/.
136 |
--------------------------------------------------------------------------------
/javatools/cheetah/classdiff_MethodChange.tmpl:
--------------------------------------------------------------------------------
1 | #extends javatools.cheetah.change_Change
2 | #from javatools.change import collect_by_typename
3 | #from javatools.cheetah import xml_entity_escape as escape
4 |
5 |
6 |
7 | #block description
8 | <%
9 | change = getattr(self, "change")
10 | ldata = change.get_ldata()
11 | a = ldata.get_name()
12 | b = ", ".join(ldata.pretty_arg_types())
13 | c = ldata.pretty_type()
14 | %>
15 | <%= escape("%s(%s):%s" % (a, b, c)) %>
16 | #end block
17 |
18 |
19 |
20 | #block details
21 | <%
22 | change = getattr(self, "change")
23 | data = collect_by_typename(change.collect())
24 | %>
25 |
26 |
27 |
28 |
29 |
39 |
40 |
41 |
42 | #end block
43 |
44 |
45 |
46 | #block collect
47 | <%
48 | change = getattr(self, "change")
49 | data = collect_by_typename(change.collect())
50 | %>
51 |
52 | $render_change(data.pop("MethodAnnotationsChange")[0])
53 | $render_change(data.pop("MethodInvisibleAnnotationsChange")[0])
54 | $render_change(data.pop("MethodCodeChange")[0])
55 |
56 | #end block
57 |
58 |
59 |
60 | #def sub_name(subch)
61 | #set label = "Method Name"
62 | #if subch.is_change()
63 |
64 | | $label |
65 | <%= escape(subch.pretty_ldata()) %> |
66 |
67 |
68 | | <%= escape(subch.pretty_rdata()) %> |
69 |
70 | #else
71 |
72 | | $label |
73 | <%= escape(subch.pretty_ldata()) %>
74 | |
75 | #end if
76 | #end def
77 |
78 |
79 |
80 | #def sub_type(subch)
81 | #set label = "Return Type"
82 | #if subch.is_change()
83 |
84 | | $label |
85 | <%= subch.pretty_ldata() %> |
86 |
87 |
88 | | <%= subch.pretty_rdata() %> |
89 |
90 | #else
91 |
92 | | $label |
93 | <%= subch.pretty_ldata() %>
94 | |
95 | #end if
96 | #end def
97 |
98 |
99 |
100 | #def sub_args(subch)
101 | #set label = "Argument Types"
102 | #if subch.is_change()
103 |
104 | | $label |
105 | (<%= ", ".join(subch.pretty_ldata()) %>) |
106 |
107 |
108 | | (<%= ", ".join(subch.pretty_rdata()) %>) |
109 |
110 | #else
111 |
112 | | $label |
113 | (<%= ", ".join(subch.pretty_ldata()) %>) |
114 |
115 | #end if
116 | #end def
117 |
118 |
119 |
120 | #def sub_flags(subch)
121 | #set label = "Method Flags"
122 | #if subch.is_change()
123 |
124 | | $label |
125 | <%= "0x%04x" % subch.get_ldata() %>:
126 | <%= " ".join(subch.pretty_ldata()) %> |
127 |
128 |
129 | |
130 | <%= "0x%04x" % subch.get_rdata() %>:
131 | <%= " ".join(subch.pretty_rdata()) %> |
132 |
133 | #else
134 |
135 | | $label |
136 | <%= "0x%04x" % subch.get_ldata() %>:
137 | <%= " ".join(subch.pretty_ldata()) %> |
138 |
139 | #end if
140 | #end def
141 |
142 |
143 |
144 | #def sub_signature(subch)
145 | #set label = "Generics Signature"
146 | #if subch.is_change()
147 |
148 | | $label |
149 | <%= escape(subch.pretty_ldata() or "(None)") %> |
150 |
151 |
152 | |
153 | <%= escape(subch.pretty_rdata() or "(None)") %> |
154 |
155 | #elif subch.get_ldata()
156 |
157 | | $label |
158 | <%= escape(subch.pretty_ldata()) %> |
159 |
160 | #end if
161 | #end def
162 |
163 |
164 |
165 | #def sub_throws(subch)
166 | #set label = "Exceptions"
167 | #if subch.is_change()
168 |
169 | | $label |
170 | (<%= ", ".join(subch.pretty_ldata()) %>) |
171 |
172 |
173 | | (<%= ", ".join(subch.pretty_rdata()) %>) |
174 |
175 | #elif subch.get_ldata()
176 |
177 | | $label |
178 | (<%= ", ".join(subch.pretty_ldata()) %>) |
179 |
180 | #end if
181 | #end def
182 |
183 |
184 |
185 | #def sub_abstract(subch)
186 | #set label = "Abstract"
187 | #if subch.is_change()
188 |
189 | | $label |
190 | <%= subch.pretty_ldata() %> |
191 |
192 |
193 | | <%= subch.pretty_rdata() %> |
194 |
195 | #elif subch.get_ldata()
196 |
197 | | $label |
198 | <%= subch.pretty_ldata() %> |
199 |
200 | #end if
201 | #end def
202 |
203 |
204 |
205 | #def sub_deprecation(subch)
206 | #set label = "Deprecated"
207 | #if subch.is_change()
208 |
209 | | $label |
210 | <%= subch.pretty_ldata() %> |
211 |
212 |
213 | | <%= subch.pretty_rdata() %> |
214 |
215 | #elif subch.get_ldata()
216 |
217 | | $label |
218 | <%= subch.pretty_ldata() %> |
219 |
220 | #end if
221 | #end def
222 |
223 |
224 |
225 | ##
226 | ## The end.
227 |
--------------------------------------------------------------------------------
/python-javatools.spec:
--------------------------------------------------------------------------------
1 | %global srcproj javatools
2 | %global srcname python-%{srcproj}
3 | %global srcver 1.6.0
4 | %global srcrel 1
5 |
6 |
7 | # There's two distinct eras of RPM packaging for python, with
8 | # different macros and different expectations. Generally speaking the
9 | # new features are available in RHEL 8+ and Fedora 22+
10 |
11 | %define old_rhel ( 0%{?rhel} && 0%{?rhel} < 8 )
12 | %define old_fedora ( 0%{?fedora} && 0%{?fedora} < 22 )
13 |
14 | %if %{old_rhel} || %{old_fedora}
15 | # old python 2.6 support
16 | %define with_old_python 1
17 | %undefine with_python2
18 | %undefine with_python3
19 | %else
20 | # newer pythons, with cooler macros
21 | %undefine with_old_python
22 | %bcond_with python2
23 | %bcond_without python3
24 | %endif
25 |
26 |
27 | # we don't generate binaries, let's turn the debuginfo part off
28 | %global debug_package %{nil}
29 |
30 |
31 | Summary: Tools for inspecting and comparing binary Java class files
32 | Name: %{srcname}
33 | Version: %{srcver}
34 | Release: %{srcrel}%{?dist}
35 | License: LGPLv3
36 | Group: Application/System
37 | URL: https://github.com/obriencj/python-javatools/
38 | BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
39 | BuildArch: noarch
40 |
41 | Source0: %{srcname}-%{srcver}.tar.gz
42 |
43 |
44 | %description
45 | Tools for inspecting and comparing binary Java class files, JARs, and
46 | JAR-based distributions
47 |
48 |
49 | %prep
50 | %setup -q
51 |
52 |
53 | %build
54 |
55 | %if %{with old_python}
56 | %{__python} setup.py build
57 | %endif
58 |
59 | %if %{with python2}
60 | %py2_build_wheel
61 | %endif
62 |
63 | %if %{with python3}
64 | %py3_build_wheel
65 | %endif
66 |
67 |
68 | %install
69 | %__rm -rf $RPM_BUILD_ROOT
70 |
71 | %if %{with old_python}
72 | %{__python} setup.py install --skip-build --root %{buildroot}
73 | %endif
74 |
75 | %if %{with python2}
76 | %py2_install_wheel %{srcproj}-%{version}-py2-none-any.whl
77 | %endif
78 |
79 | %if %{with python3}
80 | %py3_install_wheel %{srcproj}-%{version}-py3-none-any.whl
81 | %endif
82 |
83 |
84 | %clean
85 | rm -rf %{buildroot}
86 |
87 |
88 | %if %{with old_python}
89 | # package support for older python systems (centos 6, fedora
90 | # 19) with only python 2.6 available.
91 |
92 | %package -n python2-%{srcproj}
93 | Summary: %{summary}
94 | BuildRequires: python-devel python-setuptools
95 | BuildRequires: python-cheetah python-six
96 | Requires: python python-argparse python-setuptools
97 | Requires: python-cheetah python-six
98 | %{?python_provide:%python_provide python2-%{srcproj}}
99 |
100 | %description -n python2-%{srcproj}
101 | Python Java Tools
102 |
103 | %files -n python2-%{srcproj}
104 | %defattr(-,root,root,-)
105 | %{python2_sitelib}/javatools/
106 | %{python2_sitelib}/javatools-%{version}.dist-info
107 | %{_bindir}/*
108 |
109 | %doc AUTHORS ChangeLog README.md
110 | %license LICENSE
111 |
112 | %endif
113 |
114 |
115 | %if %{with python2}
116 |
117 | %package -n python2-%{srcproj}
118 | Summary: %{summary}
119 | BuildRequires: python2-devel
120 | BuildRequires: python2-pip python2-setuptools python2-wheel
121 | BuildRequires: python2-cheetah python2-six
122 | Requires: python2 python2-setuptools
123 | Requires: python2-cheetah python2-six
124 | %{?python_provide:%python_provide python2-%{srcproj}}
125 | %{?py_provides:%py_provides python2-%{srcproj}}
126 |
127 | %description -n python2-%{srcproj}
128 | Python Java Tools
129 |
130 | %files -n python2-%{srcproj}
131 | %defattr(-,root,root,-)
132 | %{python2_sitelib}/javatools/
133 | %{python2_sitelib}/javatools-%{version}.dist-info
134 | %{_bindir}/*
135 |
136 | %doc AUTHORS ChangeLog README.md
137 | %license LICENSE
138 |
139 | %endif
140 |
141 |
142 | %if %{with python3}
143 |
144 | %package -n python3-%{srcproj}
145 | Summary: %{summary}
146 | BuildRequires: python3-devel
147 | BuildRequires: python3-pip python3-setuptools python3-wheel
148 | BuildRequires: python3-cheetah python3-six
149 | Requires: python3 python3-setuptools
150 | Requires: python3-cheetah python3-six
151 | %{?python_provide:%python_provide python3-%{srcproj}}
152 | %{?py_provides:%py_provides python3-%{srcproj}}
153 |
154 | %description -n python3-%{srcproj}
155 | Python Java Tools
156 |
157 | %files -n python3-%{srcproj}
158 | %defattr(-,root,root,-)
159 | %{python3_sitelib}/javatools/
160 | %{python3_sitelib}/javatools-%{version}.dist-info
161 | %{_bindir}/*
162 |
163 | %doc AUTHORS ChangeLog README.md
164 | %license LICENSE
165 |
166 | %endif
167 |
168 |
169 | %changelog
170 |
171 | * Thu Jul 27 2023 Christopher O'Brien - 1.6.0-1
172 | - version 1.6.0
173 | - m2crypto is runtime optional
174 | - python2 and python3 support
175 |
176 | * Sun Jun 21 2020 Christopher O'Brien - 1.5.0-1
177 | - version 1.5.0
178 |
179 | * Sun Oct 05 2019 Christopher O'Brien - 1.4.0-1
180 | - version 1.4.0
181 | - added ChangeLog as its own file
182 | - move to setuptools
183 |
184 | * Thu May 23 2013 Christopher O'Brien - 1.3-1
185 | - bump to 1.3
186 |
187 | * Thu Jun 14 2012 Christopher O'Brien - 1.2-1
188 | - require python 2.6 and later
189 |
190 | * Sun May 06 2012 Christopher O'Brien - 1.1-1
191 | - dependency features, license files
192 |
193 | * Fri Apr 27 2012 Christopher O'Brien - 1.0-1
194 | - Initial build.
195 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Overview of python-javatools
3 |
4 | A [python] module for unpacking and inspecting [Java] Class files,
5 | JARs, and collections of either. Supporting features up to JDK 8.
6 |
7 | [python]: http://python.org
8 | [java]: http://www.oracle.com/technetwork/java/index.html
9 |
10 | It can do deep checking of classes to perform comparisons of
11 | functionality, and output reports in multiple formats.
12 |
13 | * [python-javatools on GitHub][github]
14 | * [python-javatools on PyPI][pypi]
15 |
16 | [github]: https://github.com/obriencj/python-javatools/
17 | [pypi]: http://pypi.python.org/pypi/javatools
18 |
19 | If you have suggestions, please use the [issue tracker] on github. Or
20 | heck, just fork it!
21 |
22 | [issue tracker]: https://github.com/obriencj/python-javatools/issues
23 |
24 |
25 | ## Requirements
26 |
27 | * [Python] 2.7, 3.7, 3.8, 3.9, 3.10, 3.11
28 | * [Setuptools]
29 | * [Six]
30 | * [Cheetah3] is used in the generation of HTML reports
31 | * [M2Crypto] (optional) is used for cryptographic operations
32 |
33 | In addition, the following tools are used in building and testing the
34 | project.
35 |
36 | * [Tox]
37 | * [GNU Make]
38 | * [Flake8]
39 |
40 | All of these packages are available in most linux distributions
41 | (eg. Fedora), and for OSX via [MacPorts] and [HomeBrew], or available
42 | directly from pip.
43 |
44 | M2Crypto can be difficult on some platforms, and so is set as an
45 | optional dependency. If an execution path attempts to perform an
46 | action which requires M2Crypto (primarily Jar signing and Jar
47 | signature verification), then a `CryptoDisabled` exception will be
48 | raised, or a message will be printed to stdout explaining that the
49 | feature is unavailable. See the [M2Crypto Install Guide] for
50 | workarounds in your environment.
51 |
52 | [six]: https://pypi.org/project/six/
53 | [cheetah3]: http://www.cheetahtemplate.org
54 | [pyxml]: http://www.python.org/community/sigs/current/xml-sig/
55 | [M2Crypto]: https://gitlab.com/m2crypto/m2crypto/
56 |
57 | [setuptools]: https://pypi.org/project/setuptools/
58 | [gnu make]: http://www.gnu.org/software/make/
59 | [flake8]: https://pypi.org/project/flake8/
60 | [tox]: https://pypi.org/project/tox
61 |
62 | [fedora]: http://fedoraproject.org
63 | [macports]: http://www.macports.org
64 | [homebrew]: https://brew.sh/
65 |
66 | [M2Crypto Install Guide]: https://gitlab.com/m2crypto/m2crypto/-/blob/master/INSTALL.rst
67 |
68 |
69 | ## Building
70 |
71 | This module uses [setuptools], so running the following will build the
72 | project:
73 |
74 | ```python setup.py build```
75 |
76 | to install, run:
77 |
78 | ```python -m pip install . --user```
79 |
80 |
81 | ### Testing
82 |
83 | Tests are written as `unittest` test cases. If you'd like to run the tests,
84 | simply invoke:
85 |
86 | ```python setup.py test```
87 |
88 | or invoke tests across a wider range of platforms via ``tox``
89 |
90 |
91 | ### RPM
92 |
93 | If you'd prefer to build an RPM, see the wiki entry for
94 | [Building as an RPM].
95 |
96 | [building as an rpm]: https://github.com/obriencj/python-javatools/wiki/Building-as-an-RPM
97 |
98 |
99 | ## Javatools Scripts
100 |
101 | * classinfo - similar to the javap utility included with most
102 | JVMs. Also does provides/requires tracking.
103 |
104 | * classdiff - attempts to find differences between two Java class
105 | files
106 |
107 | * jarinfo - prints information about a JAR. Also does
108 | provides/requires tracking.
109 |
110 | * jardiff - prints the deltas between the contents of a JAR, and runs
111 | classdiff on differing Java class files contained in the JARs
112 |
113 | * jarutil - creates and signs JARs, verifies JAR signatures
114 |
115 | * manifest - creates and queries JAR manifests
116 |
117 | * distinfo - prints information about a mixed multi-jar/class
118 | distribution, such as provides/requires lists.
119 |
120 | * distdiff - attempts to find differences between two distributions,
121 | deep-checking any JARs or Java class files found in either
122 | directory.
123 |
124 |
125 | ## Additional References
126 |
127 | * Oracle's Java Virtual Machine Specification
128 | [Chapter 4 "The class File Format"][jvms-4]
129 | * [Java Archive (JAR) Files][jars]
130 |
131 | [jvms-4]: http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html
132 | [jars]: http://docs.oracle.com/javase/1.5.0/docs/guide/jar/index.html
133 |
134 |
135 | ## Contact
136 |
137 | Author: Christopher O'Brien
138 |
139 | If you're interested in my other projects, feel free to visit
140 | [my blog].
141 |
142 | [my blog]: http://obriencj.preoccupied.net/
143 |
144 | Original Git Repository:
145 |
146 |
147 | ## License
148 |
149 | This library is free software; you can redistribute it and/or modify
150 | it under the terms of the GNU Lesser General Public License as
151 | published by the Free Software Foundation; either version 3 of the
152 | License, or (at your option) any later version.
153 |
154 | This library is distributed in the hope that it will be useful, but
155 | WITHOUT ANY WARRANTY; without even the implied warranty of
156 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
157 | Lesser General Public License for more details.
158 |
159 | You should have received a copy of the GNU Lesser General Public
160 | License along with this library; if not, see
161 | .
162 |
--------------------------------------------------------------------------------
/javatools/cheetah/setuptools.py:
--------------------------------------------------------------------------------
1 | # This library is free software; you can redistribute it and/or modify
2 | # it under the terms of the GNU Lesser General Public License as
3 | # published by the Free Software Foundation; either version 3 of the
4 | # License, or (at your option) any later version.
5 | #
6 | # This library is distributed in the hope that it will be useful, but
7 | # WITHOUT ANY WARRANTY; without even the implied warranty of
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 | # Lesser General Public License for more details.
10 | #
11 | # You should have received a copy of the GNU Lesser General Public
12 | # License along with this library; if not, see
13 | # .
14 |
15 |
16 | """
17 | Replacement setuptools/distutils build_py command which also
18 | compiles Cheetah templates to Python
19 |
20 | author: Christopher O'Brien
21 | license: LGPL v.3
22 | """
23 |
24 |
25 | from distutils.core import Command
26 | from distutils.util import newer
27 | from distutils.command.build_py import build_py
28 | from glob import glob
29 | from os import makedirs
30 | from os.path import basename, exists, join, splitext
31 |
32 |
33 | DEFAULT_CONFIG = "extras/cheetah.cfg"
34 |
35 |
36 | class cheetah_cmd(Command):
37 | """
38 | command that compiles Cheetah template files into Python
39 | """
40 |
41 | # TODO: move the act of actually compiling .tmpl -> .py into this
42 | # command, and simply make the build_py command depend on this one
43 |
44 | pass
45 |
46 |
47 | class cheetah_build_py_cmd(build_py):
48 | """
49 | build_py command with some special handling for Cheetah template
50 | files. Takes tmpl from package source directories and compiles
51 | them for distribution. This allows me to write tmpl files in the
52 | src dir of my project, and have them get compiled to py/pyc/pyo
53 | files during the build process.
54 | """
55 |
56 | # Note: it's important to override build_py rather than to add a
57 | # sub-command to build. The build command doesn't collate the
58 | # get_outputs of its sub-commands, and install specifically looks
59 | # for build_py and build_ext for the list of files to install.
60 |
61 | # Update: now that I've switched from distutils to setuptools, it
62 | # may be possible to put this into a subcommand instead. Have to
63 | # investigate.
64 |
65 | def find_package_templates(self, package, package_dir):
66 | # template files will be located under src, and will end in .tmpl
67 |
68 | self.check_package(package, package_dir)
69 | template_files = glob(join(package_dir, "*.tmpl"))
70 | templates = []
71 |
72 | for f in template_files:
73 | template = splitext(basename(f))[0]
74 | templates.append((package, template, f))
75 | return templates
76 |
77 |
78 | def build_package_templates(self):
79 | for package in self.packages:
80 | package_dir = self.get_package_dir(package)
81 | templates = self.find_package_templates(package, package_dir)
82 |
83 | for package_, template, template_file in templates:
84 | assert package == package_
85 | self.build_template(template, template_file, package)
86 |
87 |
88 | def build_template(self, template, template_file, package):
89 | """
90 | Compile the cheetah template in src into a python file in build
91 | """
92 |
93 | try:
94 | from Cheetah.Compiler import Compiler
95 | except ImportError:
96 | self.announce("unable to import Cheetah.Compiler, build failed")
97 | raise
98 | else:
99 | comp = Compiler(file=template_file, moduleName=template)
100 |
101 | # load configuration if it exists
102 | conf_fn = DEFAULT_CONFIG
103 | if exists(conf_fn):
104 | with open(conf_fn, "rt") as config:
105 | comp.updateSettingsFromConfigFileObj(config)
106 |
107 | # and just why can't I configure these?
108 | comp.setShBang("")
109 | comp.addModuleHeader("pylint: disable=C,W,R,F")
110 |
111 | outfd = join(self.build_lib, *package.split("."))
112 | outfn = join(outfd, template + ".py")
113 |
114 | if not exists(outfd):
115 | makedirs(outfd)
116 |
117 | if newer(template_file, outfn):
118 | self.announce("compiling %s -> %s" % (template_file, outfd), 2)
119 | with open(outfn, "w") as output:
120 | output.write(str(comp))
121 |
122 |
123 | def get_template_outputs(self, include_bytecode=1):
124 | built = list()
125 |
126 | for package in self.packages:
127 | package_dir = self.get_package_dir(package)
128 | templates = self.find_package_templates(package, package_dir)
129 | for _, template, _ in templates:
130 | outfd = join(self.build_lib, *package.split("."))
131 | outfn = join(outfd, template + ".py")
132 |
133 | built.append(outfn)
134 |
135 | if include_bytecode:
136 | if self.compile:
137 | built.append(outfn + "c")
138 | if self.optimize > 0:
139 | built.append(outfn + "o")
140 |
141 | return built
142 |
143 |
144 | def get_outputs(self, include_bytecode=1):
145 | # Overridden to append our compiled templates in addition to
146 | # the normal build outputs.
147 |
148 | outputs = build_py.get_outputs(self, include_bytecode)
149 | outputs.extend(self.get_template_outputs(include_bytecode))
150 |
151 | return outputs
152 |
153 |
154 | def run(self):
155 | if self.packages:
156 | self.build_package_templates()
157 |
158 | # old-style class, can't use super
159 | build_py.run(self)
160 |
161 |
162 | #
163 | # The end.
164 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | # This file defines the general configuration for the javatools
2 | # wheel, and the tools tox, nose, flake8, coverage, and sphinx
3 |
4 |
5 | [metadata]
6 | name = javatools
7 | version = 1.6.0
8 | description = Tools for working with Java class files and JARs
9 |
10 | author = Christopher O'Brien
11 | author_email = obriencj@gmail.com
12 |
13 | license = GNU Lesser General Public License v3 (LGPLv3)
14 | license_files =
15 | LICENSE
16 |
17 | long_description = file: README.md
18 | long_description_content_type = text/markdown
19 |
20 | home_page = https://github.com/obriencj/python-javatools
21 |
22 | platform = any
23 |
24 | project_urls =
25 | Source = https://github.com/obriencj/python-javatools
26 | Bug Reports = https://github.com/obriencj/python-javatools/issues
27 |
28 | classifiers =
29 | Development Status :: 5 - Production/Stable
30 | Environment :: Console
31 | Intended Audience :: Developers
32 | Intended Audience :: Information Technology
33 | License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
34 | Programming Language :: Python :: 2
35 | Programming Language :: Python :: 2.7
36 | Programming Language :: Python :: 3
37 | Programming Language :: Python :: 3.7
38 | Programming Language :: Python :: 3.8
39 | Programming Language :: Python :: 3.9
40 | Programming Language :: Python :: 3.10
41 | Programming Language :: Python :: 3.11
42 | Topic :: Software Development :: Disassemblers
43 |
44 |
45 | [options]
46 | packages =
47 | javatools
48 | javatools.cheetah
49 |
50 | zip_safe = False
51 |
52 | setup_requires =
53 | CT3 # Cheetah3
54 | six
55 |
56 | install_requires =
57 | CT3 # Cheetah3
58 | six
59 |
60 | tests_require =
61 | CT3 # Cheetah3
62 | M2Crypto >= 0.26.0
63 | coverage
64 | six
65 |
66 |
67 | [options.extras_require]
68 | crypto =
69 | M2Crypto >= 0.26.0
70 |
71 |
72 | [options.package_data]
73 | javatools.cheetah =
74 | *.tmpl
75 | data/*.css
76 | data/*.js
77 | data/*.png
78 |
79 |
80 | [options.entry_points]
81 | console_scripts =
82 | classdiff=javatools.classdiff:main
83 | classinfo=javatools.classinfo:main
84 | distdiff=javatools.distdiff:main
85 | distinfo=javatools.distinfo:main
86 | jardiff=javatools.jardiff:main
87 | jarinfo=javatools.jarinfo:main
88 | jarutil=javatools.jarutil:main
89 | manifest=javatools.manifest:main
90 |
91 |
92 | [aliases]
93 | # nose acts enough like the original test command, but without the
94 | # extremely obnoxious deprecation warning. And why are they
95 | # deprecating the test command? So someone can remove approximately 40
96 | # lines of code from setuptools, despite the test command being the
97 | # most convenient and available way to get started with unit testing.
98 |
99 | test = nosetests
100 |
101 |
102 | [tox:tox]
103 | envlist = flake8,py{27,37,38,39,310,311},coverage,bandit,twine
104 | skip_missing_interpreters = true
105 |
106 |
107 | [testenv]
108 | setenv =
109 | COVERAGE_FILE = .coverage.{envname}
110 |
111 | commands =
112 | python -B -m coverage run -m nose
113 |
114 | sitepackages = true
115 |
116 | download = true
117 |
118 | deps =
119 | CT3 # Cheetah3
120 | M2Crypto>=0.26.0
121 | coverage
122 | nose-py3
123 | six
124 |
125 |
126 | [testenv:py27]
127 |
128 | deps =
129 | CT3 # Cheetah3
130 | M2Crypto>=0.26.0
131 | coverage
132 | nose
133 | six
134 |
135 |
136 | [testenv:bandit]
137 |
138 | basepython = python3.9
139 |
140 | commands =
141 | python -B -m bandit --ini setup.cfg -qr javatools
142 |
143 | deps =
144 | bandit
145 |
146 |
147 | [testenv:twine]
148 |
149 | basepython = python3.9
150 |
151 | commands =
152 | python -B setup.py bdist_wheel
153 | python -B -m twine check --strict dist/*.whl
154 |
155 | deps =
156 | twine
157 |
158 |
159 | [testenv:flake8]
160 |
161 | basepython = python3.9
162 |
163 | commands =
164 | python -B -m flake8 javatools
165 |
166 | deps =
167 | flake8
168 |
169 |
170 | [testenv:coverage]
171 | # this is just here to combine the coverage output
172 |
173 | setenv =
174 | COVERAGE_FILE = .coverage
175 |
176 | basepython = python
177 |
178 | commands =
179 | python -B -m coverage combine
180 | python -B -m coverage report
181 | python -B -m coverage html
182 |
183 |
184 | [nosetests]
185 |
186 | all-modules = 1
187 | no-byte-compile = 1
188 | verbosity = 2
189 |
190 |
191 | [coverage:run]
192 |
193 | source =
194 | javatools
195 |
196 |
197 | [coverage:report]
198 |
199 | exclude_lines =
200 | \.\.\.
201 | pass
202 | pragma: no cover
203 | @abstract
204 |
205 |
206 | [bandit]
207 | # B101 complains about asserts
208 |
209 | skips = B101
210 |
211 |
212 | [flake8]
213 | # E303 complains about more than one blank lines between methods in a class
214 | # E731 assigning a lambda to a variable
215 | # E741 ambiguous variable name
216 | # F401 ambiguous variable name
217 | # F812 list comprehension redefines variable (I reuse tmp names)
218 | # W504 line break after binary operator
219 |
220 | ignore = E303,E731,E741,F401,F812,W504
221 |
222 | filename =
223 | *.py
224 | *.pyi
225 |
226 | exclude =
227 | __pycache__
228 | .*
229 | build
230 | dist
231 | docs
232 | gh-pages
233 | htmlcov
234 | setup.py
235 | tests
236 | todo
237 | tools
238 |
239 |
240 | [testenv:sphinx]
241 |
242 | basepython = python3.9
243 |
244 | commands =
245 | python -B setup.py build_sphinx
246 |
247 | # sphinx 7 not only doesn't have a build_sphinx command, but it also
248 | # completely ignores the settings in setup.cfg
249 | deps =
250 | sphinx<7
251 | numpydoc
252 |
253 |
254 | [build_sphinx]
255 | # some of the configuration for sphinx. The rest of it lives over in
256 | # docs/conf.py
257 |
258 | version = 1.6
259 | release = 1.6.0
260 |
261 | project = python-javatools
262 | copyright = 2014-2023, Christopher O'Brien
263 |
264 | build-dir = build/sphinx
265 | builder = dirhtml html
266 | source-dir = docs
267 |
268 |
269 | [gh-actions]
270 | python =
271 | 2.7: py27
272 | 3.7: py37
273 | 3.8: py38
274 | 3.9: py39, flake8, bandit
275 | 3.10: py310
276 |
277 |
278 | #
279 | # The end.
280 |
--------------------------------------------------------------------------------
/javatools/dirutils.py:
--------------------------------------------------------------------------------
1 | # This library is free software; you can redistribute it and/or modify
2 | # it under the terms of the GNU Lesser General Public License as
3 | # published by the Free Software Foundation; either version 3 of the
4 | # License, or (at your option) any later version.
5 | #
6 | # This library is distributed in the hope that it will be useful, but
7 | # WITHOUT ANY WARRANTY; without even the implied warranty of
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 | # Lesser General Public License for more details.
10 | #
11 | # You should have received a copy of the GNU Lesser General Public
12 | # License along with this library; if not, see
13 | # .
14 |
15 |
16 | """
17 | Utility module for discovering the differences between two directory
18 | trees
19 |
20 | :author: Christopher O'Brien
21 | :license: LGPL
22 | """
23 |
24 |
25 | from filecmp import dircmp
26 | from fnmatch import fnmatch
27 | from os import makedirs, walk
28 | from os.path import exists, isdir, join, relpath
29 | from shutil import copy
30 |
31 |
32 | LEFT = "left only"
33 | RIGHT = "right only"
34 | DIFF = "changed"
35 | SAME = "same"
36 | BOTH = SAME # meh, synonyms
37 |
38 |
39 | def fnmatches(entry, *pattern_list):
40 | """
41 | returns true if entry matches any of the glob patterns, false
42 | otherwise
43 | """
44 |
45 | for pattern in pattern_list:
46 | if pattern and fnmatch(entry, pattern):
47 | return True
48 | return False
49 |
50 |
51 | def makedirsp(dirname):
52 | """
53 | create dirname if it doesn't exist
54 | """
55 |
56 | if dirname and not exists(dirname):
57 | makedirs(dirname)
58 |
59 |
60 | def copydir(orig, dest):
61 | """
62 | copies directory orig to dest. Returns a list of tuples of
63 | relative filenames which were copied from orig to dest
64 | """
65 |
66 | copied = list()
67 |
68 | makedirsp(dest)
69 |
70 | for root, dirs, files in walk(orig):
71 | for d in dirs:
72 | # ensure directories exist
73 | makedirsp(join(dest, d))
74 |
75 | for f in files:
76 | root_f = join(root, f)
77 | dest_f = join(dest, relpath(root_f, orig))
78 | copy(root_f, dest_f)
79 | copied.append((root_f, dest_f))
80 |
81 | return copied
82 |
83 |
84 | def compare(left, right):
85 | """
86 | generator emiting pairs indicating the contents of the left and
87 | right directories. The pairs are in the form of (difference,
88 | filename) where difference is one of the LEFT, RIGHT, DIFF, or
89 | BOTH constants. This generator recursively walks both trees.
90 | """
91 |
92 | dc = dircmp(left, right, ignore=[])
93 | return _gen_from_dircmp(dc, left, right)
94 |
95 |
96 | def _gen_from_dircmp(dc, lpath, rpath):
97 | """
98 | do the work of comparing the dircmp
99 | """
100 |
101 | left_only = dc.left_only
102 | left_only.sort()
103 |
104 | for f in left_only:
105 | fp = join(dc.left, f)
106 | if isdir(fp):
107 | for r, _ds, fs in walk(fp):
108 | r = relpath(r, lpath)
109 | for f in fs:
110 | yield (LEFT, join(r, f))
111 | else:
112 | yield (LEFT, relpath(fp, lpath))
113 |
114 | right_only = dc.right_only
115 | right_only.sort()
116 |
117 | for f in right_only:
118 | fp = join(dc.right, f)
119 | if isdir(fp):
120 | for r, _ds, fs in walk(fp):
121 | r = relpath(r, rpath)
122 | for f in fs:
123 | yield (RIGHT, join(r, f))
124 | else:
125 | yield (RIGHT, relpath(fp, rpath))
126 |
127 | diff_files = dc.diff_files
128 | diff_files.sort()
129 |
130 | for f in diff_files:
131 | yield (DIFF, join(relpath(dc.right, rpath), f))
132 |
133 | same_files = dc.same_files
134 | same_files.sort()
135 |
136 | for f in same_files:
137 | yield (BOTH, join(relpath(dc.left, lpath), f))
138 |
139 | subdirs = dc.subdirs.values()
140 | subdirs = sorted(subdirs)
141 | for sub in subdirs:
142 | for event in _gen_from_dircmp(sub, lpath, rpath):
143 | yield event
144 |
145 |
146 | def collect_compare(left, right):
147 | """
148 | returns a tuple of four lists describing the file paths that have
149 | been (in order) added, removed, altered, or left the same
150 | """
151 |
152 | return collect_compare_into(left, right, [], [], [], [])
153 |
154 |
155 | def collect_compare_into(left, right, added, removed, altered, same):
156 | """
157 | collect the results of compare into the given lists (or None if
158 | you do not wish to collect results of that type. Returns a tuple
159 | of (added, removed, altered, same)
160 | """
161 |
162 | for event, filename in compare(left, right):
163 | if event == LEFT:
164 | group = removed
165 |
166 | elif event == RIGHT:
167 | group = added
168 |
169 | elif event == DIFF:
170 | group = altered
171 |
172 | elif event == BOTH:
173 | group = same
174 |
175 | else:
176 | assert False
177 |
178 | if group is not None:
179 | group.append(filename)
180 |
181 | return added, removed, altered, same
182 |
183 |
184 | class ClosingContext(object):
185 | # pylint: disable=R0903
186 | # too few public methods (none)
187 |
188 | """
189 | A simple context manager which is created with an object instance,
190 | and will return that instance from __enter__ and call the close
191 | method on the instance in __exit__
192 | """
193 |
194 |
195 | def __init__(self, managed):
196 | self.managed = managed
197 |
198 |
199 | def __enter__(self):
200 | return self.managed
201 |
202 |
203 | def __exit__(self, exc_type, _exc_val, _exc_tb):
204 | managed = self.managed
205 | self.managed = None
206 |
207 | if managed is not None and hasattr(managed, "close"):
208 | managed.close()
209 |
210 | return exc_type is None
211 |
212 |
213 | def closing(managed):
214 | """
215 | If the managed object already provides its own context management
216 | via the __enter__ and __exit__ methods, it is returned
217 | unchanged. However, if the instance does not, a ClosingContext
218 | will be created to wrap it. When the ClosingContext exits, it will
219 | call managed.close()
220 | """
221 |
222 | if managed is None or hasattr(managed, "__enter__"):
223 | return managed
224 | else:
225 | return ClosingContext(managed)
226 |
227 |
228 | #
229 | # The end.
230 |
--------------------------------------------------------------------------------
/tests/manifest.py:
--------------------------------------------------------------------------------
1 | # This library is free software; you can redistribute it and/or modify
2 | # it under the terms of the GNU Lesser General Public License as
3 | # published by the Free Software Foundation; either version 3 of the
4 | # License, or (at your option) any later version.
5 | #
6 | # This library is distributed in the hope that it will be useful, but
7 | # WITHOUT ANY WARRANTY; without even the implied warranty of
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 | # Lesser General Public License for more details.
10 | #
11 | # You should have received a copy of the GNU Lesser General Public
12 | # License along with this library; if not, see
13 | # .
14 |
15 |
16 | """
17 | unit tests for manifest-related functionality of python-javatools
18 |
19 | author: Christopher O'Brien
20 | license: LGPL v.3
21 | """
22 |
23 |
24 | from . import get_data_fn
25 | from javatools.manifest import main, Manifest, SignatureManifest
26 |
27 | from tempfile import NamedTemporaryFile
28 | from unittest import TestCase
29 |
30 |
31 | class ManifestTest(TestCase):
32 |
33 |
34 | def manifest_cli_create(self, args, expected_result):
35 | """
36 | execute the CLI manifest tool with the given arguments on our
37 | sample JAR. Verifies that the resulting output manifest is
38 | identical to the expected result.
39 | """
40 |
41 | # the result we expect to see from running the script
42 | with open(get_data_fn(expected_result), mode="rb") as f:
43 | expected_result = f.read()
44 |
45 | # the invocation of the script
46 | src_jar = get_data_fn("test_manifest/manifest-sample1.jar")
47 | with NamedTemporaryFile() as tmp_out:
48 | cmd = ["manifest", "c", src_jar, "-m", tmp_out.name]
49 | cmd.extend(args.split())
50 |
51 | # rather than trying to actually execute the script in a
52 | # subprocess, we'll give it an output file call it in the
53 | # current process. This prevents issues when there's already
54 | # an installed version of python-javatools present which may
55 | # be down-version from the one being tested. Calling the
56 | # manifest utility by name will use the installed rather than
57 | # local dev copy. Might be able to tweak this, but for now,
58 | # this is safer.
59 | main(cmd)
60 | result = tmp_out.read()
61 |
62 | self.assertEqual(
63 | result, expected_result,
64 | "Result of \"%r\" does not match expected output."
65 | " Expected:\n%s\nReceived:\n%s"
66 | % (cmd, expected_result, result))
67 |
68 |
69 | def manifest_load_store(self, src_file):
70 | """
71 | Loads a manifest object from a given sample in the data directory,
72 | then re-writes it and verifies that the result matches the
73 | original.
74 | """
75 |
76 | src_file = get_data_fn(src_file)
77 |
78 | # the expected result is identical to what we feed into the
79 | # manifest parser
80 | with open(src_file, mode='rb') as f:
81 | expected_result = f.read()
82 |
83 | # create a manifest and parse the chosen test data
84 | mf = Manifest()
85 | mf.parse_file(src_file)
86 | result = mf.get_data()
87 |
88 | self.assertEqual(
89 | result, expected_result,
90 | "Manifest load/store does not match with file %s. Received:\n%s"
91 | % (src_file, result))
92 |
93 | return mf
94 |
95 |
96 | def test_create_sha1(self):
97 | self.manifest_cli_create("-d SHA1", "test_manifest/manifest.SHA1.mf")
98 |
99 |
100 | def test_create_sha512(self):
101 | self.manifest_cli_create("-d SHA-512", "test_manifest/manifest.SHA-512.mf")
102 |
103 |
104 | def test_create_with_ignore(self):
105 | self.manifest_cli_create("-i example.txt -d MD5,SHA-512",
106 | "test_manifest/manifest.ignores.mf")
107 |
108 |
109 | def test_load(self):
110 | self.manifest_load_store("test_manifest/manifest.SHA1.mf")
111 |
112 |
113 | def test_load_sha512(self):
114 | self.manifest_load_store("test_manifest/manifest.SHA-512.mf")
115 |
116 |
117 | def test_load_dos_newlines(self):
118 | mf = self.manifest_load_store("test_manifest/manifest.dos-newlines.mf")
119 | self.assertEqual(mf.linesep, "\r\n")
120 |
121 |
122 | def test_cli_verify_ok(self):
123 | jar_file = get_data_fn("test_manifest/cli-verify-ok.jar")
124 | self.assertEqual(0, main(["argv0", "v", jar_file]))
125 |
126 |
127 | def test_cli_verify_nok(self):
128 | jar_file = get_data_fn("test_manifest/cli-verify-nok.jar")
129 | self.assertEqual(1, main(["argv0", "v", jar_file]))
130 |
131 |
132 | def test_verify_mf_checksums_no_whole_digest(self):
133 | sf_file = "test_manifest/sf-no-whole-digest.sf"
134 | mf_file = "test_manifest/sf-no-whole-digest.mf"
135 | sf = SignatureManifest()
136 | sf.parse_file(get_data_fn(sf_file))
137 | mf = Manifest()
138 | mf.parse_file(get_data_fn(mf_file))
139 |
140 | self.assertFalse(
141 | sf.verify_manifest_main_checksum(mf),
142 | "Verification of main signature in file %s against manifest %s"
143 | " succeeded, but the SF file has no Digest-Manifest section"
144 | % (sf_file, mf_file))
145 |
146 | self.assertTrue(
147 | sf.verify_manifest_main_attributes_checksum(mf),
148 | "Verification of Main-Attibutes in file %s against manifest %s"
149 | "failed" % (sf_file, mf_file))
150 |
151 | errors = sf.verify_manifest_entry_checksums(mf)
152 | self.assertEqual(
153 | 0, len(errors),
154 | "The following entries in signature file %s against manifest %s"
155 | " failed: %s"
156 | % (sf_file, mf_file, ",".join(errors)))
157 |
158 |
159 | def test_multi_digests(self):
160 | jar_file = "test_manifest/multi-digests.jar"
161 |
162 | mf_ok_file = "test_manifest/one-valid-digest-of-several.mf"
163 | mf = Manifest()
164 | mf.parse_file(get_data_fn(mf_ok_file))
165 | errors = mf.verify_jar_checksums(get_data_fn(jar_file))
166 | self.assertEqual(
167 | 0, len(errors),
168 | "The following entries in jar file %s do not match"
169 | " in manifest %s: %s"
170 | % (jar_file, mf_ok_file, ",".join(errors)))
171 |
172 | sf_ok_file = "test_manifest/one-valid-digest-of-several.sf"
173 | sf = SignatureManifest()
174 | sf.parse_file(get_data_fn(sf_ok_file))
175 |
176 | errors = sf.verify_manifest(mf)
177 | self.assertEqual(
178 | 0, len(errors),
179 | "The following entries in signature file %s against manifest %s"
180 | " failed: %s"
181 | % (sf_ok_file, mf_ok_file, ",".join(errors)))
182 |
183 |
184 | def test_add_jar_entries(self):
185 | mf = Manifest()
186 | mf.parse_file(get_data_fn("test_manifest/no-entries.mf"))
187 | mf.add_jar_entries(get_data_fn("test_manifest/junk-entries.jar"), "SHA-512")
188 | self.assertIsNotNone(mf.sub_sections.get("README.md", None),
189 | "Expected entry not added to the manifest")
190 |
191 |
192 | #
193 | # The end.
194 |
--------------------------------------------------------------------------------
/javatools/crypto.py:
--------------------------------------------------------------------------------
1 | # This library is free software; you can redistribute it and/or modify
2 | # it under the terms of the GNU Lesser General Public License as
3 | # published by the Free Software Foundation; either version 3 of the
4 | # License, or (at your option) any later version.
5 | #
6 | # This library is distributed in the hope that it will be useful, but
7 | # WITHOUT ANY WARRANTY; without even the implied warranty of
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 | # Lesser General Public License for more details.
10 | #
11 | # You should have received a copy of the GNU Lesser General Public
12 | # License along with this library; if not, see
13 | # .
14 |
15 |
16 | """
17 | Cryptography-related functions for handling JAR signature block files.
18 |
19 | :author: Konstantin Shemyak
20 | :license: LGPL v.3
21 | """
22 |
23 |
24 | from functools import wraps
25 |
26 |
27 | try:
28 | from M2Crypto import SMIME, X509, BIO, RSA, DSA, EC, m2
29 | except ImportError:
30 | __ENABLED = False
31 | else:
32 | __ENABLED = True
33 |
34 |
35 | class CryptoDisabled(Exception):
36 | """
37 | cryptography is disabled due to lack of M2Crypto
38 | """
39 |
40 | pass
41 |
42 |
43 | class CannotFindKeyTypeError(Exception):
44 | """
45 | Failed to determine the type of the private key.
46 | """
47 |
48 | pass
49 |
50 |
51 | class SignatureBlockVerificationError(Exception):
52 | """
53 | The Signature Block File verification failed.
54 | """
55 |
56 | pass
57 |
58 |
59 | def crypto_enabled():
60 | return __ENABLED
61 |
62 |
63 | def requires_crypto(fn):
64 | @wraps(fn)
65 | def wrapper(*args, **kwds):
66 | if not crypto_enabled():
67 | raise CryptoDisabled("javatools API %s requires M2Crypto" %
68 | fn.__name__)
69 | return fn(*args, **kwds)
70 | return wrapper
71 |
72 |
73 | @requires_crypto
74 | def private_key_type(key_file):
75 | """
76 | Determines type of the private key: RSA, DSA, EC.
77 |
78 | :param key_file: file path
79 | :type key_file: str
80 | :return: one of "RSA", "DSA" or "EC"
81 | :except CannotFindKeyTypeError
82 | """
83 |
84 | keytypes = (("RSA", RSA), ("DSA", DSA), ("EC", EC))
85 |
86 | for key, ktype in keytypes:
87 | try:
88 | ktype.load_key(key_file)
89 | except (RSA.RSAError, DSA.DSAError, ValueError):
90 | continue
91 | else:
92 | return key
93 | else:
94 | raise CannotFindKeyTypeError()
95 |
96 |
97 | @requires_crypto
98 | def create_signature_block(openssl_digest, certificate, private_key,
99 | extra_certs, data):
100 |
101 | """
102 | Produces a signature block for the data.
103 |
104 | Reference
105 | ---------
106 | http://docs.oracle.com/javase/7/docs/technotes/guides/jar/jar.html#Digital_Signatures
107 |
108 | Note: Oracle does not specify the content of the "signature
109 | file block", friendly saying that "These are binary files
110 | not intended to be interpreted by humans".
111 |
112 | :param openssl_digest: alrogithm known to OpenSSL used to digest the data
113 | :type openssl_digest: str
114 | :param certificate: filename of the certificate file (PEM format)
115 | :type certificate: str
116 | :param private_key:filename of private key used to sign (PEM format)
117 | :type private_key: str
118 | :param extra_certs: additional certificates to embed into the signature (PEM format)
119 | :type extra_certs: array of filenames
120 | :param data: the content to be signed
121 | :type data: bytes
122 | :returns: content of the signature block file as produced by jarsigner
123 | :rtype: bytes
124 | """ # noqa
125 |
126 | smime = SMIME.SMIME()
127 | with BIO.openfile(private_key) as k, BIO.openfile(certificate) as c:
128 | smime.load_key_bio(k, c)
129 |
130 | if extra_certs is not None:
131 | # Could we use just X509.new_stack_from_der() instead?
132 | stack = X509.X509_Stack()
133 | for cert in extra_certs:
134 | stack.push(X509.load_cert(cert))
135 | smime.set_x509_stack(stack)
136 |
137 | pkcs7 = smime.sign(BIO.MemoryBuffer(data),
138 | algo=openssl_digest,
139 | flags=(SMIME.PKCS7_BINARY |
140 | SMIME.PKCS7_DETACHED |
141 | SMIME.PKCS7_NOATTR))
142 | tmp = BIO.MemoryBuffer()
143 | pkcs7.write_der(tmp)
144 | return tmp.read()
145 |
146 |
147 | @requires_crypto
148 | def ignore_missing_email_protection_eku_cb(ok, ctx):
149 | """
150 | For verifying PKCS7 signature, m2Crypto uses OpenSSL's PKCS7_verify().
151 | The latter requires that ExtendedKeyUsage extension, if present,
152 | contains 'emailProtection' OID. (Is it because S/MIME is/was the
153 | primary use case for PKCS7?)
154 | We do not want to fail the verification in this case. At present,
155 | M2Crypto lacks possibility of removing or modifying an existing
156 | extension. Let's assign a custom verification callback.
157 | """
158 | # The error we want to ignore is indicated by X509_V_ERR_INVALID_PURPOSE.
159 | err = ctx.get_error()
160 | if err != m2.X509_V_ERR_INVALID_PURPOSE:
161 | return ok
162 |
163 | # PKCS7_verify() has this requriement only for the signing certificate.
164 | # Do not modify the behavior for certificates upper in the chain.
165 | if ctx.get_error_depth() > 0:
166 | return ok
167 |
168 | # There is another cause of ERR_INVALID_PURPOSE: incompatible keyUsage.
169 | # Do not modify the default behavior in this case.
170 | cert = ctx.get_current_cert()
171 | try:
172 | key_usage = cert.get_ext('keyUsage').get_value()
173 | if 'digitalSignature' not in key_usage \
174 | and 'nonRepudiation' not in key_usage:
175 | return ok
176 | except LookupError:
177 | pass
178 |
179 | # Here, keyUsage is either absent, or contains the needed bit(s).
180 | # So ERR_INVALID_PURPOSE is caused by EKU not containing 'emailProtection'.
181 | # Ignore this error.
182 | return 1
183 |
184 |
185 | @requires_crypto
186 | def verify_signature_block(certificate_file, content, signature):
187 | """
188 | Verifies the 'signature' over the 'content', trusting the
189 | 'certificate'.
190 |
191 | :param certificate_file: the trusted certificate (PEM format)
192 | :type certificate_file: str
193 | :param content: The signature should match this content
194 | :type content: str
195 | :param signature: data (DER format) subject to check
196 | :type signature: str
197 | :return None if the signature validates.
198 | :exception SignatureBlockVerificationError
199 | """
200 |
201 | sig_bio = BIO.MemoryBuffer(signature)
202 | pkcs7 = SMIME.PKCS7(m2.pkcs7_read_bio_der(sig_bio._ptr()), 1)
203 | signers_cert_stack = pkcs7.get0_signers(X509.X509_Stack())
204 | trusted_cert_store = X509.X509_Store()
205 | trusted_cert_store.set_verify_cb(ignore_missing_email_protection_eku_cb)
206 | trusted_cert_store.load_info(certificate_file)
207 | smime = SMIME.SMIME()
208 | smime.set_x509_stack(signers_cert_stack)
209 | smime.set_x509_store(trusted_cert_store)
210 | data_bio = BIO.MemoryBuffer(content)
211 |
212 | try:
213 | smime.verify(pkcs7, data_bio)
214 | except SMIME.PKCS7_Error as message:
215 | raise SignatureBlockVerificationError(message)
216 | else:
217 | return None
218 |
219 |
220 | #
221 | # The end.
222 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/javatools/distinfo.py:
--------------------------------------------------------------------------------
1 | # This library is free software; you can redistribute it and/or modify
2 | # it under the terms of the GNU Lesser General Public License as
3 | # published by the Free Software Foundation; either version 3 of the
4 | # License, or (at your option) any later version.
5 | #
6 | # This library is distributed in the hope that it will be useful, but
7 | # WITHOUT ANY WARRANTY; without even the implied warranty of
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 | # Lesser General Public License for more details.
10 | #
11 | # You should have received a copy of the GNU Lesser General Public
12 | # License along with this library; if not, see
13 | # .
14 |
15 |
16 |
17 | """
18 | Utility script and module for discovering information about a
19 | distribution of mixed class files and JARs.
20 |
21 | :author: Christopher O'Brien
22 | :license: LGPL
23 | """
24 |
25 |
26 | from __future__ import print_function
27 |
28 |
29 | import sys
30 |
31 | from json import dump
32 | from argparse import ArgumentParser
33 | from os.path import isdir, join
34 | from shutil import rmtree
35 | from tempfile import mkdtemp
36 |
37 | from . import unpack_classfile
38 | from .jarinfo import JarInfo, JAR_PATTERNS, REQ_BY_CLASS, PROV_BY_CLASS
39 | from .dirutils import fnmatches
40 | from .ziputils import open_zip
41 |
42 |
43 | DIST_JAR = "jar"
44 | DIST_CLASS = "class"
45 |
46 | REQ_BY_JAR = "jar.requires"
47 | PROV_BY_JAR = "jar.provides"
48 |
49 |
50 | class DistInfo(object):
51 |
52 |
53 | def __init__(self, base_path):
54 | self.base_path = base_path
55 |
56 | # a pair of strings useful for later reporting. Non-mandatory
57 | self.product = None
58 | self.version = None
59 |
60 | # if the dist is a zip, we'll explode it into tmpdir
61 | self.tmpdir = None
62 |
63 | self._contents = None
64 | self._requires = None
65 | self._provides = None
66 |
67 |
68 | def __del__(self):
69 | self.close()
70 |
71 |
72 | def _working_path(self):
73 | if self.tmpdir:
74 | return self.tmpdir
75 |
76 | elif isdir(self.base_path):
77 | return self.base_path
78 |
79 | else:
80 | self.tmpdir = mkdtemp()
81 | with open_zip(self.base_path, "r") as zf:
82 | zf.extractall(path=self.tmpdir)
83 | return self.tmpdir
84 |
85 |
86 | def _collect_requires_provides(self):
87 | req = {}
88 | prov = {}
89 |
90 | p = set()
91 |
92 | for entry in self.get_jars():
93 | ji = self.get_jarinfo(entry)
94 | for sym, data in ji.get_requires().items():
95 | req.setdefault(sym, []).append((REQ_BY_JAR, entry, data))
96 | for sym, data in ji.get_provides().items():
97 | prov.setdefault(sym, []).append((PROV_BY_JAR, entry, data))
98 | p.add(sym)
99 | ji.close()
100 |
101 | for entry in self.get_classes():
102 | ci = self.get_classinfo(entry)
103 | for sym in ci.get_requires():
104 | req.setdefault(sym, []).append((REQ_BY_CLASS, entry))
105 | for sym in ci.get_provides(private=False):
106 | prov.setdefault(sym, []).append((PROV_BY_CLASS, entry))
107 | for sym in ci.get_provides(private=True):
108 | p.add(sym)
109 |
110 | req = dict((k, v) for k, v in req.items() if k not in p)
111 |
112 | self._requires = req
113 | self._provides = prov
114 |
115 |
116 | def get_requires(self, ignored=tuple()):
117 | """ a map of requirements to what requires it. ignored is an
118 | optional list of globbed patterns indicating packages,
119 | classes, etc that shouldn't be included in the provides map"""
120 |
121 | if self._requires is None:
122 | self._collect_requires_provides()
123 |
124 | d = self._requires
125 | if ignored:
126 | d = dict((k, v) for k, v in d.items()
127 | if not fnmatches(k, *ignored))
128 | return d
129 |
130 |
131 | def get_provides(self, ignored=tuple()):
132 | """ a map of provided classes and class members, and what
133 | provides them. ignored is an optional list of globbed patterns
134 | indicating packages, classes, etc that shouldn't be included
135 | in the provides map"""
136 |
137 | if self._provides is None:
138 | self._collect_requires_provides()
139 |
140 | d = self._provides
141 | if ignored:
142 | d = dict((k, v) for k, v in d.items()
143 | if not fnmatches(k, *ignored))
144 | return d
145 |
146 |
147 | def get_jars(self):
148 | """ sequence of entry names found in this distribution """
149 |
150 | for entry in self.get_contents():
151 | if fnmatches(entry, *JAR_PATTERNS):
152 | yield entry
153 |
154 |
155 | def get_jarinfo(self, entry):
156 | return JarInfo(join(self.base_path, entry))
157 |
158 |
159 | def get_classes(self):
160 | """ sequence of entry names found in the distribution. This
161 | is only the collection of class files directly in the dist, it
162 | does not include classes within JARs that are inthe dist."""
163 |
164 | for entry in self.get_contents():
165 | if fnmatches(entry, "*.class"):
166 | yield entry
167 |
168 |
169 | def get_classinfo(self, entry):
170 | return unpack_classfile(join(self.base_path, entry))
171 |
172 |
173 | def get_contents(self):
174 | if self._contents is None:
175 | self._contents = tuple(_collect_dist(self._working_path()))
176 | return self._contents
177 |
178 |
179 | def close(self):
180 | """ if this was a zip'd distribution, any introspection
181 | may have resulted in opening or creating temporary files.
182 | Call close in order to clean up. """
183 |
184 | if self.tmpdir:
185 | rmtree(self.tmpdir)
186 | self.tmpdir = None
187 |
188 | self._contents = None
189 |
190 |
191 | def _collect_dist(pathn):
192 | from os.path import join, relpath
193 | from os import walk
194 | for r, _ds, fs in walk(pathn):
195 | for f in fs:
196 | yield relpath(join(r, f), pathn)
197 |
198 |
199 | # --- CLI ---
200 | #
201 |
202 |
203 | def cli_dist_provides(options, info):
204 | print("distribution provides:")
205 |
206 | for provided in sorted(info.get_provides(options.api_ignore)):
207 | print(" ", provided)
208 | print()
209 |
210 |
211 | def cli_dist_requires(options, info):
212 | print("distribution requires:")
213 |
214 | for required in sorted(info.get_requires(options.api_ignore)):
215 | print(" ", required)
216 | print()
217 |
218 |
219 | def cli_distinfo(options, info):
220 |
221 | if options.dist_provides:
222 | cli_dist_provides(options, info)
223 |
224 | if options.dist_requires:
225 | cli_dist_requires(options, info)
226 |
227 | # TODO: simple things like listing JARs and non-JAR files
228 |
229 |
230 | def cli_distinfo_json(options, info):
231 | data = {}
232 |
233 | if options.dist_provides:
234 | data["dist.provides"] = info.get_provides(options.api_ignore)
235 |
236 | if options.dist_requires:
237 | data["dist.requires"] = info.get_requires(options.api_ignore)
238 |
239 | dump(data, sys.stdout, sort_keys=True, indent=2)
240 |
241 |
242 | def cli(options):
243 | # pylint: disable=W0613
244 | # parser unused
245 |
246 | pathn = options.dist
247 | info = DistInfo(pathn)
248 |
249 | if options.json:
250 | cli_distinfo_json(options, info)
251 | else:
252 | cli_distinfo(options, info)
253 |
254 | info.close()
255 | return 0
256 |
257 |
258 | def add_distinfo_optgroup(parser):
259 | g = parser.add_argument_group("Distribution Info Options")
260 |
261 | g.add_argument("--dist-provides", dest="dist_provides",
262 | action="store_true", default=False,
263 | help="API provides information at the distribution level")
264 |
265 | g.add_argument("--dist-requires", dest="dist_requires",
266 | action="store_true", default=False,
267 | help="API requires information at the distribution level")
268 |
269 |
270 | def create_optparser(progname):
271 | from .jarinfo import add_jarinfo_optgroup
272 | from .classinfo import add_classinfo_optgroup
273 |
274 | parser = ArgumentParser(prog=progname)
275 | parser.add_argument("dist", help="distribution to inspect")
276 | parser.add_argument("--json", dest="json", action="store_true",
277 | help="output in JSON mode")
278 |
279 | add_distinfo_optgroup(parser)
280 | add_jarinfo_optgroup(parser)
281 | add_classinfo_optgroup(parser)
282 |
283 | return parser
284 |
285 |
286 | def main(args=sys.argv):
287 | parser = create_optparser(args[0])
288 | return cli(parser.parse_args(args[1:]))
289 |
290 |
291 | #
292 | # The end.
293 |
--------------------------------------------------------------------------------