From 3cd9fd1735ca233ec77d846e239c905b247cbbd7 Mon Sep 17 00:00:00 2001 From: vvb2060 Date: Mon, 9 Aug 2021 19:27:58 +0800 Subject: [PATCH] [core] Preload module apk (#877) --- .../aidl/org/lsposed/lspd/models/Module.aidl | 8 +- .../{ModuleConfig.aidl => PreLoadedApk.aidl} | 3 +- .../de/robv/android/xposed/XposedInit.java | 167 ++++++++---------- .../main/java/org/lsposed/lspd/core/Main.java | 10 +- .../lsposed/lspd/service/ConfigManager.java | 141 ++++++++++----- .../lspd/util/LspModuleClassLoader.java | 6 +- 6 files changed, 174 insertions(+), 161 deletions(-) rename core/src/main/aidl/org/lsposed/lspd/models/{ModuleConfig.aidl => PreLoadedApk.aidl} (63%) diff --git a/core/src/main/aidl/org/lsposed/lspd/models/Module.aidl b/core/src/main/aidl/org/lsposed/lspd/models/Module.aidl index b5530ead..4a494ffb 100644 --- a/core/src/main/aidl/org/lsposed/lspd/models/Module.aidl +++ b/core/src/main/aidl/org/lsposed/lspd/models/Module.aidl @@ -1,8 +1,8 @@ package org.lsposed.lspd.models; -import org.lsposed.lspd.models.ModuleConfig; +import org.lsposed.lspd.models.PreLoadedApk; parcelable Module { - String name; - String apk; - ModuleConfig config; + String packageName; + String apkPath; + PreLoadedApk file; } diff --git a/core/src/main/aidl/org/lsposed/lspd/models/ModuleConfig.aidl b/core/src/main/aidl/org/lsposed/lspd/models/PreLoadedApk.aidl similarity index 63% rename from core/src/main/aidl/org/lsposed/lspd/models/ModuleConfig.aidl rename to core/src/main/aidl/org/lsposed/lspd/models/PreLoadedApk.aidl index e8e01890..643e9d67 100644 --- a/core/src/main/aidl/org/lsposed/lspd/models/ModuleConfig.aidl +++ b/core/src/main/aidl/org/lsposed/lspd/models/PreLoadedApk.aidl @@ -1,6 +1,7 @@ package org.lsposed.lspd.models; -parcelable ModuleConfig { +parcelable PreLoadedApk { List preLoadedDexes; List moduleClassNames; + List moduleLibraryNames; } diff --git a/core/src/main/java/de/robv/android/xposed/XposedInit.java b/core/src/main/java/de/robv/android/xposed/XposedInit.java index 622e797d..7532002b 100644 --- a/core/src/main/java/de/robv/android/xposed/XposedInit.java +++ b/core/src/main/java/de/robv/android/xposed/XposedInit.java @@ -31,7 +31,6 @@ import static de.robv.android.xposed.XposedHelpers.getObjectField; import static de.robv.android.xposed.XposedHelpers.getParameterIndexByType; import static de.robv.android.xposed.XposedHelpers.setStaticObjectField; -import android.annotation.SuppressLint; import android.content.pm.ApplicationInfo; import android.content.res.Resources; import android.content.res.ResourcesImpl; @@ -40,25 +39,20 @@ import android.content.res.XResources; import android.os.Build; import android.os.IBinder; import android.os.Process; -import android.os.SharedMemory; import android.util.ArraySet; import android.util.Log; +import org.lsposed.lspd.models.PreLoadedApk; import org.lsposed.lspd.nativebridge.NativeAPI; import org.lsposed.lspd.nativebridge.ResourcesHook; import org.lsposed.lspd.util.LspModuleClassLoader; -import java.io.BufferedReader; import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.lang.ref.WeakReference; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import de.robv.android.xposed.callbacks.XC_InitPackageResources; @@ -87,7 +81,7 @@ public final class XposedInit { findAndHookMethod("android.app.ApplicationPackageManager", null, "getResourcesForApplication", ApplicationInfo.class, new XC_MethodHook() { @Override - protected void beforeHookedMethod(MethodHookParam param) throws Throwable { + protected void beforeHookedMethod(MethodHookParam param) { ApplicationInfo app = (ApplicationInfo) param.args[0]; XResources.setPackageNameForResDir(app.packageName, app.uid == Process.myUid() ? app.sourceDir : app.publicSourceDir); @@ -119,7 +113,7 @@ public final class XposedInit { hookAllMethods(classGTLR, createResourceMethod, new XC_MethodHook() { @Override - protected void afterHookedMethod(MethodHookParam param) throws Throwable { + protected void afterHookedMethod(MethodHookParam param) { // At least on OnePlus 5, the method has an additional parameter compared to AOSP. final int activityTokenIdx = getParameterIndexByType(param.method, IBinder.class); final int resKeyIdx = getParameterIndexByType(param.method, classResKey); @@ -131,15 +125,18 @@ public final class XposedInit { } Object activityToken = param.args[activityTokenIdx]; + //noinspection SynchronizeOnNonFinalField synchronized (param.thisObject) { ArrayList> resourceReferences; if (activityToken != null) { Object activityResources = callMethod(param.thisObject, "getOrCreateActivityResourcesStructLocked", activityToken); + //noinspection unchecked resourceReferences = (ArrayList>) getObjectField(activityResources, "activityResources"); } else { + //noinspection unchecked resourceReferences = (ArrayList>) getObjectField(param.thisObject, "mResourceReferences"); } - resourceReferences.add(new WeakReference(newRes)); + resourceReferences.add(new WeakReference<>(newRes)); } } }); @@ -214,23 +211,23 @@ public final class XposedInit { } } - public static boolean loadModules() throws IOException { + public static void loadModules() { boolean hasLoaded = !modulesLoaded.compareAndSet(false, true); if (hasLoaded) { - return false; + return; } synchronized (moduleLoadLock) { var moduleList = serviceClient.getModulesList(); - ArraySet newLoadedApk = new ArraySet<>(); + var newLoadedApk = new ArraySet(); moduleList.forEach(module -> { - var apk = module.apk; - var name = module.name; - var dexes = module.config.preLoadedDexes; + var apk = module.apkPath; + var name = module.packageName; + var file = module.file; if (loadedModules.contains(apk)) { newLoadedApk.add(apk); } else { loadedModules.add(apk); // temporarily add it for XSharedPreference - boolean loadSuccess = loadModule(name, apk, dexes); + boolean loadSuccess = loadModule(name, apk, file); if (loadSuccess) { newLoadedApk.add(apk); } @@ -240,14 +237,13 @@ public final class XposedInit { loadedModules.addAll(newLoadedApk); // refresh callback according to current loaded module list - pruneCallbacks(loadedModules); + pruneCallbacks(); }); } - return true; } // remove deactivated or outdated module callbacks - private static void pruneCallbacks(Set loadedModules) { + private static void pruneCallbacks() { synchronized (moduleLoadLock) { Object[] loadedPkgSnapshot = sLoadedPackageCallbacks.getSnapshot(); Object[] initPkgResSnapshot = sInitPackageResourcesCallbacks.getSnapshot(); @@ -280,110 +276,87 @@ public final class XposedInit { * Load all so from an APK by reading assets/native_init. * It will only store the so names but not doing anything. */ - private static boolean initNativeModule(ClassLoader mcl, String name) { - try (InputStream is = mcl.getResourceAsStream("assets/native_init")) { - if (is == null) return true; - BufferedReader moduleLibraryReader = new BufferedReader(new InputStreamReader(is)); - String moduleLibraryName; - while ((moduleLibraryName = moduleLibraryReader.readLine()) != null) { - if (!moduleLibraryName.startsWith("#")) { - NativeAPI.recordNativeEntrypoint(moduleLibraryName); - } - } - return true; - } catch (IOException e) { - Log.e(TAG, " Failed to load native library list from " + name, e); - return false; - } + private static void initNativeModule(List moduleLibraryNames) { + moduleLibraryNames.forEach(NativeAPI::recordNativeEntrypoint); } - private static boolean initModule(ClassLoader mcl, String name, String apk) { - InputStream is = mcl.getResourceAsStream("assets/xposed_init"); - if (is == null) { - return true; - } - try (BufferedReader moduleClassesReader = new BufferedReader(new InputStreamReader(is))) { - String moduleClassName; - while ((moduleClassName = moduleClassesReader.readLine()) != null) { - moduleClassName = moduleClassName.trim(); - if (moduleClassName.isEmpty() || moduleClassName.startsWith("#")) + private static boolean initModule(ClassLoader mcl, String apk, List moduleClassNames) { + var count = 0; + for (var moduleClassName : moduleClassNames) { + try { + Log.i(TAG, " Loading class " + moduleClassName); + + Class moduleClass = mcl.loadClass(moduleClassName); + + if (!IXposedMod.class.isAssignableFrom(moduleClass)) { + Log.e(TAG, " This class doesn't implement any sub-interface of IXposedMod, skipping it"); + continue; + } else if (disableResources && IXposedHookInitPackageResources.class.isAssignableFrom(moduleClass)) { + Log.e(TAG, " This class requires resource-related hooks (which are disabled), skipping it."); continue; - - try { - Log.i(TAG, " Loading class " + moduleClassName); - Class moduleClass = mcl.loadClass(moduleClassName); - - if (!IXposedMod.class.isAssignableFrom(moduleClass)) { - Log.e(TAG, " This class doesn't implement any sub-interface of IXposedMod, skipping it"); - continue; - } else if (disableResources && IXposedHookInitPackageResources.class.isAssignableFrom(moduleClass)) { - Log.e(TAG, " This class requires resource-related hooks (which are disabled), skipping it."); - continue; - } - - final Object moduleInstance = moduleClass.newInstance(); - if (moduleInstance instanceof IXposedHookZygoteInit) { - IXposedHookZygoteInit.StartupParam param = new IXposedHookZygoteInit.StartupParam(); - param.modulePath = apk; - param.startsSystemServer = startsSystemServer; - - XposedBridge.hookInitZygote(new IXposedHookZygoteInit.Wrapper( - (IXposedHookZygoteInit) moduleInstance, param)); - ((IXposedHookZygoteInit) moduleInstance).initZygote(param); - } - - if (moduleInstance instanceof IXposedHookLoadPackage) - XposedBridge.hookLoadPackage(new IXposedHookLoadPackage.Wrapper( - (IXposedHookLoadPackage) moduleInstance, apk)); - - if (moduleInstance instanceof IXposedHookInitPackageResources) - XposedBridge.hookInitPackageResources(new IXposedHookInitPackageResources.Wrapper( - (IXposedHookInitPackageResources) moduleInstance, apk)); - } catch (Throwable t) { - Log.e(TAG, " Failed to load class " + moduleClassName, t); - return false; } + + final Object moduleInstance = moduleClass.newInstance(); + + if (moduleInstance instanceof IXposedHookZygoteInit) { + IXposedHookZygoteInit.StartupParam param = new IXposedHookZygoteInit.StartupParam(); + param.modulePath = apk; + param.startsSystemServer = startsSystemServer; + + XposedBridge.hookInitZygote(new IXposedHookZygoteInit.Wrapper( + (IXposedHookZygoteInit) moduleInstance, param)); + ((IXposedHookZygoteInit) moduleInstance).initZygote(param); + count++; + } + + if (moduleInstance instanceof IXposedHookLoadPackage) { + XposedBridge.hookLoadPackage(new IXposedHookLoadPackage.Wrapper( + (IXposedHookLoadPackage) moduleInstance, apk)); + count++; + } + + if (moduleInstance instanceof IXposedHookInitPackageResources) { + XposedBridge.hookInitPackageResources(new IXposedHookInitPackageResources.Wrapper( + (IXposedHookInitPackageResources) moduleInstance, apk)); + count++; + } + } catch (Throwable t) { + Log.e(TAG, " Failed to load class " + moduleClassName, t); } - return true; - } catch (IOException e) { - Log.e(TAG, " Failed to load module " + name + " from " + apk, e); - return false; } + return count > 0; } /** * Load a module from an APK by calling the init(String) method for all classes defined * in assets/xposed_init. */ - @SuppressLint("PrivateApi") - private static boolean loadModule(String name, String apk, List dexes) { + private static boolean loadModule(String name, String apk, PreLoadedApk file) { Log.i(TAG, "Loading module " + name + " from " + apk); - if (!new File(apk).exists()) { - Log.e(TAG, " File does not exist"); - return false; - } - - var librarySearchPath = new StringBuilder(); + var sb = new StringBuilder(); var abis = Process.is64Bit() ? Build.SUPPORTED_64_BIT_ABIS : Build.SUPPORTED_32_BIT_ABIS; for (String abi : abis) { - librarySearchPath.append(apk).append("!/lib/").append(abi).append(File.pathSeparator); + sb.append(apk).append("!/lib/").append(abi).append(File.pathSeparator); } - ClassLoader initLoader = XposedInit.class.getClassLoader(); - ClassLoader mcl = LspModuleClassLoader.loadApk(new File(apk), dexes, librarySearchPath.toString(), initLoader); + var librarySearchPath = sb.toString(); + + var initLoader = XposedInit.class.getClassLoader(); + var mcl = LspModuleClassLoader.loadApk(apk, file.preLoadedDexes, librarySearchPath, initLoader); try { if (mcl.loadClass(XposedBridge.class.getName()).getClassLoader() != initLoader) { Log.e(TAG, " Cannot load module: " + name); Log.e(TAG, " The Xposed API classes are compiled into the module's APK."); Log.e(TAG, " This may cause strange issues and must be fixed by the module developer."); - Log.e(TAG, " For details, see: http://api.xposed.info/using.html"); + Log.e(TAG, " For details, see: https://api.xposed.info/using.html"); return false; } } catch (ClassNotFoundException ignored) { + return false; } - - return initNativeModule(mcl, name) && initModule(mcl, name, apk); + initNativeModule(file.moduleLibraryNames); + return initModule(mcl, apk, file.moduleClassNames); } public final static HashSet loadedPackagesInProcess = new HashSet<>(1); diff --git a/core/src/main/java/org/lsposed/lspd/core/Main.java b/core/src/main/java/org/lsposed/lspd/core/Main.java index 51bcb037..b62e6af1 100644 --- a/core/src/main/java/org/lsposed/lspd/core/Main.java +++ b/core/src/main/java/org/lsposed/lspd/core/Main.java @@ -78,14 +78,6 @@ public class Main { } } - private static void loadModulesSafely() { - try { - XposedInit.loadModules(); - } catch (Exception exception) { - Utils.logE("error loading module list", exception); - } - } - public static void forkPostCommon(boolean isSystem, String appDataDir, String niceName) { // init logger YahfaHooker.init(); @@ -95,7 +87,7 @@ public class Main { PrebuiltMethodsDeopter.deoptBootMethods(); // do it once for secondary zygote installBootstrapHooks(isSystem, appDataDir); Utils.logI("Loading modules for " + niceName + "/" + Process.myUid()); - loadModulesSafely(); + XposedInit.loadModules(); } public static void forkAndSpecializePost(String appDataDir, String niceName, IBinder binder) { diff --git a/core/src/main/java/org/lsposed/lspd/service/ConfigManager.java b/core/src/main/java/org/lsposed/lspd/service/ConfigManager.java index 34f3a2c7..2827a6ce 100644 --- a/core/src/main/java/org/lsposed/lspd/service/ConfigManager.java +++ b/core/src/main/java/org/lsposed/lspd/service/ConfigManager.java @@ -47,12 +47,14 @@ import org.apache.commons.lang3.SerializationUtils; import org.lsposed.lspd.BuildConfig; import org.lsposed.lspd.models.Application; import org.lsposed.lspd.models.Module; -import org.lsposed.lspd.models.ModuleConfig; +import org.lsposed.lspd.models.PreLoadedApk; +import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Serializable; import java.nio.channels.Channels; @@ -200,8 +202,8 @@ public class ConfigManager { private final Map> cachedScope = new ConcurrentHashMap<>(); - // apkPath, dexes - private final Map> cachedDexes = new ConcurrentHashMap<>(); + // apkPath, PreLoadedApk + private final Map cachedApkFile = new ConcurrentHashMap<>(); // appId, packageName private final Map cachedModule = new ConcurrentHashMap<>(); @@ -256,14 +258,17 @@ public class ConfigManager { int apkPathIdx = cursor.getColumnIndex("apk_path"); int pkgNameIdx = cursor.getColumnIndex("module_pkg_name"); while (cursor.moveToNext()) { - var module = new Module(); - var config = new ModuleConfig(); var path = cursor.getString(apkPathIdx); - config.preLoadedDexes = getModuleDexes(path); - module.name = cursor.getString(pkgNameIdx); - module.apk = path; - module.config = config; - modules.add(module); + var file = getCachedApkFile(path); + if (file != null) { + var module = new Module(); + module.packageName = cursor.getString(pkgNameIdx); + module.apkPath = path; + module.file = file; + modules.add(module); + } else { + Log.w(TAG, "Can not load " + path + ", skip!"); + } } } return modules; @@ -476,7 +481,7 @@ public class ConfigManager { for (var obsoleteScope : obsoleteScopes) { removeModuleScopeWithoutCache(obsoleteScope); } - cleanModuleDexes(); + checkCachedApkFile(); } Log.d(TAG, "cached modules"); for (int uid : cachedModule.keySet()) { @@ -507,26 +512,35 @@ public class ConfigManager { app.userId = cursor.getInt(userIdIdx); // system server always loads database if (app.packageName.equals("android")) continue; - String apk_path = cursor.getString(apkPathIdx); - String module_pkg = cursor.getString(modulePkgNameIdx); try { List processesScope = getAssociatedProcesses(app); if (processesScope.isEmpty()) { obsoletePackages.add(app); continue; } + var apkPath = cursor.getString(apkPathIdx); + var modulePackageName = cursor.getString(modulePkgNameIdx); + var file = getCachedApkFile(apkPath); + Module module; + if (file != null) { + module = new Module(); + module.packageName = modulePackageName; + module.apkPath = apkPath; + module.file = file; + } else { + Log.w(TAG, "Can not load " + apkPath + ", skip!"); + continue; + } for (ProcessScope processScope : processesScope) { - var module = new Module(); - var config = new ModuleConfig(); - config.preLoadedDexes = getModuleDexes(apk_path); - module.name = module_pkg; - module.apk = apk_path; - module.config = config; - cachedScope.computeIfAbsent(processScope, ignored -> new LinkedList<>()).add(module); - if (module_pkg.equals(app.packageName)) { + cachedScope.computeIfAbsent(processScope, + ignored -> new LinkedList<>()).add(module); + // Always allow the module to inject itself + if (modulePackageName.equals(app.packageName)) { var appId = processScope.uid % PER_USER_RANGE; for (var user : UserService.getUsers()) { - cachedScope.computeIfAbsent(new ProcessScope(processScope.processName, user.id * PER_USER_RANGE + appId), + var moduleUid = user.id * PER_USER_RANGE + appId; + var moduleSelf = new ProcessScope(processScope.processName, moduleUid); + cachedScope.computeIfAbsent(moduleSelf, ignored -> new LinkedList<>()).add(module); } } @@ -543,43 +557,76 @@ public class ConfigManager { Log.d(TAG, "cached Scope"); cachedScope.forEach((ps, modules) -> { Log.d(TAG, ps.processName + "/" + ps.uid); - modules.forEach(module -> Log.d(TAG, "\t" + module.name)); + modules.forEach(module -> Log.d(TAG, "\t" + module.packageName)); }); } - private List loadModuleDexes(String path) { - var sharedMemories = new ArrayList(); - try (var apkFile = new ZipFile(path)) { - int secondary = 2; - for (var dexFile = apkFile.getEntry("classes.dex"); dexFile != null; - dexFile = apkFile.getEntry("classes" + secondary + ".dex"), secondary++) { - try (var in = apkFile.getInputStream(dexFile)) { - var memory = SharedMemory.create(null, in.available()); - var byteBuffer = memory.mapReadWrite(); - Channels.newChannel(in).read(byteBuffer); - SharedMemory.unmap(byteBuffer); - memory.setProtect(OsConstants.PROT_READ); - sharedMemories.add(memory); - } catch (IOException | ErrnoException e) { - Log.w(TAG, "Can not load " + dexFile + " in " + path, e); - } + private void readDexes(ZipFile apkFile, List preLoadedDexes) { + int secondary = 2; + for (var dexFile = apkFile.getEntry("classes.dex"); dexFile != null; + dexFile = apkFile.getEntry("classes" + secondary + ".dex"), secondary++) { + try (var in = apkFile.getInputStream(dexFile)) { + var memory = SharedMemory.create(null, in.available()); + var byteBuffer = memory.mapReadWrite(); + Channels.newChannel(in).read(byteBuffer); + SharedMemory.unmap(byteBuffer); + memory.setProtect(OsConstants.PROT_READ); + preLoadedDexes.add(memory); + } catch (IOException | ErrnoException e) { + Log.w(TAG, "Can not load " + dexFile + " in " + apkFile, e); + } + } + } + + private void readName(ZipFile apkFile, String initName, List names) { + var initEntry = apkFile.getEntry(initName); + if (initEntry == null) return; + try (var in = apkFile.getInputStream(initEntry)) { + var reader = new BufferedReader(new InputStreamReader(in)); + String name; + while ((name = reader.readLine()) != null) { + name = name.trim(); + if (name.isEmpty() || name.startsWith("#")) continue; + names.add(name); } } catch (IOException e) { - Log.e(TAG, "Can not open " + path, e); + Log.e(TAG, "Can not open " + initEntry, e); } - return sharedMemories; } - private List getModuleDexes(String path) { - return cachedDexes.computeIfAbsent(path, this::loadModuleDexes); + @Nullable + private PreLoadedApk loadModule(String path) { + var file = new PreLoadedApk(); + var preLoadedDexes = new ArrayList(); + var moduleClassNames = new ArrayList(1); + var moduleLibraryNames = new ArrayList(1); + try (var apkFile = new ZipFile(path)) { + readDexes(apkFile, preLoadedDexes); + readName(apkFile, "assets/xposed_init", moduleClassNames); + readName(apkFile, "assets/native_init", moduleLibraryNames); + } catch (IOException e) { + Log.e(TAG, "Can not open " + path, e); + return null; + } + if (preLoadedDexes.isEmpty()) return null; + if (moduleClassNames.isEmpty()) return null; + file.preLoadedDexes = preLoadedDexes; + file.moduleClassNames = moduleClassNames; + file.moduleLibraryNames = moduleLibraryNames; + return file; } - private void cleanModuleDexes() { - cachedDexes.entrySet().removeIf(entry -> { + @Nullable + private PreLoadedApk getCachedApkFile(String path) { + return cachedApkFile.computeIfAbsent(path, this::loadModule); + } + + private void checkCachedApkFile() { + cachedApkFile.entrySet().removeIf(entry -> { var path = entry.getKey(); - var dexes = entry.getValue(); + var file = entry.getValue(); if (!new File(path).exists()) { - dexes.stream().parallel().forEach(SharedMemory::close); + file.preLoadedDexes.stream().parallel().forEach(SharedMemory::close); return true; } return false; diff --git a/core/src/main/java/org/lsposed/lspd/util/LspModuleClassLoader.java b/core/src/main/java/org/lsposed/lspd/util/LspModuleClassLoader.java index 4704381a..902fdf61 100644 --- a/core/src/main/java/org/lsposed/lspd/util/LspModuleClassLoader.java +++ b/core/src/main/java/org/lsposed/lspd/util/LspModuleClassLoader.java @@ -176,7 +176,7 @@ public final class LspModuleClassLoader extends ByteBufferDexClassLoader { super.toString() + "]"; } - public static LspModuleClassLoader loadApk(File apk, + public static LspModuleClassLoader loadApk(String apk, List dexes, String librarySearchPath, ClassLoader parent) { @@ -191,9 +191,9 @@ public final class LspModuleClassLoader extends ByteBufferDexClassLoader { LspModuleClassLoader cl; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { cl = new LspModuleClassLoader(dexBuffers, librarySearchPath, - parent, apk.getAbsolutePath()); + parent, apk); } else { - cl = new LspModuleClassLoader(dexBuffers, parent, apk.getAbsolutePath()); + cl = new LspModuleClassLoader(dexBuffers, parent, apk); cl.initNativeLibraryDirs(librarySearchPath); } Arrays.stream(dexBuffers).parallel().forEach(SharedMemory::unmap);