View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.xml.secure;
19  
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  
22  import java.io.IOException;
23  import java.io.UncheckedIOException;
24  import java.nio.file.Files;
25  import java.nio.file.Path;
26  import java.nio.file.Paths;
27  import java.util.Arrays;
28  import java.util.Locale;
29  import java.util.Set;
30  import java.util.TreeSet;
31  
32  import javax.xml.transform.Source;
33  
34  import org.junit.jupiter.api.AfterAll;
35  import org.junit.jupiter.api.BeforeAll;
36  import org.junit.jupiter.api.Test;
37  import org.junit.jupiter.api.condition.DisabledInNativeImage;
38  import org.vafer.jdependency.Clazz;
39  import org.vafer.jdependency.Clazzpath;
40  
41  /**
42   * Guards the shade footprint: the set of classes a consumer pulls in when they shade a single factory entry point.
43   *
44   * <p>
45   * Using {@code jdependency}, the same library {@code maven-shade-plugin}'s {@code minimizeJar} uses, this test computes each entry point's transitive class
46   * closure over the compiled {@code target/classes} and pins it to an expected set. It keeps each entry point from silently regaining a dependency on classes it
47   * should not need (for example, a sibling resolver floor or another factory class), so schema builds only on the shared SAX path. TrAX and XPath additionally
48   * build on the DOM path used by their Xalan getAssociatedStylesheet and InputSource rewrites, while the six public entry points together pull in the whole
49   * library. Update the expected sets deliberately: a change here is a change to what a downstream shade includes.
50   * </p>
51   *
52   * <p>
53   * The test reads the compiled {@code .class} files from the code-source location, which only exists on a regular JVM: a native image carries no bytecode (and
54   * nobody shades one), so the test is disabled there, just as it is excluded from the Android test compile.
55   * </p>
56   */
57  @DisabledInNativeImage
58  class ShadingFootprintTest {
59  
60      private static final String PKG = "org.apache.commons.xml.secure.";
61  
62      // @formatter:off
63      private static final Set<String> DOCUMENT_BUILDER_FACTORY = set(
64              "FallbackIgnoreEntityResolver2",
65              "SecureDocumentBuilder",
66              "SecureDocumentBuilderFactory",
67              "SecureDocumentBuilderFactory$1",
68              "SecureDocumentBuilderFactory$Wrapper",
69              "SecureException",
70              "MethodHandleFactory",
71              "MethodHandleFactory$ThrowableCallable");
72      // @formatter:on
73  
74      // @formatter:off
75      private static final Set<String> SAX_PARSER_FACTORY = set(
76              "FallbackIgnoreEntityResolver2",
77              "SecureException",
78              "SecureSAXParser",
79              "SecureSAXParserFactory",
80              "SecureSAXParserFactory$1",
81              "SecureSAXParserFactory$SecureExpatXMLReader",
82              "SecureSAXParserFactory$Wrapper",
83              "SecureXMLReader",
84              "MethodHandleFactory",
85              "MethodHandleFactory$ThrowableCallable");
86      // @formatter:on
87  
88      // @formatter:off
89      private static final Set<String> XML_INPUT_FACTORY = set(
90              "FallbackIgnoreXMLResolver",
91              "SecureException",
92              "SecureXMLInputFactory",
93              "SecureXMLInputFactory$1",
94              "SecureXMLInputFactory$Wrapper",
95              "MethodHandleFactory",
96              "MethodHandleFactory$ThrowableCallable");
97      // @formatter:on
98  
99      /**
100      * TrAX, XPath and schema re-harden their sub-parsers through {@link SecureSAXParserFactory#secure(Source, boolean)}, so each builds on the full SAX closure
101      * below; TrAX additionally parses the Xalan {@code getAssociatedStylesheet} source and XPath its InputSource-taking evaluate calls through the DOM entry
102      * point, so their closures carry that set too.
103      */
104     // @formatter:off
105     private static final Set<String> TRANSFORMER_FACTORY = saxParserFactoryPlus(
106             "FallbackIgnoreEntityResolver2",
107             "FallbackIgnoreURIResolver",
108             "SecureDocumentBuilder",
109             "SecureDocumentBuilderFactory",
110             "SecureDocumentBuilderFactory$1",
111             "SecureDocumentBuilderFactory$Wrapper",
112             "SecureException",
113             "SecureSAXParser",
114             "SecureSAXParserFactory",
115             "SecureSAXParserFactory$1",
116             "SecureSAXParserFactory$SecureExpatXMLReader",
117             "SecureSAXParserFactory$Wrapper",
118             "SecureTemplates",
119             "SecureTemplatesHandler",
120             "SecureTransformer",
121             "SecureTransformerFactory",
122             "SecureTransformerFactory$1",
123             "SecureTransformerFactory$Wrapper",
124             "SecureTransformerHandler",
125             "SecureXMLFilter",
126             "SecureXMLReader",
127             "SaxonProvider",
128             "SaxonProvider$1",
129             "SaxonProvider$SecureConfiguration",
130             "SaxonProvider$SaxonProviderConfigurer");
131     // @formatter:on
132 
133     // @formatter:off
134     private static final Set<String> XPATH_FACTORY = saxParserFactoryPlus(
135             "FallbackIgnoreEntityResolver2",
136             "FallbackIgnoreURIResolver",
137             "SecureDocumentBuilder",
138             "SecureDocumentBuilderFactory",
139             "SecureDocumentBuilderFactory$1",
140             "SecureDocumentBuilderFactory$Wrapper",
141             "MethodHandleFactory",
142             "MethodHandleFactory$ThrowableCallable",
143             "SecureException",
144             "SecureSAXParser",
145             "SecureSAXParserFactory",
146             "SecureSAXParserFactory$1",
147             "SecureSAXParserFactory$SecureExpatXMLReader",
148             "SecureSAXParserFactory$Wrapper",
149             "SecureXMLReader",
150             "SecureXPath",
151             "SecureXPathExpression",
152             "SecureXPathFactory",
153             "SecureXPathFactory$1",
154             "SecureXPathFactory$Wrapper",
155             "SaxonProvider",
156             "SaxonProvider$1",
157             "SaxonProvider$SecureConfiguration",
158             "SaxonProvider$SaxonProviderConfigurer");
159     // @formatter:on
160 
161     // @formatter:off
162     private static final Set<String> SCHEMA_FACTORY = saxParserFactoryPlus(
163             "FallbackIgnoreEntityResolver2",
164             "FallbackIgnoreLSResourceResolver",
165             "SecureException",
166             "SecureSAXParser",
167             "SecureSAXParserFactory",
168             "SecureSAXParserFactory$1",
169             "SecureSAXParserFactory$SecureExpatXMLReader",
170             "SecureSAXParserFactory$Wrapper",
171             "SecureSchema",
172             "SecureSchemaFactory",
173             "SecureSchemaFactory$1",
174             "SecureSchemaFactory$Wrapper",
175             "SecureValidator",
176             "SecureValidatorHandler",
177             "SecureXMLReader");
178     // @formatter:on
179 
180     /**
181      * Class count of the {@link #rootClosure()} DOM entry point, guarding that closure against drift.
182      */
183     private static final int LIBRARY_CLASS_COUNT = 8;
184 
185     /**
186      * Entry points reported by the {@link #reportFootprint()} diagnostic, most focused first, ending with the whole library.
187      */
188     private static final String[] REPORTED = {"SecureDocumentBuilderFactory", "SecureSAXParserFactory", "SecureXMLInputFactory",
189             "SecureTransformerFactory", "SecureXPathFactory", "SecureSchemaFactory"};
190 
191     private static Clazzpath clazzpath;
192     private static Path classesDir;
193 
194     /**
195      * Sums the uncompressed {@code .class} file sizes of a closure's classes, as they would land in a shaded jar.
196      */
197     private static long bytesOf(final Set<String> simpleNames) {
198         long total = 0;
199         for (final String name : simpleNames) {
200             try {
201                 total += Files.size(classesDir.resolve("org/apache/commons/xml/secure/" + name + ".class"));
202             } catch (final IOException e) {
203                 throw new UncheckedIOException(e);
204             }
205         }
206         return total;
207     }
208 
209     /**
210      * Transitive class closure of {@code PKG + simpleName}, restricted to this library's own package and reported by simple name.
211      */
212     private static Set<String> closureOf(final String simpleName) {
213         final Clazz entry = clazzpath.getClazz(PKG + simpleName);
214         if (entry == null) {
215             throw new IllegalStateException("Not on the compiled classpath: " + PKG + simpleName);
216         }
217         final Set<String> names = new TreeSet<>();
218         names.add(strip(entry.getName()));
219         for (final Clazz dependency : entry.getTransitiveDependencies()) {
220             if (dependency.getName().startsWith(PKG)) {
221                 names.add(strip(dependency.getName()));
222             }
223         }
224         return names;
225     }
226 
227     @BeforeAll
228     static void indexCompiledClasses() throws Exception {
229         classesDir = Paths.get(SecureException.class.getProtectionDomain().getCodeSource().getLocation().toURI());
230         clazzpath = new Clazzpath();
231         clazzpath.addClazzpathUnit(classesDir);
232     }
233 
234     /**
235      * Prints each entry point's shade closure size (uncompressed {@code .class} bytes) and its share of the full library, to track the footprint over the
236      * refactor.
237      */
238     @AfterAll
239     static void reportFootprint() {
240         final Set<String> libraryClosure = new TreeSet<>();
241         for (final String entry : REPORTED) {
242             libraryClosure.addAll(closureOf(entry));
243         }
244         final long library = bytesOf(libraryClosure);
245         final StringBuilder report = new StringBuilder("\nShade footprint (uncompressed .class bytes, % of full library):\n");
246         for (final String entry : REPORTED) {
247             final Set<String> closure = closureOf(entry);
248             final long bytes = bytesOf(closure);
249             report.append(String.format(Locale.ROOT, "  %-24s %2d classes  %7d bytes  %5.1f%%%n", entry, closure.size(), bytes, 100.0 * bytes / library));
250         }
251         if (Boolean.getBoolean(ShadingFootprintTest.class.getName() + ".reportFootprint")) {
252             System.out.print(report);
253         }
254     }
255 
256     private static Set<String> rootClosure() {
257         return closureOf("SecureDocumentBuilderFactory");
258     }
259 
260     /**
261      * {@link #SAX_PARSER_FACTORY} plus the extra names; used where an entry point's closure is the SAX path plus its own classes.
262      */
263     private static Set<String> saxParserFactoryPlus(final String... more) {
264         final Set<String> union = new TreeSet<>(SAX_PARSER_FACTORY);
265         union.addAll(Arrays.asList(more));
266         return union;
267     }
268 
269     private static Set<String> set(final String... names) {
270         return new TreeSet<>(Arrays.asList(names));
271     }
272 
273     private static String strip(final String qualifiedName) {
274         return qualifiedName.substring(PKG.length());
275     }
276 
277     @Test
278     void documentBuilderFactoryFootprint() {
279         assertEquals(DOCUMENT_BUILDER_FACTORY, closureOf("SecureDocumentBuilderFactory"));
280     }
281 
282     @Test
283     void rootClosureMatchesDocumentBuilderFootprint() {
284         assertEquals(LIBRARY_CLASS_COUNT, rootClosure().size(), "SecureDocumentBuilderFactory closure size drifted");
285     }
286 
287     @Test
288     void saxParserFactoryFootprint() {
289         assertEquals(SAX_PARSER_FACTORY, closureOf("SecureSAXParserFactory"));
290     }
291 
292     @Test
293     void schemaFactoryFootprint() {
294         assertEquals(SCHEMA_FACTORY, closureOf("SecureSchemaFactory"));
295     }
296 
297     @Test
298     void transformerFactoryFootprint() {
299         assertEquals(TRANSFORMER_FACTORY, closureOf("SecureTransformerFactory"));
300     }
301 
302     @Test
303     void xmlInputFactoryFootprint() {
304         assertEquals(XML_INPUT_FACTORY, closureOf("SecureXMLInputFactory"));
305     }
306 
307     @Test
308     void xPathFactoryFootprint() {
309         assertEquals(XPATH_FACTORY, closureOf("SecureXPathFactory"));
310     }
311 }