8304846: Provide a shared utility to dump generated classes defined via Lookup API

Reviewed-by: jvernee
This commit is contained in:
Mandy Chung 2023-04-04 18:07:02 +00:00
parent 2ee4245105
commit dd59471798
11 changed files with 440 additions and 393 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2017, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -25,7 +25,6 @@
package java.lang.invoke;
import jdk.internal.access.SharedSecrets;
import jdk.internal.loader.BootLoader;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.FieldVisitor;
@ -36,9 +35,6 @@ import sun.invoke.util.BytecodeName;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -570,22 +566,9 @@ abstract class ClassSpecializer<T,K,S extends ClassSpecializer<T,K,S>.SpeciesDat
@SuppressWarnings("removal")
Class<? extends T> generateConcreteSpeciesCode(String className, ClassSpecializer<T,K,S>.SpeciesData speciesData) {
byte[] classFile = generateConcreteSpeciesCodeFile(className, speciesData);
// load class
InvokerBytecodeGenerator.maybeDump(classBCName(className), classFile);
ClassLoader cl = topClass.getClassLoader();
ProtectionDomain pd = null;
if (cl != null) {
pd = AccessController.doPrivileged(
new PrivilegedAction<>() {
@Override
public ProtectionDomain run() {
return topClass().getProtectionDomain();
}
});
}
Class<?> speciesCode = SharedSecrets.getJavaLangAccess()
.defineClass(cl, className, classFile, pd, "_ClassSpecializer_generateConcreteSpeciesCode");
var lookup = new MethodHandles.Lookup(topClass);
Class<?> speciesCode = lookup.makeClassDefiner(classBCName(className), classFile, dumper())
.defineClass(false);
return speciesCode.asSubclass(topClass());
}

View File

@ -27,21 +27,16 @@ package java.lang.invoke;
import jdk.internal.misc.CDS;
import jdk.internal.org.objectweb.asm.*;
import jdk.internal.util.ClassFileDumper;
import sun.invoke.util.BytecodeDescriptor;
import sun.invoke.util.VerifyAccess;
import sun.security.action.GetPropertyAction;
import sun.security.action.GetBooleanAction;
import java.io.FilePermission;
import java.io.Serializable;
import java.lang.constant.ConstantDescs;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.nio.file.Path;
import java.util.LinkedHashSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.PropertyPermission;
import java.util.Set;
import static java.lang.invoke.MethodHandleStatics.CLASSFILE_VERSION;
@ -85,11 +80,8 @@ import static jdk.internal.org.objectweb.asm.Opcodes.*;
private static final String[] EMPTY_STRING_ARRAY = new String[0];
// Used to ensure that dumped class files for failed definitions have a unique class name
private static final AtomicInteger counter = new AtomicInteger();
// For dumping generated classes to disk, for debugging purposes
private static final ProxyClassesDumper dumper;
private static final ClassFileDumper lambdaProxyClassFileDumper;
private static final boolean disableEagerInitialization;
@ -97,9 +89,11 @@ import static jdk.internal.org.objectweb.asm.Opcodes.*;
private static final ConstantDynamic implMethodCondy;
static {
final String dumpProxyClassesKey = "jdk.internal.lambda.dumpProxyClasses";
String dumpPath = GetPropertyAction.privilegedGetProperty(dumpProxyClassesKey);
dumper = (null == dumpPath) ? null : ProxyClassesDumper.getInstance(dumpPath);
// To dump the lambda proxy classes, set this system property:
// -Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles
// or -Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles=true
final String dumpProxyClassesKey = "jdk.invoke.LambdaMetafactory.dumpProxyClassFiles";
lambdaProxyClassFileDumper = ClassFileDumper.getInstance(dumpProxyClassesKey, Path.of("DUMP_LAMBDA_PROXY_CLASS_FILES"));
final String disableEagerInitializationKey = "jdk.internal.lambda.disableEagerInitialization";
disableEagerInitialization = GetBooleanAction.privilegedGetProperty(disableEagerInitializationKey);
@ -363,51 +357,15 @@ import static jdk.internal.org.objectweb.asm.Opcodes.*;
final byte[] classBytes = cw.toByteArray();
try {
// this class is linked at the indy callsite; so define a hidden nestmate
Lookup lookup = null;
try {
if (useImplMethodHandle) {
lookup = caller.defineHiddenClassWithClassData(classBytes, implementation, !disableEagerInitialization,
NESTMATE, STRONG);
} else {
lookup = caller.defineHiddenClass(classBytes, !disableEagerInitialization, NESTMATE, STRONG);
}
return lookup.lookupClass();
} finally {
// If requested, dump out to a file for debugging purposes
if (dumper != null) {
String name;
if (lookup != null) {
String definedName = lookup.lookupClass().getName();
int suffixIdx = definedName.lastIndexOf('/');
assert suffixIdx != -1;
name = lambdaClassName + '.' + definedName.substring(suffixIdx + 1);
} else {
name = lambdaClassName + ".failed-" + counter.incrementAndGet();
}
doDump(name, classBytes);
}
}
} catch (IllegalAccessException e) {
throw new LambdaConversionException("Exception defining lambda proxy class", e);
var classdata = useImplMethodHandle? implementation : null;
return caller.makeHiddenClassDefiner(lambdaClassName, classBytes, Set.of(NESTMATE, STRONG), lambdaProxyClassFileDumper)
.defineClass(!disableEagerInitialization, classdata);
} catch (Throwable t) {
throw new InternalError(t);
}
}
@SuppressWarnings("removal")
private void doDump(final String className, final byte[] classBytes) {
AccessController.doPrivileged(new PrivilegedAction<>() {
@Override
public Void run() {
dumper.dumpClass(className, classBytes);
return null;
}
}, null,
new FilePermission("<<ALL FILES>>", "read, write"),
// createDirectories may need it
new PropertyPermission("user.dir", "read"));
}
/**
* Generate a static field and a static initializer that sets this field to an instance of the lambda
*/

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2012, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -34,11 +34,7 @@ import jdk.internal.org.objectweb.asm.Type;
import sun.invoke.util.VerifyAccess;
import sun.invoke.util.VerifyType;
import sun.invoke.util.Wrapper;
import sun.reflect.misc.ReflectUtil;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
@ -125,7 +121,7 @@ class InvokerBytecodeGenerator {
name = invokerName.substring(0, p);
invokerName = invokerName.substring(p + 1);
}
if (DUMP_CLASS_FILES) {
if (dumper().isEnabled()) {
name = makeDumpableClassName(name);
}
this.name = name;
@ -173,58 +169,8 @@ class InvokerBytecodeGenerator {
}
/** instance counters for dumped classes */
private static final HashMap<String,Integer> DUMP_CLASS_FILES_COUNTERS;
/** debugging flag for saving generated class files */
private static final File DUMP_CLASS_FILES_DIR;
static {
if (DUMP_CLASS_FILES) {
DUMP_CLASS_FILES_COUNTERS = new HashMap<>();
try {
File dumpDir = new File("DUMP_CLASS_FILES");
if (!dumpDir.exists()) {
dumpDir.mkdirs();
}
DUMP_CLASS_FILES_DIR = dumpDir;
System.out.println("Dumping class files to "+DUMP_CLASS_FILES_DIR+"/...");
} catch (Exception e) {
throw newInternalError(e);
}
} else {
DUMP_CLASS_FILES_COUNTERS = null;
DUMP_CLASS_FILES_DIR = null;
}
}
private void maybeDump(final byte[] classFile) {
if (DUMP_CLASS_FILES) {
maybeDump(className, classFile);
}
}
// Also used from BoundMethodHandle
@SuppressWarnings("removal")
static void maybeDump(final String className, final byte[] classFile) {
if (DUMP_CLASS_FILES) {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<>() {
public Void run() {
try {
String dumpName = className.replace('.','/');
File dumpFile = new File(DUMP_CLASS_FILES_DIR, dumpName+".class");
System.out.println("dump: " + dumpFile);
dumpFile.getParentFile().mkdirs();
FileOutputStream file = new FileOutputStream(dumpFile);
file.write(classFile);
file.close();
return null;
} catch (IOException ex) {
throw newInternalError(ex);
}
}
});
}
}
private static final HashMap<String,Integer> DUMP_CLASS_FILES_COUNTERS =
dumper().isEnabled() ? new HashMap<>(): null;
private static String makeDumpableClassName(String className) {
Integer ctr;
@ -271,7 +217,7 @@ class InvokerBytecodeGenerator {
// unique static variable name
String name;
if (DUMP_CLASS_FILES) {
if (dumper().isEnabled()) {
Class<?> c = arg.getClass();
while (c.isArray()) {
c = c.getComponentType();
@ -299,7 +245,7 @@ class InvokerBytecodeGenerator {
* Extract the MemberName of a newly-defined method.
*/
private MemberName loadMethod(byte[] classFile) {
Class<?> invokerClass = LOOKUP.makeHiddenClassDefiner(className, classFile, Set.of())
Class<?> invokerClass = LOOKUP.makeHiddenClassDefiner(className, classFile, Set.of(), dumper())
.defineClass(true, classDataValues());
return resolveInvokerMember(invokerClass, invokerName, invokerType);
}
@ -809,9 +755,7 @@ class InvokerBytecodeGenerator {
clinit(cw, className, classData);
bogusMethod(lambdaForm);
final byte[] classFile = toByteArray();
maybeDump(classFile);
return classFile;
return toByteArray();
}
void setClassWriter(ClassWriter cw) {
@ -1898,9 +1842,7 @@ class InvokerBytecodeGenerator {
clinit(cw, className, classData);
bogusMethod(invokerType);
final byte[] classFile = cw.toByteArray();
maybeDump(classFile);
return classFile;
return cw.toByteArray();
}
/**
@ -1967,9 +1909,7 @@ class InvokerBytecodeGenerator {
clinit(cw, className, classData);
bogusMethod(dstType);
final byte[] classFile = cw.toByteArray();
maybeDump(classFile);
return classFile;
return cw.toByteArray();
}
/**
@ -1977,7 +1917,7 @@ class InvokerBytecodeGenerator {
* for debugging purposes.
*/
private void bogusMethod(Object os) {
if (DUMP_CLASS_FILES) {
if (dumper().isEnabled()) {
mv = cw.visitMethod(Opcodes.ACC_STATIC, "dummy", "()V", null, null);
mv.visitLdcInsn(os.toString());
mv.visitInsn(Opcodes.POP);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2008, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2008, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -1101,8 +1101,9 @@ abstract class MethodHandleImpl {
// use the original class name
name = name.replace('/', '_');
}
name = name.replace('.', '/');
Class<?> invokerClass = new Lookup(targetClass)
.makeHiddenClassDefiner(name, INJECTED_INVOKER_TEMPLATE, Set.of(NESTMATE))
.makeHiddenClassDefiner(name, INJECTED_INVOKER_TEMPLATE, Set.of(NESTMATE), dumper())
.defineClass(true, targetClass);
assert checkInjectedInvoker(targetClass, invokerClass);
return invokerClass;
@ -1655,12 +1656,6 @@ abstract class MethodHandleImpl {
return BindCaller.reflectiveInvoker(caller);
}
@Override
public Lookup defineHiddenClassWithClassData(Lookup caller, String name, byte[] bytes, Object classData, boolean initialize) {
// skip name and access flags validation
return caller.makeHiddenClassDefiner(name, bytes, Set.of()).defineClassAsLookup(initialize, classData);
}
@Override
public Class<?>[] exceptionTypes(MethodHandle handle) {
return VarHandles.exceptionTypes(handle);

View File

@ -27,9 +27,11 @@ package java.lang.invoke;
import jdk.internal.misc.CDS;
import jdk.internal.misc.Unsafe;
import jdk.internal.util.ClassFileDumper;
import sun.security.action.GetPropertyAction;
import java.lang.reflect.ClassFileFormatVersion;
import java.nio.file.Path;
import java.util.Properties;
import static java.lang.invoke.LambdaForm.basicTypeSignature;
@ -49,7 +51,6 @@ class MethodHandleStatics {
static final Unsafe UNSAFE = Unsafe.getUnsafe();
static final int CLASSFILE_VERSION = ClassFileFormatVersion.latest().major();
static final boolean DEBUG_METHOD_HANDLE_NAMES;
static final boolean DUMP_CLASS_FILES;
static final boolean TRACE_INTERPRETER;
static final boolean TRACE_METHOD_LINKAGE;
static final boolean TRACE_RESOLVE;
@ -62,13 +63,13 @@ class MethodHandleStatics {
static final boolean VAR_HANDLE_GUARDS;
static final int MAX_ARITY;
static final boolean VAR_HANDLE_IDENTITY_ADAPT;
static final ClassFileDumper DUMP_CLASS_FILES;
static {
Properties props = GetPropertyAction.privilegedGetProperties();
DEBUG_METHOD_HANDLE_NAMES = Boolean.parseBoolean(
props.getProperty("java.lang.invoke.MethodHandle.DEBUG_NAMES"));
DUMP_CLASS_FILES = Boolean.parseBoolean(
props.getProperty("java.lang.invoke.MethodHandle.DUMP_CLASS_FILES"));
TRACE_INTERPRETER = Boolean.parseBoolean(
props.getProperty("java.lang.invoke.MethodHandle.TRACE_INTERPRETER"));
TRACE_METHOD_LINKAGE = Boolean.parseBoolean(
@ -96,6 +97,9 @@ class MethodHandleStatics {
MAX_ARITY = Integer.parseInt(
props.getProperty("java.lang.invoke.MethodHandleImpl.MAX_ARITY", "255"));
DUMP_CLASS_FILES = ClassFileDumper.getInstance("jdk.invoke.MethodHandle.dumpMethodHandleInternals",
Path.of("DUMP_METHOD_HANDLE_INTERNALS"));
if (CUSTOMIZE_THRESHOLD < -1 || CUSTOMIZE_THRESHOLD > 127) {
throw newInternalError("CUSTOMIZE_THRESHOLD should be in [-1...127] range");
}
@ -107,12 +111,16 @@ class MethodHandleStatics {
/*non-public*/
static boolean debugEnabled() {
return (DEBUG_METHOD_HANDLE_NAMES |
DUMP_CLASS_FILES |
DUMP_CLASS_FILES.isEnabled() |
TRACE_INTERPRETER |
TRACE_METHOD_LINKAGE |
LOG_LF_COMPILATION_FAILURE);
}
static ClassFileDumper dumper() {
return DUMP_CLASS_FILES;
}
/**
* If requested, logs the result of resolving the LambdaForm to stdout
* and informs the CDS subsystem about it.

View File

@ -36,6 +36,7 @@ import jdk.internal.org.objectweb.asm.Type;
import jdk.internal.reflect.CallerSensitive;
import jdk.internal.reflect.CallerSensitiveAdapter;
import jdk.internal.reflect.Reflection;
import jdk.internal.util.ClassFileDumper;
import jdk.internal.vm.annotation.ForceInline;
import sun.invoke.util.ValueConversions;
import sun.invoke.util.VerifyAccess;
@ -55,6 +56,7 @@ import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.ByteOrder;
import java.nio.file.Path;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
@ -2237,8 +2239,23 @@ public class MethodHandles {
.defineClassAsLookup(initialize, classData);
}
// A default dumper for writing class files passed to Lookup::defineClass
// and Lookup::defineHiddenClass to disk for debugging purposes. To enable,
// set -Djdk.invoke.MethodHandle.dumpHiddenClassFiles or
// -Djdk.invoke.MethodHandle.dumpHiddenClassFiles=true
//
// This default dumper does not dump hidden classes defined by LambdaMetafactory
// and LambdaForms and method handle internals. They are dumped via
// different ClassFileDumpers.
private static ClassFileDumper defaultDumper() {
return DEFAULT_DUMPER;
}
private static final ClassFileDumper DEFAULT_DUMPER = ClassFileDumper.getInstance(
"jdk.invoke.MethodHandle.dumpClassFiles", Path.of("DUMP_CLASS_FILES"));
static class ClassFile {
final String name;
final String name; // internal name
final int accessFlags;
final byte[] bytes;
ClassFile(String name, int accessFlags, byte[] bytes) {
@ -2260,6 +2277,18 @@ public class MethodHandles {
* or the class is not in the given package name.
*/
static ClassFile newInstance(byte[] bytes, String pkgName) {
var cf = readClassFile(bytes);
// check if it's in the named package
int index = cf.name.lastIndexOf('/');
String pn = (index == -1) ? "" : cf.name.substring(0, index).replace('/', '.');
if (!pn.equals(pkgName)) {
throw newIllegalArgumentException(cf.name + " not in same package as lookup class");
}
return cf;
}
private static ClassFile readClassFile(byte[] bytes) {
int magic = readInt(bytes, 0);
if (magic != 0xCAFEBABE) {
throw new ClassFormatError("Incompatible magic value: " + magic);
@ -2274,7 +2303,7 @@ public class MethodHandles {
int accessFlags;
try {
ClassReader reader = new ClassReader(bytes);
// ClassReader::getClassName does not check if `this_class` is CONSTANT_Class_info
// ClassReader does not check if `this_class` is CONSTANT_Class_info
// workaround to read `this_class` using readConst and validate the value
int thisClass = reader.readUnsignedShort(reader.header + 2);
Object constant = reader.readConst(thisClass, new char[reader.getMaxStringLength()]);
@ -2284,7 +2313,7 @@ public class MethodHandles {
if (!type.getDescriptor().startsWith("L")) {
throw new ClassFormatError("this_class item: #" + thisClass + " not a CONSTANT_Class_info");
}
name = type.getClassName();
name = type.getInternalName();
accessFlags = reader.readUnsignedShort(reader.header);
} catch (RuntimeException e) {
// ASM exceptions are poorly specified
@ -2292,19 +2321,10 @@ public class MethodHandles {
cfe.initCause(e);
throw cfe;
}
// must be a class or interface
if ((accessFlags & Opcodes.ACC_MODULE) != 0) {
throw newIllegalArgumentException("Not a class or interface: ACC_MODULE flag is set");
}
// check if it's in the named package
int index = name.lastIndexOf('.');
String pn = (index == -1) ? "" : name.substring(0, index);
if (!pn.equals(pkgName)) {
throw newIllegalArgumentException(name + " not in same package as lookup class");
}
return new ClassFile(name, accessFlags, bytes);
}
@ -2338,7 +2358,22 @@ public class MethodHandles {
*/
private ClassDefiner makeClassDefiner(byte[] bytes) {
ClassFile cf = ClassFile.newInstance(bytes, lookupClass().getPackageName());
return new ClassDefiner(this, cf, STRONG_LOADER_LINK);
return new ClassDefiner(this, cf, STRONG_LOADER_LINK, defaultDumper());
}
/**
* Returns a ClassDefiner that creates a {@code Class} object of a normal class
* from the given bytes. No package name check on the given bytes.
*
* @param name internal name
* @param bytes class bytes
* @param dumper dumper to write the given bytes to the dumper's output directory
* @return ClassDefiner that defines a normal class of the given bytes.
*/
ClassDefiner makeClassDefiner(String name, byte[] bytes, ClassFileDumper dumper) {
// skip package name validation
ClassFile cf = ClassFile.newInstanceNoCheck(name, bytes);
return new ClassDefiner(this, cf, STRONG_LOADER_LINK, dumper);
}
/**
@ -2349,14 +2384,15 @@ public class MethodHandles {
* before calling this factory method.
*
* @param bytes class bytes
* @param dumper dumper to write the given bytes to the dumper's output directory
* @return ClassDefiner that defines a hidden class of the given bytes.
*
* @throws IllegalArgumentException if {@code bytes} is not a class or interface or
* {@code bytes} denotes a class in a different package than the lookup class
*/
ClassDefiner makeHiddenClassDefiner(byte[] bytes) {
ClassDefiner makeHiddenClassDefiner(byte[] bytes, ClassFileDumper dumper) {
ClassFile cf = ClassFile.newInstance(bytes, lookupClass().getPackageName());
return makeHiddenClassDefiner(cf, Set.of(), false);
return makeHiddenClassDefiner(cf, Set.of(), false, dumper);
}
/**
@ -2375,25 +2411,27 @@ public class MethodHandles {
* @throws IllegalArgumentException if {@code bytes} is not a class or interface or
* {@code bytes} denotes a class in a different package than the lookup class
*/
ClassDefiner makeHiddenClassDefiner(byte[] bytes,
Set<ClassOption> options,
boolean accessVmAnnotations) {
private ClassDefiner makeHiddenClassDefiner(byte[] bytes,
Set<ClassOption> options,
boolean accessVmAnnotations) {
ClassFile cf = ClassFile.newInstance(bytes, lookupClass().getPackageName());
return makeHiddenClassDefiner(cf, options, accessVmAnnotations);
return makeHiddenClassDefiner(cf, options, accessVmAnnotations, defaultDumper());
}
/**
* Returns a ClassDefiner that creates a {@code Class} object of a hidden class
* from the given bytes and the given options. No package name check on the given name.
* from the given bytes and the given options. No package name check on the given bytes.
*
* @param name fully-qualified name that specifies the prefix of the hidden class
* @param name internal name that specifies the prefix of the hidden class
* @param bytes class bytes
* @param options class options
* @param dumper dumper to write the given bytes to the dumper's output directory
* @return ClassDefiner that defines a hidden class of the given bytes and options.
*/
ClassDefiner makeHiddenClassDefiner(String name, byte[] bytes, Set<ClassOption> options) {
ClassDefiner makeHiddenClassDefiner(String name, byte[] bytes, Set<ClassOption> options, ClassFileDumper dumper) {
Objects.requireNonNull(dumper);
// skip name and access flags validation
return makeHiddenClassDefiner(ClassFile.newInstanceNoCheck(name, bytes), options, false);
return makeHiddenClassDefiner(ClassFile.newInstanceNoCheck(name, bytes), options, false, dumper);
}
/**
@ -2403,10 +2441,12 @@ public class MethodHandles {
* @param cf ClassFile
* @param options class options
* @param accessVmAnnotations true to give the hidden class access to VM annotations
* @param dumper dumper to write the given bytes to the dumper's output directory
*/
private ClassDefiner makeHiddenClassDefiner(ClassFile cf,
Set<ClassOption> options,
boolean accessVmAnnotations) {
boolean accessVmAnnotations,
ClassFileDumper dumper) {
int flags = HIDDEN_CLASS | ClassOption.optionsToFlag(options);
if (accessVmAnnotations | VM.isSystemDomainLoader(lookupClass.getClassLoader())) {
// jdk.internal.vm.annotations are permitted for classes
@ -2414,24 +2454,26 @@ public class MethodHandles {
flags |= ACCESS_VM_ANNOTATIONS;
}
return new ClassDefiner(this, cf, flags);
return new ClassDefiner(this, cf, flags, dumper);
}
static class ClassDefiner {
private final Lookup lookup;
private final String name;
private final String name; // internal name
private final byte[] bytes;
private final int classFlags;
private final ClassFileDumper dumper;
private ClassDefiner(Lookup lookup, ClassFile cf, int flags) {
private ClassDefiner(Lookup lookup, ClassFile cf, int flags, ClassFileDumper dumper) {
assert ((flags & HIDDEN_CLASS) != 0 || (flags & STRONG_LOADER_LINK) == STRONG_LOADER_LINK);
this.lookup = lookup;
this.bytes = cf.bytes;
this.name = cf.name;
this.classFlags = flags;
this.dumper = dumper;
}
String className() {
String internalName() {
return name;
}
@ -2458,12 +2500,35 @@ public class MethodHandles {
Class<?> lookupClass = lookup.lookupClass();
ClassLoader loader = lookupClass.getClassLoader();
ProtectionDomain pd = (loader != null) ? lookup.lookupClassProtectionDomain() : null;
Class<?> c = SharedSecrets.getJavaLangAccess()
.defineClass(loader, lookupClass, name, bytes, pd, initialize, classFlags, classData);
assert !isNestmate() || c.getNestHost() == lookupClass.getNestHost();
return c;
Class<?> c = null;
try {
c = SharedSecrets.getJavaLangAccess()
.defineClass(loader, lookupClass, name, bytes, pd, initialize, classFlags, classData);
assert !isNestmate() || c.getNestHost() == lookupClass.getNestHost();
return c;
} finally {
// dump the classfile for debugging
if (dumper.isEnabled()) {
String name = internalName();
if (c != null) {
dumper.dumpClass(name, c, bytes);
} else {
dumper.dumpFailedClass(name, bytes);
}
}
}
}
/**
* Defines the class of the given bytes and the given classData.
* If {@code initialize} parameter is true, then the class will be initialized.
*
* @param initialize true if the class to be initialized
* @param classData classData or null
* @return a Lookup for the defined class
*
* @throws LinkageError linkage error
*/
Lookup defineClassAsLookup(boolean initialize, Object classData) {
Class<?> c = defineClass(initialize, classData);
return new Lookup(c, null, FULL_POWER_MODES);

View File

@ -1,147 +0,0 @@
/*
* Copyright (c) 2013, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package java.lang.invoke;
import sun.util.logging.PlatformLogger;
import java.io.FilePermission;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Helper class used by InnerClassLambdaMetafactory to log generated classes
*
* @implNote
* <p> Because this class is called by LambdaMetafactory, make use
* of lambda lead to recursive calls cause stack overflow.
*/
final class ProxyClassesDumper {
private static final char[] HEX = {
'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
private static final char[] BAD_CHARS = {
'\\', ':', '*', '?', '"', '<', '>', '|'
};
private static final String[] REPLACEMENT = {
"%5C", "%3A", "%2A", "%3F", "%22", "%3C", "%3E", "%7C"
};
private final Path dumpDir;
@SuppressWarnings("removal")
public static ProxyClassesDumper getInstance(String path) {
if (null == path) {
return null;
}
try {
path = path.trim();
final Path dir = Path.of(path.isEmpty() ? "." : path);
AccessController.doPrivileged(new PrivilegedAction<>() {
@Override
public Void run() {
validateDumpDir(dir);
return null;
}
}, null, new FilePermission("<<ALL FILES>>", "read, write"));
return new ProxyClassesDumper(dir);
} catch (InvalidPathException ex) {
PlatformLogger.getLogger(ProxyClassesDumper.class.getName())
.warning("Path " + path + " is not valid - dumping disabled", ex);
} catch (IllegalArgumentException iae) {
PlatformLogger.getLogger(ProxyClassesDumper.class.getName())
.warning(iae.getMessage() + " - dumping disabled");
}
return null;
}
private ProxyClassesDumper(Path path) {
dumpDir = Objects.requireNonNull(path);
}
private static void validateDumpDir(Path path) {
if (!Files.exists(path)) {
throw new IllegalArgumentException("Directory " + path + " does not exist");
} else if (!Files.isDirectory(path)) {
throw new IllegalArgumentException("Path " + path + " is not a directory");
} else if (!Files.isWritable(path)) {
throw new IllegalArgumentException("Directory " + path + " is not writable");
}
}
public static String encodeForFilename(String className) {
final int len = className.length();
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
char c = className.charAt(i);
// control characters
if (c <= 31) {
sb.append('%');
sb.append(HEX[c >> 4 & 0x0F]);
sb.append(HEX[c & 0x0F]);
} else {
int j = 0;
for (; j < BAD_CHARS.length; j++) {
if (c == BAD_CHARS[j]) {
sb.append(REPLACEMENT[j]);
break;
}
}
if (j >= BAD_CHARS.length) {
sb.append(c);
}
}
}
return sb.toString();
}
public void dumpClass(String className, final byte[] classBytes) {
Path file;
try {
file = dumpDir.resolve(encodeForFilename(className) + ".class");
} catch (InvalidPathException ex) {
PlatformLogger.getLogger(ProxyClassesDumper.class.getName())
.warning("Invalid path for class " + className);
return;
}
try {
Path dir = file.getParent();
Files.createDirectories(dir);
Files.write(file, classBytes);
} catch (Exception ignore) {
PlatformLogger.getLogger(ProxyClassesDumper.class.getName())
.warning("Exception writing to path at " + file.toString());
// simply don't care if this operation failed
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -167,12 +167,6 @@ public interface JavaLangInvokeAccess {
*/
MethodHandle reflectiveInvoker(Class<?> caller);
/**
* Defines a hidden class of the given name and bytes with class data.
* The given bytes is trusted.
*/
Lookup defineHiddenClassWithClassData(Lookup caller, String name, byte[] bytes, Object classData, boolean initialize);
/**
* A best-effort method that tries to find any exceptions thrown by the given method handle.
* @param handle the handle to check

View File

@ -0,0 +1,232 @@
/*
* Copyright (c) 2013, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.internal.util;
import jdk.internal.misc.VM;
import sun.security.action.GetPropertyAction;
import java.io.FilePermission;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.HexFormat;
import java.util.Objects;
import java.util.PropertyPermission;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* ClassFile dumper utility class to log normal and hidden classes.
*
* @implNote
* Because this class is called by MethodHandleStatics, LambdaForms generation
* and LambdaMetafactory, make use of lambda lead to recursive calls cause stack overflow.
*/
public final class ClassFileDumper {
private static final ConcurrentHashMap<String, ClassFileDumper> DUMPER_MAP
= new ConcurrentHashMap<>();
/**
* Returns a ClassFileDumper instance for the given key. To enable
* dumping of the generated classes, set the system property via
* -D<key>=<path>.
*
* The system property is read only once when it is the first time
* the dumper instance for the given key is created.
*
* If not enabled, this method returns ClassFileDumper with null
* dump path.
*/
public static ClassFileDumper getInstance(String key) {
Objects.requireNonNull(key);
var dumper = DUMPER_MAP.get(key);
if (dumper == null) {
String path = GetPropertyAction.privilegedGetProperty(key);
Path dir;
if (path == null || path.trim().isEmpty()) {
dir = null;
} else {
dir = validateDumpDir(Path.of(path.trim()));
}
var newDumper = new ClassFileDumper(key, dir);
var v = DUMPER_MAP.putIfAbsent(key, newDumper);
dumper = v != null ? v : newDumper;
}
return dumper;
}
/**
* Returns a ClassFileDumper instance for the given key with a given
* dump path. To enable dumping of the generated classes
* -D<key> or -D<key>=true
*
* The system property is read only once when it is the first time
* the dumper instance for the given key is created.
*
* If not enabled, this method returns ClassFileDumper with null
* dump path.
*/
public static ClassFileDumper getInstance(String key, Path path) {
Objects.requireNonNull(key);
Objects.requireNonNull(path);
var dumper = DUMPER_MAP.get(key);
if (dumper == null) {
String value = GetPropertyAction.privilegedGetProperty(key);
boolean enabled = value != null && value.isEmpty()
? true : Boolean.parseBoolean(value);
Path dir = enabled ? validateDumpDir(path) : null;
var newDumper = new ClassFileDumper(key, dir);
var v = DUMPER_MAP.putIfAbsent(key, newDumper);
dumper = v != null ? v : newDumper;
}
if (dumper.isEnabled() && !path.equals(dumper.dumpPath())) {
throw new IllegalArgumentException("mismatched dump path for " + key);
}
return dumper;
}
private final String key;
private final Path dumpDir;
private final AtomicInteger counter = new AtomicInteger();
private ClassFileDumper(String key, Path path) {
this.key = key;
this.dumpDir = path;
}
public String key() {
return key;
}
public boolean isEnabled() {
return dumpDir != null;
}
public Path dumpPath() {
return dumpDir;
}
public Path pathname(String internalName) {
return dumpDir.resolve(encodeForFilename(internalName) + ".class");
}
/**
* This method determines the path name from the given name and {@code Class}
* object. If it is a hidden class, it will dump the given bytes at
* a path of the given name with a suffix "." concatenated
* with the suffix of the hidden class name.
*/
public void dumpClass(String name, Class<?> c, byte[] bytes) {
if (!isEnabled()) return;
String cn = c.getName();
int suffixIdx = cn.lastIndexOf('/');
if (suffixIdx > 0) {
name += '.' + cn.substring(suffixIdx + 1);
}
write(pathname(name), bytes);
}
/**
* This method dumps the given bytes at a path of the given name with
* a suffix ".failed-$COUNTER" where $COUNTER will be incremented
* for each time this method is called.
*/
public void dumpFailedClass(String name, byte[] bytes) {
if (!isEnabled()) return;
write(pathname(name + ".failed-" + counter.incrementAndGet()), bytes);
}
@SuppressWarnings("removal")
private void write(Path path, byte[] bytes) {
AccessController.doPrivileged(new PrivilegedAction<>() {
@Override public Void run() {
try {
Path dir = path.getParent();
Files.createDirectories(dir);
Files.write(path, bytes);
} catch (Exception ex) {
if (VM.isModuleSystemInited()) {
// log only when lambda is ready to use
System.getLogger(ClassFileDumper.class.getName())
.log(System.Logger.Level.WARNING, "Exception writing to " +
path.toString() + " " + ex.getMessage());
}
// simply don't care if this operation failed
}
return null;
}},
null,
new FilePermission("<<ALL FILES>>", "read, write"),
// createDirectories may need it
new PropertyPermission("user.dir", "read"));
}
@SuppressWarnings("removal")
private static Path validateDumpDir(Path path) {
return AccessController.doPrivileged(new PrivilegedAction<>() {
@Override
public Path run() {
try {
Files.createDirectories(path);
} catch (IOException ex) {
throw new UncheckedIOException("Fail to create " + path, ex);
}
if (!Files.isDirectory(path)) {
throw new IllegalArgumentException("Path " + path + " is not a directory");
} else if (!Files.isWritable(path)) {
throw new IllegalArgumentException("Directory " + path + " is not writable");
}
return path;
}
});
}
private static final HexFormat HEX = HexFormat.of().withUpperCase();
private static final Set<Character> BAD_CHARS = Set.of('\\', ':', '*', '?', '"', '<', '>', '|');
private static String encodeForFilename(String className) {
int len = className.length();
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
char c = className.charAt(i);
// control characters
if (c <= 31 || BAD_CHARS.contains(c)) {
sb.append('%');
HEX.toHexDigits(sb, (byte)c);
} else {
sb.append(c);
}
}
return sb.toString();
}
}

View File

@ -51,6 +51,7 @@ import static java.nio.file.Files.*;
import static jdk.internal.org.objectweb.asm.Opcodes.*;
public class LambdaAsm {
static final Path DUMP_LAMBDA_PROXY_CLASS_FILES = Path.of("DUMP_LAMBDA_PROXY_CLASS_FILES");
static final File TestFile = new File("A.java");
@ -58,7 +59,7 @@ public class LambdaAsm {
emitCode();
LUtils.compile(TestFile.getName());
LUtils.TestResult tr = LUtils.doExec(LUtils.JAVA_CMD.getAbsolutePath(),
"-Djdk.internal.lambda.dumpProxyClasses=.",
"-Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles=true",
"-cp", ".", "A");
if (tr.exitValue != 0) {
System.out.println("Error: " + tr.toString());
@ -134,7 +135,7 @@ public class LambdaAsm {
static void verifyInvokerBytecodeGenerator() throws Exception {
int count = 0;
int mcount = 0;
try (DirectoryStream<Path> ds = newDirectoryStream(new File(".").toPath(),
try (DirectoryStream<Path> ds = newDirectoryStream(DUMP_LAMBDA_PROXY_CLASS_FILES,
// filter in lambda proxy classes
"A$I$$Lambda.*.class")) {
for (Path p : ds) {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2013, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2013, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -23,7 +23,7 @@
/*
* @test
* @bug 8023524
* @bug 8023524 8304846
* @summary tests logging generated classes for lambda
* @library /java/nio/file
* @modules jdk.compiler
@ -51,6 +51,7 @@ import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
public class LogGeneratedClassesTest extends LUtils {
static final Path DUMP_LAMBDA_PROXY_CLASS_FILES = Path.of("DUMP_LAMBDA_PROXY_CLASS_FILES");
String longFQCN;
@BeforeClass
@ -93,20 +94,14 @@ public class LogGeneratedClassesTest extends LUtils {
test = new File("LongPackageName.java");
createFile(test, scratch);
compile("-d", ".", test.getName());
// create target
Files.createDirectory(Paths.get("dump"));
Files.createDirectories(Paths.get("dumpLong/com/example/nonsense"));
Files.createFile(Paths.get("dumpLong/com/example/nonsense/nonsense"));
Files.createFile(Paths.get("file"));
}
@AfterClass
public void cleanup() throws IOException {
Files.delete(Paths.get("TestLambda.java"));
Files.delete(Paths.get("LongPackageName.java"));
Files.delete(Paths.get("file"));
TestUtil.removeAll(Paths.get("com"));
TestUtil.removeAll(DUMP_LAMBDA_PROXY_CLASS_FILES);
TestUtil.removeAll(Paths.get("notDir"));
TestUtil.removeAll(Paths.get("dump"));
TestUtil.removeAll(Paths.get("dumpLong"));
}
@ -122,52 +117,68 @@ public class LogGeneratedClassesTest extends LUtils {
@Test
public void testLogging() throws IOException {
assertTrue(Files.exists(Paths.get("dump")));
Path testDir = Path.of("dump");
Path dumpDir = testDir.resolve(DUMP_LAMBDA_PROXY_CLASS_FILES);
Files.createDirectory(testDir);
TestResult tr = doExec(JAVA_CMD.getAbsolutePath(),
"-cp", ".",
"-cp", "..",
"-Duser.dir=" + testDir.toAbsolutePath(),
"-Djava.security.manager=allow",
"-Djdk.internal.lambda.dumpProxyClasses=dump",
"-Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles",
"com.example.TestLambda");
// 2 our own class files. We don't care about the others
assertEquals(Files.find(
Paths.get("dump"),
dumpDir,
99,
(p, a) -> p.startsWith(Paths.get("dump/com/example"))
(p, a) -> p.startsWith(dumpDir.resolve("com/example"))
&& a.isRegularFile()).count(),
2, "Two lambda captured");
2, "Two lambda captured");
tr.assertZero("Should still return 0");
}
@Test
public void testDumpDirNotExist() throws IOException {
assertFalse(Files.exists(Paths.get("notExist")));
Path testDir = Path.of("NotExist");
Path dumpDir = testDir.resolve(DUMP_LAMBDA_PROXY_CLASS_FILES);
Files.createDirectory(testDir);
TestUtil.removeAll(dumpDir);
assertFalse(Files.exists(dumpDir));
TestResult tr = doExec(JAVA_CMD.getAbsolutePath(),
"-cp", ".",
"-cp", "..",
"-Duser.dir=" + testDir.toAbsolutePath(),
"-Djava.security.manager=allow",
"-Djdk.internal.lambda.dumpProxyClasses=notExist",
"-Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles",
"com.example.TestLambda");
assertEquals(tr.testOutput.stream()
.filter(s -> s.startsWith("WARNING"))
.filter(s -> s.contains("does not exist"))
.count(),
1, "only show error once");
// The dump directory will be created if not exist
assertEquals(Files.find(
dumpDir,
99,
(p, a) -> p.startsWith(dumpDir.resolve("com/example"))
&& a.isRegularFile()).count(),
2, "Two lambda captured");
tr.assertZero("Should still return 0");
}
@Test
public void testDumpDirIsFile() throws IOException {
assertTrue(Files.isRegularFile(Paths.get("file")));
Path testDir = Path.of("notDir");
Path dumpFile = testDir.resolve(DUMP_LAMBDA_PROXY_CLASS_FILES);
Files.createDirectory(testDir);
Files.createFile(dumpFile);
assertTrue(Files.isRegularFile(dumpFile));
TestResult tr = doExec(JAVA_CMD.getAbsolutePath(),
"-cp", ".",
"-cp", "..",
"-Duser.dir=" + testDir.toAbsolutePath(),
"-Djava.security.manager=allow",
"-Djdk.internal.lambda.dumpProxyClasses=file",
"-Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles",
"com.example.TestLambda");
assertEquals(tr.testOutput.stream()
.filter(s -> s.startsWith("WARNING"))
.filter(s -> s.contains("not a directory"))
.filter(s -> s.contains("java.nio.file.FileAlreadyExistsException"))
.count(),
1, "only show error once");
tr.assertZero("Should still return 0");
assertTrue(tr.exitValue !=0);
}
private static boolean isWriteableDirectory(Path p) {
@ -206,59 +217,66 @@ public class LogGeneratedClassesTest extends LUtils {
return;
}
Files.createDirectory(Paths.get("readOnly"),
Path testDir = Path.of("readOnly");
Path dumpDir = testDir.resolve(DUMP_LAMBDA_PROXY_CLASS_FILES);
Files.createDirectory(testDir);
Files.createDirectory(dumpDir,
asFileAttribute(fromString("r-xr-xr-x")));
try {
if (isWriteableDirectory(Paths.get("readOnly"))) {
if (isWriteableDirectory(dumpDir)) {
// Skipping the test: it's allowed to write into read-only directory
// (e.g. current user is super user).
System.out.println("WARNING: readOnly directory is writeable. Skipping testDumpDirNotWritable test.");
System.out.println("WARNING: The dump directory is writeable. Skipping testDumpDirNotWritable test.");
return;
}
TestResult tr = doExec(JAVA_CMD.getAbsolutePath(),
"-cp", ".",
"-cp", "..",
"-Duser.dir=" + testDir.toAbsolutePath(),
"-Djava.security.manager=allow",
"-Djdk.internal.lambda.dumpProxyClasses=readOnly",
"-Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles",
"com.example.TestLambda");
assertEquals(tr.testOutput.stream()
.filter(s -> s.startsWith("WARNING"))
.filter(s -> s.contains("not writable"))
.filter(s -> s.contains("is not writable"))
.count(),
1, "only show error once");
tr.assertZero("Should still return 0");
assertTrue(tr.exitValue != 0);
} finally {
TestUtil.removeAll(Paths.get("readOnly"));
TestUtil.removeAll(testDir);
}
}
@Test
public void testLoggingException() throws IOException {
assertTrue(Files.exists(Paths.get("dumpLong")));
Path testDir = Path.of("dumpLong");
Path dumpDir = testDir.resolve(DUMP_LAMBDA_PROXY_CLASS_FILES);
Files.createDirectories(dumpDir.resolve("com/example/nonsense"));
Files.createFile(dumpDir.resolve("com/example/nonsense/nonsense"));
TestResult tr = doExec(JAVA_CMD.getAbsolutePath(),
"-cp", ".",
"-Djava.security.manager=allow",
"-Djdk.internal.lambda.dumpProxyClasses=dumpLong",
"-cp", "..",
"-Duser.dir=" + testDir.toAbsolutePath(),
"-Djava.security.manager=allow",
"-Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles",
longFQCN);
assertEquals(tr.testOutput.stream()
.filter(s -> s.startsWith("WARNING: Exception"))
.count(),
2, "show error each capture");
// dumpLong/com/example/nonsense/nonsense
Path dumpPath = Paths.get("dumpLong/com/example/nonsense");
// dumpLong/DUMP_LAMBDA_PROXY_CLASS_FILES/com/example/nonsense/nonsense
Path dumpPath = dumpDir.resolve("com/example/nonsense");
Predicate<Path> filter = p -> p.getParent() == null || dumpPath.startsWith(p) || p.startsWith(dumpPath);
boolean debug = true;
if (debug) {
Files.walk(Paths.get("dumpLong"))
Files.walk(dumpDir)
.forEachOrdered(p -> {
if (filter.test(p)) {
System.out.println("accepted: " + p.toString());
} else {
System.out.println("filetered out: " + p.toString());
System.out.println("filtered out: " + p.toString());
}
});
}
assertEquals(Files.walk(Paths.get("dumpLong"))
assertEquals(Files.walk(dumpDir)
.filter(filter)
.count(), 5, "Two lambda captured failed to log");
tr.assertZero("Should still return 0");