blob: cc9768021fa67d58b735b0b1bd04f7f57633a9ac [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.xml.secure;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Locale;
import java.util.Set;
import java.util.TreeSet;
import javax.xml.transform.Source;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.vafer.jdependency.Clazz;
import org.vafer.jdependency.Clazzpath;
/**
* Guards the shade footprint: the set of classes a consumer pulls in when they shade a single factory entry point.
*
* <p>Using {@code jdependency}, the same library {@code maven-shade-plugin}'s {@code minimizeJar} uses, this test computes each entry point's transitive class
* 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
* 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 on the
* DOM path their Xalan getAssociatedStylesheet and InputSource rewrites parse through, while the six public entry points together pull the whole
* library. Update the expected sets deliberately: a change here is a change to what a downstream shade includes.</p>
*
* <p>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
* nobody shades one), so the test is disabled there, just as it is excluded from the Android test compile.</p>
*/
@DisabledInNativeImage
class ShadingFootprintTest {
private static final String PKG = "org.apache.commons.xml.secure.";
// @formatter:off
private static final Set<String> DOCUMENT_BUILDER_FACTORY = set(
"FallbackIgnoreEntityResolver2",
"SecureDocumentBuilder",
"SecureDocumentBuilderFactory",
"SecureDocumentBuilderFactory$1",
"SecureDocumentBuilderFactory$Wrapper",
"SecureException",
"MethodHandleFactory",
"MethodHandleFactory$ThrowableCallable");
// @formatter:on
// @formatter:off
private static final Set<String> SAX_PARSER_FACTORY = set(
"FallbackIgnoreEntityResolver2",
"SecureException",
"SecureSAXParser",
"SecureSAXParserFactory",
"SecureSAXParserFactory$1",
"SecureSAXParserFactory$SecureExpatXMLReader",
"SecureSAXParserFactory$Wrapper",
"SecureXMLReader",
"MethodHandleFactory",
"MethodHandleFactory$ThrowableCallable");
// @formatter:on
// @formatter:off
private static final Set<String> XML_INPUT_FACTORY = set(
"FallbackIgnoreXMLResolver",
"SecureException",
"SecureXMLInputFactory",
"SecureXMLInputFactory$1",
"SecureXMLInputFactory$Wrapper",
"MethodHandleFactory",
"MethodHandleFactory$ThrowableCallable");
// @formatter:on
/**
* TrAX, XPath and schema re-harden their sub-parsers through {@link SecureSAXParserFactory#secure(Source, boolean)}, so each builds on the full SAX closure below;
* TrAX additionally parses the Xalan {@code getAssociatedStylesheet} source and XPath its InputSource-taking evaluate calls through the DOM entry point, so
* their closures carry that set too.
*/
// @formatter:off
private static final Set<String> TRANSFORMER_FACTORY = saxParserFactoryPlus(
"FallbackIgnoreEntityResolver2",
"FallbackIgnoreURIResolver",
"SecureDocumentBuilder",
"SecureDocumentBuilderFactory",
"SecureDocumentBuilderFactory$1",
"SecureDocumentBuilderFactory$Wrapper",
"SecureException",
"SecureSAXParser",
"SecureSAXParserFactory",
"SecureSAXParserFactory$1",
"SecureSAXParserFactory$SecureExpatXMLReader",
"SecureSAXParserFactory$Wrapper",
"SecureTemplates",
"SecureTemplatesHandler",
"SecureTransformer",
"SecureTransformerFactory",
"SecureTransformerFactory$1",
"SecureTransformerFactory$Wrapper",
"SecureTransformerHandler",
"SecureXMLFilter",
"SecureXMLReader",
"SaxonProvider",
"SaxonProvider$1",
"SaxonProvider$SecureConfiguration",
"SaxonProvider$SaxonProviderConfigurer");
// @formatter:on
// @formatter:off
private static final Set<String> XPATH_FACTORY = saxParserFactoryPlus(
"FallbackIgnoreEntityResolver2",
"FallbackIgnoreURIResolver",
"SecureDocumentBuilder",
"SecureDocumentBuilderFactory",
"SecureDocumentBuilderFactory$1",
"SecureDocumentBuilderFactory$Wrapper",
"MethodHandleFactory",
"MethodHandleFactory$ThrowableCallable",
"SecureException",
"SecureSAXParser",
"SecureSAXParserFactory",
"SecureSAXParserFactory$1",
"SecureSAXParserFactory$SecureExpatXMLReader",
"SecureSAXParserFactory$Wrapper",
"SecureXMLReader",
"SecureXPath",
"SecureXPathExpression",
"SecureXPathFactory",
"SecureXPathFactory$1",
"SecureXPathFactory$Wrapper",
"SaxonProvider",
"SaxonProvider$1",
"SaxonProvider$SecureConfiguration",
"SaxonProvider$SaxonProviderConfigurer");
// @formatter:on
// @formatter:off
private static final Set<String> SCHEMA_FACTORY = saxParserFactoryPlus(
"FallbackIgnoreEntityResolver2",
"FallbackIgnoreLSResourceResolver",
"SecureException",
"SecureSAXParser",
"SecureSAXParserFactory",
"SecureSAXParserFactory$1",
"SecureSAXParserFactory$SecureExpatXMLReader",
"SecureSAXParserFactory$Wrapper",
"SecureSchema",
"SecureSchemaFactory",
"SecureSchemaFactory$1",
"SecureSchemaFactory$Wrapper",
"SecureValidator",
"SecureValidatorHandler",
"SecureXMLReader");
// @formatter:on
/**
* Class count of the {@link #rootClosure()} DOM entry point, guarding that closure against drift.
*/
private static final int LIBRARY_CLASS_COUNT = 8;
/**
* Entry points reported by the {@link #reportFootprint()} diagnostic, most-focused first, ending with the whole library.
*/
private static final String[] REPORTED = {"SecureDocumentBuilderFactory", "SecureSAXParserFactory", "SecureXMLInputFactory",
"SecureTransformerFactory", "SecureXPathFactory", "SecureSchemaFactory"};
private static Clazzpath clazzpath;
private static Path classesDir;
/**
* Sums the uncompressed {@code .class} file sizes of a closure's classes, as they would land in a shaded jar.
*/
private static long bytesOf(final Set<String> simpleNames) {
long total = 0;
for (final String name : simpleNames) {
try {
total += Files.size(classesDir.resolve("org/apache/commons/xml/secure/" + name + ".class"));
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
}
return total;
}
/**
* Transitive class closure of {@code PKG + simpleName}, restricted to this library's own package and reported by simple name.
*/
private static Set<String> closureOf(final String simpleName) {
final Clazz entry = clazzpath.getClazz(PKG + simpleName);
if (entry == null) {
throw new IllegalStateException("Not on the compiled classpath: " + PKG + simpleName);
}
final Set<String> names = new TreeSet<>();
names.add(strip(entry.getName()));
for (final Clazz dependency : entry.getTransitiveDependencies()) {
if (dependency.getName().startsWith(PKG)) {
names.add(strip(dependency.getName()));
}
}
return names;
}
@BeforeAll
static void indexCompiledClasses() throws Exception {
classesDir = Paths.get(SecureException.class.getProtectionDomain().getCodeSource().getLocation().toURI());
clazzpath = new Clazzpath();
clazzpath.addClazzpathUnit(classesDir);
}
/**
* 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
* refactor.
*/
@AfterAll
static void reportFootprint() {
final Set<String> libraryClosure = new TreeSet<>();
for (final String entry : REPORTED) {
libraryClosure.addAll(closureOf(entry));
}
final long library = bytesOf(libraryClosure);
final StringBuilder report = new StringBuilder("\nShade footprint (uncompressed .class bytes, % of full library):\n");
for (final String entry : REPORTED) {
final Set<String> closure = closureOf(entry);
final long bytes = bytesOf(closure);
report.append(String.format(Locale.ROOT, " %-24s %2d classes %7d bytes %5.1f%%%n", entry, closure.size(), bytes, 100.0 * bytes / library));
}
if (Boolean.getBoolean(ShadingFootprintTest.class.getName() + ".reportFootprint")) {
System.out.print(report);
}
}
private static Set<String> rootClosure() {
return closureOf("SecureDocumentBuilderFactory");
}
/**
* {@link #SAX_PARSER_FACTORY} plus the extra names; used where an entry point's closure is the SAX path plus its own classes.
*/
private static Set<String> saxParserFactoryPlus(final String... more) {
final Set<String> union = new TreeSet<>(SAX_PARSER_FACTORY);
union.addAll(Arrays.asList(more));
return union;
}
private static Set<String> set(final String... names) {
return new TreeSet<>(Arrays.asList(names));
}
private static String strip(final String qualifiedName) {
return qualifiedName.substring(PKG.length());
}
@Test
void documentBuilderFactoryFootprint() {
assertEquals(DOCUMENT_BUILDER_FACTORY, closureOf("SecureDocumentBuilderFactory"));
}
@Test
void rootClosureMatchesDocumentBuilderFootprint() {
assertEquals(LIBRARY_CLASS_COUNT, rootClosure().size(), "SecureDocumentBuilderFactory closure size drifted");
}
@Test
void saxParserFactoryFootprint() {
assertEquals(SAX_PARSER_FACTORY, closureOf("SecureSAXParserFactory"));
}
@Test
void schemaFactoryFootprint() {
assertEquals(SCHEMA_FACTORY, closureOf("SecureSchemaFactory"));
}
@Test
void transformerFactoryFootprint() {
assertEquals(TRANSFORMER_FACTORY, closureOf("SecureTransformerFactory"));
}
@Test
void xmlInputFactoryFootprint() {
assertEquals(XML_INPUT_FACTORY, closureOf("SecureXMLInputFactory"));
}
@Test
void xPathFactoryFootprint() {
assertEquals(XPATH_FACTORY, closureOf("SecureXPathFactory"));
}
}