[core]Load module in memory (#809)
This commit is contained in:
parent
bbd5c36fe0
commit
e2a0d2f05a
|
|
@ -46,6 +46,7 @@ import android.util.Log;
|
||||||
|
|
||||||
import org.lsposed.lspd.nativebridge.NativeAPI;
|
import org.lsposed.lspd.nativebridge.NativeAPI;
|
||||||
import org.lsposed.lspd.nativebridge.ResourcesHook;
|
import org.lsposed.lspd.nativebridge.ResourcesHook;
|
||||||
|
import org.lsposed.lspd.util.InMemoryDelegateLastClassLoader;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
@ -373,7 +374,7 @@ public final class XposedInit {
|
||||||
librarySearchPath.append(apk).append("!/lib/").append(abi).append(File.pathSeparator);
|
librarySearchPath.append(apk).append("!/lib/").append(abi).append(File.pathSeparator);
|
||||||
}
|
}
|
||||||
ClassLoader initLoader = XposedInit.class.getClassLoader();
|
ClassLoader initLoader = XposedInit.class.getClassLoader();
|
||||||
ClassLoader mcl = new DelegateLastClassLoader(apk, librarySearchPath.toString(), initLoader);
|
ClassLoader mcl = InMemoryDelegateLastClassLoader.loadApk(new File(apk), librarySearchPath.toString(), initLoader);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (mcl.loadClass(XposedBridge.class.getName()).getClassLoader() != initLoader) {
|
if (mcl.loadClass(XposedBridge.class.getName()).getClassLoader() != initLoader) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
package org.lsposed.lspd.util;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FilterInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.JarURLConnection;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
|
||||||
|
import sun.net.www.ParseUtil;
|
||||||
|
|
||||||
|
public final class ClassPathURLStreamHandler extends Handler {
|
||||||
|
private final String fileUri;
|
||||||
|
private final JarFile jarFile;
|
||||||
|
|
||||||
|
public ClassPathURLStreamHandler(String jarFileName) throws IOException {
|
||||||
|
jarFile = new JarFile(jarFileName);
|
||||||
|
fileUri = new File(jarFileName).toURI().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public URL getEntryUrlOrNull(String entryName) {
|
||||||
|
if (jarFile.getEntry(entryName) != null) {
|
||||||
|
try {
|
||||||
|
String encodedName = ParseUtil.encodePath(entryName, false);
|
||||||
|
return new URL("jar", null, -1, fileUri + "!/" + encodedName, this);
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
throw new RuntimeException("Invalid entry name", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected URLConnection openConnection(URL url) throws IOException {
|
||||||
|
return new ClassPathURLConnection(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ClassPathURLConnection extends JarURLConnection {
|
||||||
|
private JarFile connectionJarFile = null;
|
||||||
|
private ZipEntry jarEntry = null;
|
||||||
|
private InputStream jarInput = null;
|
||||||
|
private boolean closed = false;
|
||||||
|
|
||||||
|
private ClassPathURLConnection(URL url) throws MalformedURLException {
|
||||||
|
super(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connect() throws IOException {
|
||||||
|
if (closed) {
|
||||||
|
throw new IllegalStateException("JarURLConnection has been closed");
|
||||||
|
}
|
||||||
|
if (!connected) {
|
||||||
|
jarEntry = jarFile.getEntry(getEntryName());
|
||||||
|
if (jarEntry == null) {
|
||||||
|
throw new FileNotFoundException("URL=" + url + ", zipfile=" + jarFile.getName());
|
||||||
|
}
|
||||||
|
connected = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JarFile getJarFile() throws IOException {
|
||||||
|
connect();
|
||||||
|
if (connectionJarFile != null) return connectionJarFile;
|
||||||
|
return connectionJarFile = new JarFile(jarFile.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() throws IOException {
|
||||||
|
connect();
|
||||||
|
if (jarInput != null) return jarInput;
|
||||||
|
return jarInput = new FilterInputStream(jarFile.getInputStream(jarEntry)) {
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
super.close();
|
||||||
|
closed = true;
|
||||||
|
jarFile.close();
|
||||||
|
if (connectionJarFile != null) connectionJarFile.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getContentType() {
|
||||||
|
String cType = guessContentTypeFromName(getEntryName());
|
||||||
|
if (cType == null) {
|
||||||
|
cType = "content/unknown";
|
||||||
|
}
|
||||||
|
return cType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getContentLength() {
|
||||||
|
try {
|
||||||
|
connect();
|
||||||
|
return (int) getJarEntry().getSize();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package org.lsposed.lspd.util;
|
||||||
|
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
|
public class CompoundEnumeration<E> implements Enumeration<E> {
|
||||||
|
private final Enumeration<E>[] enums;
|
||||||
|
private int index = 0;
|
||||||
|
|
||||||
|
public CompoundEnumeration(Enumeration<E>[] enums) {
|
||||||
|
this.enums = enums;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean next() {
|
||||||
|
while (index < enums.length) {
|
||||||
|
if (enums[index] != null && enums[index].hasMoreElements()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasMoreElements() {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
public E nextElement() {
|
||||||
|
if (!next()) {
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
}
|
||||||
|
return enums[index].nextElement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
package org.lsposed.lspd.util;
|
||||||
|
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
public abstract class Handler extends java.net.URLStreamHandler {
|
||||||
|
|
||||||
|
private static final String separator = "!/";
|
||||||
|
|
||||||
|
private static int indexOfBangSlash(String spec) {
|
||||||
|
int indexOfBang = spec.length();
|
||||||
|
while ((indexOfBang = spec.lastIndexOf('!', indexOfBang)) != -1) {
|
||||||
|
if ((indexOfBang != (spec.length() - 1)) &&
|
||||||
|
(spec.charAt(indexOfBang + 1) == '/')) {
|
||||||
|
return indexOfBang + 1;
|
||||||
|
} else {
|
||||||
|
indexOfBang--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean sameFile(URL u1, URL u2) {
|
||||||
|
if (!u1.getProtocol().equals("jar") || !u2.getProtocol().equals("jar"))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
String file1 = u1.getFile();
|
||||||
|
String file2 = u2.getFile();
|
||||||
|
int sep1 = file1.indexOf(separator);
|
||||||
|
int sep2 = file2.indexOf(separator);
|
||||||
|
|
||||||
|
if (sep1 == -1 || sep2 == -1) {
|
||||||
|
return super.sameFile(u1, u2);
|
||||||
|
}
|
||||||
|
|
||||||
|
String entry1 = file1.substring(sep1 + 2);
|
||||||
|
String entry2 = file2.substring(sep2 + 2);
|
||||||
|
|
||||||
|
if (!entry1.equals(entry2))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
URL enclosedURL1, enclosedURL2;
|
||||||
|
try {
|
||||||
|
enclosedURL1 = new URL(file1.substring(0, sep1));
|
||||||
|
enclosedURL2 = new URL(file2.substring(0, sep2));
|
||||||
|
} catch (MalformedURLException unused) {
|
||||||
|
return super.sameFile(u1, u2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.sameFile(enclosedURL1, enclosedURL2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int hashCode(URL u) {
|
||||||
|
int h = 0;
|
||||||
|
|
||||||
|
String protocol = u.getProtocol();
|
||||||
|
if (protocol != null)
|
||||||
|
h += protocol.hashCode();
|
||||||
|
|
||||||
|
String file = u.getFile();
|
||||||
|
int sep = file.indexOf(separator);
|
||||||
|
|
||||||
|
if (sep == -1)
|
||||||
|
return h + file.hashCode();
|
||||||
|
|
||||||
|
URL enclosedURL;
|
||||||
|
String fileWithoutEntry = file.substring(0, sep);
|
||||||
|
try {
|
||||||
|
enclosedURL = new URL(fileWithoutEntry);
|
||||||
|
h += enclosedURL.hashCode();
|
||||||
|
} catch (MalformedURLException unused) {
|
||||||
|
h += fileWithoutEntry.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
String entry = file.substring(sep + 2);
|
||||||
|
h += entry.hashCode();
|
||||||
|
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
protected void parseURL(URL url, String spec, int start, int limit) {
|
||||||
|
String file = null;
|
||||||
|
String ref = null;
|
||||||
|
// first figure out if there is an anchor
|
||||||
|
int refPos = spec.indexOf('#', limit);
|
||||||
|
boolean refOnly = refPos == start;
|
||||||
|
if (refPos > -1) {
|
||||||
|
ref = spec.substring(refPos + 1);
|
||||||
|
if (refOnly) {
|
||||||
|
file = url.getFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// then figure out if the spec is
|
||||||
|
// 1. absolute (jar:)
|
||||||
|
// 2. relative (i.e. url + foo/bar/baz.ext)
|
||||||
|
// 3. anchor-only (i.e. url + #foo), which we already did (refOnly)
|
||||||
|
boolean absoluteSpec = false;
|
||||||
|
if (spec.length() >= 4) {
|
||||||
|
absoluteSpec = spec.substring(0, 4).equalsIgnoreCase("jar:");
|
||||||
|
}
|
||||||
|
spec = spec.substring(start, limit);
|
||||||
|
|
||||||
|
if (absoluteSpec) {
|
||||||
|
file = parseAbsoluteSpec(spec);
|
||||||
|
} else if (!refOnly) {
|
||||||
|
file = parseContextSpec(url, spec);
|
||||||
|
|
||||||
|
// Canonize the result after the bangslash
|
||||||
|
int bangSlash = indexOfBangSlash(file);
|
||||||
|
String toBangSlash = file.substring(0, bangSlash);
|
||||||
|
String afterBangSlash = file.substring(bangSlash);
|
||||||
|
afterBangSlash = canonizeString(afterBangSlash);
|
||||||
|
file = toBangSlash + afterBangSlash;
|
||||||
|
}
|
||||||
|
setURL(url, "jar", "", -1, file, ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String canonizeString(String file) {
|
||||||
|
int i;
|
||||||
|
int lim;
|
||||||
|
|
||||||
|
// Remove embedded /../
|
||||||
|
while ((i = file.indexOf("/../")) >= 0) {
|
||||||
|
if ((lim = file.lastIndexOf('/', i - 1)) >= 0) {
|
||||||
|
file = file.substring(0, lim) + file.substring(i + 3);
|
||||||
|
} else {
|
||||||
|
file = file.substring(i + 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove embedded /./
|
||||||
|
while ((i = file.indexOf("/./")) >= 0) {
|
||||||
|
file = file.substring(0, i) + file.substring(i + 2);
|
||||||
|
}
|
||||||
|
// Remove trailing ..
|
||||||
|
while (file.endsWith("/..")) {
|
||||||
|
i = file.indexOf("/..");
|
||||||
|
if ((lim = file.lastIndexOf('/', i - 1)) >= 0) {
|
||||||
|
file = file.substring(0, lim + 1);
|
||||||
|
} else {
|
||||||
|
file = file.substring(0, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove trailing .
|
||||||
|
if (file.endsWith("/."))
|
||||||
|
file = file.substring(0, file.length() - 1);
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseAbsoluteSpec(String spec) {
|
||||||
|
int index;
|
||||||
|
// check for !/
|
||||||
|
if ((index = indexOfBangSlash(spec)) == -1) {
|
||||||
|
throw new NullPointerException("no !/ in spec");
|
||||||
|
}
|
||||||
|
// test the inner URL
|
||||||
|
try {
|
||||||
|
String innerSpec = spec.substring(0, index - 1);
|
||||||
|
new URL(innerSpec);
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
throw new NullPointerException("invalid url: " +
|
||||||
|
spec + " (" + e + ")");
|
||||||
|
}
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseContextSpec(URL url, String spec) {
|
||||||
|
String ctxFile = url.getFile();
|
||||||
|
// if the spec begins with /, chop up the jar back !/
|
||||||
|
if (spec.startsWith("/")) {
|
||||||
|
int bangSlash = indexOfBangSlash(ctxFile);
|
||||||
|
if (bangSlash == -1) {
|
||||||
|
throw new NullPointerException("malformed " + "context url:" + url + ": no !/");
|
||||||
|
}
|
||||||
|
ctxFile = ctxFile.substring(0, bangSlash);
|
||||||
|
}
|
||||||
|
if (!ctxFile.endsWith("/") && (!spec.startsWith("/"))) {
|
||||||
|
// chop up the last component
|
||||||
|
int lastSlash = ctxFile.lastIndexOf('/');
|
||||||
|
if (lastSlash == -1) {
|
||||||
|
throw new NullPointerException("malformed " + "context url:" + url);
|
||||||
|
}
|
||||||
|
ctxFile = ctxFile.substring(0, lastSlash + 1);
|
||||||
|
}
|
||||||
|
return (ctxFile + spec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
package org.lsposed.lspd.util;
|
||||||
|
|
||||||
|
import static de.robv.android.xposed.XposedBridge.TAG;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.Channels;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
|
import hidden.ByteBufferDexClassLoader;
|
||||||
|
|
||||||
|
@SuppressWarnings("ConstantConditions")
|
||||||
|
public final class InMemoryDelegateLastClassLoader extends ByteBufferDexClassLoader {
|
||||||
|
private final String apk;
|
||||||
|
|
||||||
|
private InMemoryDelegateLastClassLoader(ByteBuffer[] dexBuffers,
|
||||||
|
String librarySearchPath,
|
||||||
|
ClassLoader parent,
|
||||||
|
String apk) {
|
||||||
|
super(dexBuffers, librarySearchPath, parent);
|
||||||
|
this.apk = apk;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||||
|
var cl = findLoadedClass(name);
|
||||||
|
if (cl != null) {
|
||||||
|
return cl;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Object.class.getClassLoader().loadClass(name);
|
||||||
|
} catch (ClassNotFoundException ignored) {
|
||||||
|
}
|
||||||
|
ClassNotFoundException fromSuper;
|
||||||
|
try {
|
||||||
|
return findClass(name);
|
||||||
|
} catch (ClassNotFoundException ex) {
|
||||||
|
fromSuper = ex;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return getParent().loadClass(name);
|
||||||
|
} catch (ClassNotFoundException cnfe) {
|
||||||
|
throw fromSuper;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected URL findResource(String name) {
|
||||||
|
try {
|
||||||
|
var urlHandler = new ClassPathURLStreamHandler(apk);
|
||||||
|
return urlHandler.getEntryUrlOrNull(name);
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Enumeration<URL> findResources(String name) {
|
||||||
|
var result = new ArrayList<URL>();
|
||||||
|
var url = findResource(name);
|
||||||
|
if (url != null) result.add(url);
|
||||||
|
return Collections.enumeration(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public URL getResource(String name) {
|
||||||
|
var resource = Object.class.getClassLoader().getResource(name);
|
||||||
|
if (resource != null) return resource;
|
||||||
|
resource = findResource(name);
|
||||||
|
if (resource != null) return resource;
|
||||||
|
final var cl = getParent();
|
||||||
|
return (cl == null) ? null : cl.getResource(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Enumeration<URL> getResources(String name) throws IOException {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
final var resources = (Enumeration<URL>[]) new Enumeration<?>[]{
|
||||||
|
Object.class.getClassLoader().getResources(name),
|
||||||
|
findResources(name),
|
||||||
|
getParent() == null ? null : getParent().getResources(name)};
|
||||||
|
return new CompoundEnumeration<>(resources);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InMemoryDelegateLastClassLoader loadApk(File apk, String librarySearchPath, ClassLoader parent) {
|
||||||
|
var byteBuffers = new ArrayList<ByteBuffer>();
|
||||||
|
try (var apkFile = new ZipFile(apk)) {
|
||||||
|
int secondaryNumber = 2;
|
||||||
|
for (var dexFile = apkFile.getEntry("classes.dex"); dexFile != null;
|
||||||
|
dexFile = apkFile.getEntry("classes" + secondaryNumber + ".dex"), secondaryNumber++) {
|
||||||
|
try (var in = apkFile.getInputStream(dexFile)) {
|
||||||
|
var byteBuffer = ByteBuffer.allocate(in.available());
|
||||||
|
byteBuffer.mark();
|
||||||
|
Channels.newChannel(in).read(byteBuffer);
|
||||||
|
byteBuffer.reset();
|
||||||
|
byteBuffers.add(byteBuffer);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w(TAG, "Can not read " + dexFile + " in " + apk, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Can not open " + apk, e);
|
||||||
|
}
|
||||||
|
var dexBuffers = new ByteBuffer[byteBuffers.size()];
|
||||||
|
return new InMemoryDelegateLastClassLoader(byteBuffers.toArray(dexBuffers),
|
||||||
|
librarySearchPath, parent, apk.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package hidden;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import dalvik.system.BaseDexClassLoader;
|
||||||
|
|
||||||
|
public class ByteBufferDexClassLoader extends BaseDexClassLoader {
|
||||||
|
public ByteBufferDexClassLoader(ByteBuffer[] dexFiles, String librarySearchPath, ClassLoader parent) {
|
||||||
|
super(dexFiles, librarySearchPath, parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ package dalvik.system;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
|
|
||||||
public class BaseDexClassLoader extends ClassLoader {
|
public class BaseDexClassLoader extends ClassLoader {
|
||||||
|
|
@ -14,6 +15,10 @@ public class BaseDexClassLoader extends ClassLoader {
|
||||||
throw new RuntimeException("Stub!");
|
throw new RuntimeException("Stub!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BaseDexClassLoader(ByteBuffer[] dexFiles, String librarySearchPath, ClassLoader parent) {
|
||||||
|
throw new RuntimeException("Stub!");
|
||||||
|
}
|
||||||
|
|
||||||
protected Class<?> findClass(String name) throws ClassNotFoundException {
|
protected Class<?> findClass(String name) throws ClassNotFoundException {
|
||||||
throw new RuntimeException("Stub!");
|
throw new RuntimeException("Stub!");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package sun.net.www;
|
||||||
|
|
||||||
|
public class ParseUtil {
|
||||||
|
public static String encodePath(String path, boolean flag) {
|
||||||
|
throw new RuntimeException("Stub!");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue