/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed 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
 *
 *     http://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 com.facebook.soloader;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.os.StrictMode;
import android.text.TextUtils;
import android.util.Log;
import com.facebook.soloader.nativeloader.NativeLoader;
import dalvik.system.BaseDexClassLoader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.annotation.Nullable;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.NotThreadSafe;
import javax.annotation.concurrent.ThreadSafe;

/**
 * Native code loader.
 *
 * <p>To load a native library, call the static method {@link #loadLibrary} from the static
 * initializer of the Java class declaring the native methods. The argument should be the library's
 * short name.
 *
 * <p>For example, if the native code is in libmy_jni_methods.so:
 *
 * <pre>{@code
 * class MyClass {
 *   static {
 *     SoLoader.loadLibrary("my_jni_methods");
 *   }
 * }
 * }</pre>
 *
 * <p>Before any library can be loaded SoLoader needs to be initialized. The application using
 * SoLoader should do that by calling SoLoader.init early on app initialization path. The call must
 * happen before any class using SoLoader in its static initializer is loaded.
 */
@ThreadSafe
public class SoLoader {

  /* package */ static final String TAG = "SoLoader";
  /* package */ static final boolean DEBUG = false;
  /* package */ static final boolean SYSTRACE_LIBRARY_LOADING;
  /* package */ @Nullable static SoFileLoader sSoFileLoader;

  /**
   * locking controlling the list of SoSources. We want to allow long running iterations over the
   * list to happen concurrently, but also ensure that nothing modifies the list while others are
   * reading it.
   */
  private static final ReentrantReadWriteLock sSoSourcesLock = new ReentrantReadWriteLock();

  /**
   * Ordered list of sources to consult when trying to load a shared library or one of its
   * dependencies. {@code null} indicates that SoLoader is uninitialized.
   */
  @GuardedBy("sSoSourcesLock")
  @Nullable
  private static SoSource[] sSoSources = null;

  @GuardedBy("sSoSourcesLock")
  private static final AtomicInteger sSoSourcesVersion = new AtomicInteger(0);

  /** A backup SoSources to try if a lib file is corrupted */
  @GuardedBy("sSoSourcesLock")
  @Nullable
  private static UnpackingSoSource[] sBackupSoSources;

  /**
   * A SoSource for the Context.ApplicationInfo.nativeLibsDir that can be updated if the application
   * moves this directory
   */
  @GuardedBy("sSoSourcesLock")
  @Nullable
  private static ApplicationSoSource sApplicationSoSource;

  /** Records the sonames (e.g., "libdistract.so") of shared libraries we've loaded. */
  @GuardedBy("SoLoader.class")
  private static final HashSet<String> sLoadedLibraries = new HashSet<>();

  /**
   * Libraries that are in the process of being loaded, and lock objects to synchronize on and wait
   * for the loading to end.
   *
   * <p>To prevent potential deadlock, always acquire sSoSourcesLock before these locks!
   */
  @GuardedBy("SoLoader.class")
  private static final Map<String, Object> sLoadingLibraries = new HashMap<>();

  /**
   * Libraries that have loaded and completed their JniOnLoad. Reads and writes are guarded by the
   * corresponding lock in sLoadingLibraries. However, as we only add to this set (and never
   * remove), it is safe to perform un-guarded reads as an optional optimization prior to locking.
   */
  private static final Set<String> sLoadedAndMergedLibraries =
      Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());

  /** Wrapper for System.loadLibrary. */
  @Nullable private static SystemLoadLibraryWrapper sSystemLoadLibraryWrapper = null;

  /**
   * Name of the directory we use for extracted DSOs from built-in SO sources (main APK, exopackage)
   */
  private static final String SO_STORE_NAME_MAIN = "lib-main";

  /** Name of the directory we use for extracted DSOs from split APKs */
  private static final String SO_STORE_NAME_SPLIT = "lib-";

  private static final String[] DEFAULT_DENY_LIST =
      new String[] {System.mapLibraryName("breakpad")};

  /** Enable the exopackage SoSource. */
  public static final int SOLOADER_ENABLE_EXOPACKAGE = (1 << 0);

  /**
   * Allow deferring some initialization work to asynchronous background threads. Shared libraries
   * are nevertheless ready to load as soon as init returns.
   */
  public static final int SOLOADER_ALLOW_ASYNC_INIT = (1 << 1);

  public static final int SOLOADER_LOOK_IN_ZIP = (1 << 2);

  /**
   * In some contexts, using a backup so source in case of so corruption is not feasible e.g. lack
   * of write permissions to the library path.
   */
  public static final int SOLOADER_DISABLE_BACKUP_SOSOURCE = (1 << 3);

  /**
   * Skip calling JNI_OnLoad if the library is merged. This is necessary for libraries that don't
   * define JNI_OnLoad and are only loaded for their side effects (like static constructors
   * registering callbacks). DO NOT use this to allow implicit JNI registration (by naming your
   * methods Java_com_facebook_whatever) because that is buggy on Android.
   */
  public static final int SOLOADER_SKIP_MERGED_JNI_ONLOAD = (1 << 4);

  /** System Apps ignore PREFER_ANDROID_LIBS_DIRECTORY. Don't do that for this app. */
  public static final int SOLOADER_DONT_TREAT_AS_SYSTEMAPP = (1 << 5);

  /**
   * In API level 23 and above, it’s possible to open a .so file directly from your APK. Enabling
   * this flag will explicitly add the direct SoSource in soSource list.
   */
  public static final int SOLOADER_ENABLE_DIRECT_SOSOURCE = (1 << 6);

  /**
   * For compatibility, we need explicitly enable the backup soSource. This flag conflicts with
   * {@link #SOLOADER_DISABLE_BACKUP_SOSOURCE}, you should only set one of them or none.
   */
  public static final int SOLOADER_EXPLICITLY_ENABLE_BACKUP_SOSOURCE = (1 << 7);

  @GuardedBy("sSoSourcesLock")
  private static int sFlags;

  interface AppType {
    public static final int UNSET = 0;

    /** Normal user installed APP */
    public static final int THIRD_PARTY_APP = 1;

    /** The APP is installed in the device's system image. {@link ApplicationInfo#FLAG_SYSTEM} */
    public static final int SYSTEM_APP = 2;

    /**
     * The APP has been installed as an update to a built-in system application. {@link
     * ApplicationInfo#FLAG_UPDATED_SYSTEM_APP}
     */
    public static final int UPDATED_SYSTEM_APP = 3;
  }

  private static int sAppType = AppType.UNSET;

  static {
    boolean shouldSystrace = false;
    try {
      shouldSystrace = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2;
    } catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
      // In some test contexts, the Build class and/or some of its dependencies do not exist.
    }

    SYSTRACE_LIBRARY_LOADING = shouldSystrace;
  }

  public static void init(Context context, int flags) throws IOException {
    init(context, flags, null, DEFAULT_DENY_LIST);
  }

  public static void init(Context context, int flags, @Nullable SoFileLoader soFileLoader)
      throws IOException {
    init(context, flags, soFileLoader, DEFAULT_DENY_LIST);
  }

  /**
   * Initializes native code loading for this app; this class's other static facilities cannot be
   * used until this {@link #init} is called. This method is idempotent: calls after the first are
   * ignored.
   *
   * @param context application context
   * @param flags Zero or more of the SOLOADER_* flags
   * @param soFileLoader the custom {@link SoFileLoader}, you can implement your own loader
   * @param denyList Skip load libs from system soSource, due to the linker namespace restriction
   */
  public static void init(
      Context context, int flags, @Nullable SoFileLoader soFileLoader, String[] denyList)
      throws IOException {
    StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
    try {
      sAppType = getAppType(context, flags);
      if ((flags & SOLOADER_EXPLICITLY_ENABLE_BACKUP_SOSOURCE) == 0
          && SysUtil.isSupportedDirectLoad(context, sAppType)) {
        // SoLoader doesn't need backup soSource if it supports directly loading .so file from APK
        flags |= (SOLOADER_DISABLE_BACKUP_SOSOURCE | SOLOADER_ENABLE_DIRECT_SOSOURCE);
      }

      initSoLoader(soFileLoader);
      initSoSources(context, flags, denyList);
      NativeLoader.initIfUninitialized(new NativeLoaderToSoLoaderDelegate());
    } finally {
      StrictMode.setThreadPolicy(oldPolicy);
    }
  }

  /**
   * Backward compatibility.
   *
   * @param context application context
   * @param nativeExopackage pass {@link #SOLOADER_ENABLE_EXOPACKAGE} as flags if true
   * @see <a href="https://buck.build/article/exopackage.html">What is exopackage?</a>
   */
  public static void init(Context context, boolean nativeExopackage) {
    try {
      init(context, nativeExopackage ? SOLOADER_ENABLE_EXOPACKAGE : 0, null, DEFAULT_DENY_LIST);
    } catch (IOException ex) {
      throw new RuntimeException(ex);
    }
  }

  private static void initSoSources(Context context, int flags, String[] denyList)
      throws IOException {
    if (sSoSources != null) {
      return;
    }

    sSoSourcesLock.writeLock().lock();
    try {
      sFlags = flags;

      ArrayList<SoSource> soSources = new ArrayList<>();
      AddSystemLibSoSource(soSources, denyList);

      //
      // We can only proceed forward if we have a Context. The prominent case
      // where we don't have a Context is barebones dalvikvm instantiations. In
      // that case, the caller is responsible for providing a correct LD_LIBRARY_PATH.
      //

      if (context != null) {
        //
        // Prepend our own SoSource for our own DSOs.
        //

        if ((flags & SOLOADER_ENABLE_EXOPACKAGE) != 0) {
          sBackupSoSources = null;
          if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "adding exo package source: " + SO_STORE_NAME_MAIN);
          }
          soSources.add(0, new ExoSoSource(context, SO_STORE_NAME_MAIN));
        } else {
          if ((flags & SOLOADER_ENABLE_DIRECT_SOSOURCE) != 0) {
            addDirectApkSoSource(context, soSources);
          }
          addApplicationSoSource(context, soSources, getApplicationSoSourceFlags());
          AddBackupSoSource(context, soSources, ApkSoSource.PREFER_ANDROID_LIBS_DIRECTORY);
        }
      }

      SoSource[] finalSoSources = soSources.toArray(new SoSource[soSources.size()]);
      int prepareFlags = makePrepareFlags();
      for (int i = finalSoSources.length; i-- > 0; ) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
          Log.d(TAG, "Preparing SO source: " + finalSoSources[i]);
        }
        finalSoSources[i].prepare(prepareFlags);
      }
      sSoSources = finalSoSources;
      sSoSourcesVersion.getAndIncrement();
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "init finish: " + sSoSources.length + " SO sources prepared");
      }
    } finally {
      sSoSourcesLock.writeLock().unlock();
    }
  }

  private static int getApplicationSoSourceFlags() {
    switch (sAppType) {
      case AppType.THIRD_PARTY_APP:
        return 0;
      case AppType.SYSTEM_APP:
      case AppType.UPDATED_SYSTEM_APP:
        return DirectorySoSource.RESOLVE_DEPENDENCIES;
      default:
        throw new RuntimeException("Unsupported app type, we should not reach here");
    }
  }

  /** Add DirectApk SoSources for disabling android:extractNativeLibs */
  private static void addDirectApkSoSource(Context context, ArrayList<SoSource> soSources) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
        && context.getApplicationInfo().splitSourceDirs != null) {
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "adding directApk sources from split apks");
      }
      for (String splitApkDir : context.getApplicationInfo().splitSourceDirs) {
        DirectApkSoSource splitApkSource = new DirectApkSoSource(new File(splitApkDir));
        if (Log.isLoggable(TAG, Log.DEBUG)) {
          Log.d(TAG, "adding directApk source: " + splitApkSource.toString());
        }
        soSources.add(0, splitApkSource);
      }
    }

    DirectApkSoSource directApkSoSource = new DirectApkSoSource(context);
    if (Log.isLoggable(TAG, Log.DEBUG)) {
      Log.d(TAG, "adding directApk source: " + directApkSoSource.toString());
    }
    soSources.add(0, directApkSoSource);
  }

  /** Add a DirectorySoSource for the application's nativeLibraryDir . */
  private static void addApplicationSoSource(
      Context context, ArrayList<SoSource> soSources, int flags) {

    // On old versions of Android, Bionic doesn't add our library directory to its
    // internal search path, and the system doesn't resolve dependencies between
    // modules we ship. On these systems, we resolve dependencies ourselves. On other
    // systems, Bionic's built-in resolver suffices.

    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1) {
      flags |= DirectorySoSource.RESOLVE_DEPENDENCIES;
    }

    sApplicationSoSource = new ApplicationSoSource(context, flags);
    if (Log.isLoggable(TAG, Log.DEBUG)) {
      Log.d(TAG, "adding application source: " + sApplicationSoSource.toString());
    }
    soSources.add(0, sApplicationSoSource);
  }

  /** Add the SoSources for recovering the dso if the file is corrupted or missed */
  private static void AddBackupSoSource(
      Context context, ArrayList<SoSource> soSources, int apkSoSourceFlags) throws IOException {
    if ((sFlags & SOLOADER_DISABLE_BACKUP_SOSOURCE) != 0) {
      sBackupSoSources = null;
      // Clean up backups
      final File backupDir = UnpackingSoSource.getSoStorePath(context, SO_STORE_NAME_MAIN);
      try {
        SysUtil.dumbDeleteRecursive(backupDir);
      } catch (IOException e) {
        Log.w(TAG, "Failed to delete " + backupDir.getCanonicalPath(), e);
      }
      return;
    }

    final File mainApkDir = new File(context.getApplicationInfo().sourceDir);
    ArrayList<UnpackingSoSource> backupSources = new ArrayList<>();
    ApkSoSource mainApkSource =
        new ApkSoSource(context, mainApkDir, SO_STORE_NAME_MAIN, apkSoSourceFlags);
    backupSources.add(mainApkSource);
    if (Log.isLoggable(TAG, Log.DEBUG)) {
      Log.d(TAG, "adding backup source from : " + mainApkSource.toString());
    }

    addBackupSoSourceFromSplitApk(context, apkSoSourceFlags, backupSources);

    sBackupSoSources = backupSources.toArray(new UnpackingSoSource[backupSources.size()]);
    soSources.addAll(0, backupSources);
  }

  private static void addBackupSoSourceFromSplitApk(
      Context context, int apkSoSourceFlags, ArrayList<UnpackingSoSource> backupSources) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
        && context.getApplicationInfo().splitSourceDirs != null) {
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "adding backup sources from split apks");
      }
      int splitIndex = 0;
      for (String splitApkDir : context.getApplicationInfo().splitSourceDirs) {
        ApkSoSource splitApkSource =
            new ApkSoSource(
                context,
                new File(splitApkDir),
                SO_STORE_NAME_SPLIT + (splitIndex++),
                apkSoSourceFlags);
        if (Log.isLoggable(TAG, Log.DEBUG)) {
          Log.d(TAG, "adding backup source: " + splitApkSource.toString());
        }
        backupSources.add(splitApkSource);
      }
    }
  }

  /**
   * Add SoSource objects for each of the system library directories.
   *
   * @param soSources target soSource list
   * @param denyList Skip load libs from current soSource, due to the linker namespace restriction
   */
  private static void AddSystemLibSoSource(ArrayList<SoSource> soSources, String[] denyList) {
    String systemLibPaths =
        SysUtil.is64Bit() ? "/system/lib64:/vendor/lib64" : "/system/lib:/vendor/lib";

    String LD_LIBRARY_PATH = System.getenv("LD_LIBRARY_PATH");
    if (LD_LIBRARY_PATH != null && !LD_LIBRARY_PATH.equals("")) {
      systemLibPaths += ":" + LD_LIBRARY_PATH;
    }

    final Set<String> libPathSet = new HashSet<>(Arrays.asList(systemLibPaths.split(":")));
    for (String libPath : libPathSet) {
      // Don't pass DirectorySoSource.RESOLVE_DEPENDENCIES for directories we find on
      // LD_LIBRARY_PATH: Bionic's dynamic linker is capable of correctly resolving dependencies
      // these libraries have on each other, so doing that ourselves would be a waste.
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "adding system library source: " + libPath);
      }
      File systemSoDirectory = new File(libPath);
      soSources.add(
          new DirectorySoSource(systemSoDirectory, DirectorySoSource.ON_LD_LIBRARY_PATH, denyList));
    }
  }

  private static int makePrepareFlags() {
    int prepareFlags = 0;
    // ensure the write lock is being held to protect sFlags
    // this is used when preparing new SoSources in the list.
    sSoSourcesLock.writeLock().lock();
    try {
      if ((sFlags & SOLOADER_ALLOW_ASYNC_INIT) != 0) {
        prepareFlags |= SoSource.PREPARE_FLAG_ALLOW_ASYNC_INIT;
      }
      return prepareFlags;
    } finally {
      sSoSourcesLock.writeLock().unlock();
    }
  }

  private static synchronized void initSoLoader(@Nullable SoFileLoader soFileLoader) {
    if (soFileLoader == null && sSoFileLoader != null) {
      return;
    }
    if (soFileLoader != null) {
      sSoFileLoader = soFileLoader;
      return;
    }

    final Runtime runtime = Runtime.getRuntime();
    final Method nativeLoadRuntimeMethod = getNativeLoadRuntimeMethod();

    final boolean hasNativeLoadMethod = nativeLoadRuntimeMethod != null;

    final String localLdLibraryPath =
        hasNativeLoadMethod ? Api14Utils.getClassLoaderLdLoadLibrary() : null;
    final String localLdLibraryPathNoZips = makeNonZipPath(localLdLibraryPath);

    sSoFileLoader =
        new SoFileLoader() {
          @Override
          public void loadBytes(String pathName, ElfByteChannel bytes, int loadFlags) {
            throw new UnsupportedOperationException();
          }

          @Override
          public void load(final String pathToSoFile, final int loadFlags) {
            String error = null;
            if (hasNativeLoadMethod) {
              final boolean inZip = (loadFlags & SOLOADER_LOOK_IN_ZIP) == SOLOADER_LOOK_IN_ZIP;
              final String path = inZip ? localLdLibraryPath : localLdLibraryPathNoZips;
              try {
                synchronized (runtime) {
                  error =
                      (String)
                          nativeLoadRuntimeMethod.invoke(
                              runtime, pathToSoFile, SoLoader.class.getClassLoader(), path);
                  if (error != null) {
                    throw new UnsatisfiedLinkError(error);
                  }
                }
              } catch (IllegalAccessException
                  | IllegalArgumentException
                  | InvocationTargetException e) {
                error = "Error: Cannot load " + pathToSoFile;
                throw new RuntimeException(error, e);
              } finally {
                if (error != null) {
                  Log.e(
                      TAG,
                      "Error when loading lib: "
                          + error
                          + " lib hash: "
                          + getLibHash(pathToSoFile)
                          + " search path is "
                          + path);
                }
              }
            } else {
              System.load(pathToSoFile);
            }
          }

          /** * Logs MD5 of lib that failed loading */
          private String getLibHash(String libPath) {
            String digestStr;
            try {
              File libFile = new File(libPath);
              MessageDigest digest = MessageDigest.getInstance("MD5");
              try (InputStream libInStream = new FileInputStream(libFile)) {
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = libInStream.read(buffer)) > 0) {
                  digest.update(buffer, 0, bytesRead);
                }
                digestStr = String.format("%32x", new BigInteger(1, digest.digest()));
              }
            } catch (IOException | SecurityException | NoSuchAlgorithmException e) {
              digestStr = e.toString();
            }
            return digestStr;
          }
        };
  }

  private static @Nullable Method getNativeLoadRuntimeMethod() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Build.VERSION.SDK_INT > 27) {
      return null;
    }

    try {
      final Method method =
          Runtime.class.getDeclaredMethod(
              "nativeLoad", String.class, ClassLoader.class, String.class);
      method.setAccessible(true);
      return method;
    } catch (final NoSuchMethodException | SecurityException e) {
      Log.w(TAG, "Cannot get nativeLoad method", e);
      return null;
    }
  }

  private static int getAppType(Context context, int flags) {
    if (sAppType != AppType.UNSET) {
      return sAppType;
    }
    if ((flags & SOLOADER_DONT_TREAT_AS_SYSTEMAPP) != 0 || context == null) {
      return AppType.THIRD_PARTY_APP;
    }

    final int type;
    final ApplicationInfo appInfo = context.getApplicationInfo();

    if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
      type = AppType.THIRD_PARTY_APP;
    } else if ((appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) {
      type = AppType.UPDATED_SYSTEM_APP;
    } else {
      type = AppType.SYSTEM_APP;
    }
    if (Log.isLoggable(TAG, Log.DEBUG)) {
      Log.d(TAG, "ApplicationInfo.flags is: " + appInfo.flags + " appType is: " + type);
    }
    return type;
  }

  /** Turn shared-library loading into a no-op. Useful in special circumstances. */
  public static void setInTestMode() {
    TestOnlyUtils.setSoSources(new SoSource[] {new NoopSoSource()});
  }

  /** Make shared-library loading delegate to the system. Useful for tests. */
  public static void deinitForTest() {
    TestOnlyUtils.setSoSources(null);
  }

  @NotThreadSafe
  static class TestOnlyUtils {
    /* Set so sources. Useful for tests. */
    /* package */ static void setSoSources(SoSource[] sources) {
      sSoSourcesLock.writeLock().lock();
      try {
        sSoSources = sources;
        sSoSourcesVersion.getAndIncrement();
      } finally {
        sSoSourcesLock.writeLock().unlock();
      }
    }

    /**
     * Set so file loader. <br>
     * N.B. <b>ONLY FOR TESTS</b>. It has read/write race with {@code
     * SoLoader.sSoFileLoader.load(String, int)} in {@link DirectorySoSource#loadLibraryFrom}
     *
     * @param loader {@link SoFileLoader}
     */
    /* package */ static void setSoFileLoader(SoFileLoader loader) {
      sSoFileLoader = loader;
    }

    /** Reset internal status. Only for tests. */
    /* package */ static void resetStatus() {
      synchronized (SoLoader.class) {
        sLoadedLibraries.clear();
        sLoadingLibraries.clear();
        sSoFileLoader = null;
      }
      setSoSources(null);
    }
  }

  /**
   * Provide a wrapper object for calling {@link System#loadLibrary}. This is useful for controlling
   * which ClassLoader libraries are loaded into.
   */
  public static void setSystemLoadLibraryWrapper(SystemLoadLibraryWrapper wrapper) {
    sSystemLoadLibraryWrapper = wrapper;
  }

  public static final class WrongAbiError extends UnsatisfiedLinkError {
    WrongAbiError(Throwable cause, String machine) {
      super(
          "APK was built for a different platform. Supported ABIs: "
              + Arrays.toString(SysUtil.getSupportedAbis())
              + " error: "
              + machine);
      initCause(cause);
    }
  }

  /**
   * Gets the full path of a library.
   *
   * @param libName the library file name, including the prefix and extension.
   * @return the full path of the library, or null if it is not found in none of the SoSources.
   * @throws IOException if there is an error calculating the canonical path of libName
   */
  public static @Nullable String getLibraryPath(String libName) throws IOException {
    String libPath = null;

    sSoSourcesLock.readLock().lock();
    try {
      if (sSoSources != null) {
        for (int i = 0; libPath == null && i < sSoSources.length; ++i) {
          SoSource currentSource = sSoSources[i];
          libPath = currentSource.getLibraryPath(libName);
        }
      }
    } finally {
      sSoSourcesLock.readLock().unlock();
    }

    return libPath;
  }

  /**
   * Gets the dependencies of a library.
   *
   * @param libName the library file name, including the prefix and extension.
   * @return An array naming the dependencies of the library, or null if it is not found in any
   *     SoSources
   * @throws IOException if there is an error reading libName
   */
  public static @Nullable String[] getLibraryDependencies(String libName) throws IOException {
    String[] deps = null;

    sSoSourcesLock.readLock().lock();
    try {
      if (sSoSources != null) {
        for (int i = 0; deps == null && i < sSoSources.length; ++i) {
          SoSource currentSource = sSoSources[i];
          deps = currentSource.getLibraryDependencies(libName);
        }
      }
    } finally {
      sSoSourcesLock.readLock().unlock();
    }

    return deps;
  }

  /**
   * Returns the so file for the specified library. Returns null if the library does not exist or if
   * it's not backed by a file.
   */
  public static @Nullable File getSoFile(String shortName) {
    String mergedLibName = MergedSoMapping.mapLibName(shortName);
    String soName = mergedLibName != null ? mergedLibName : shortName;
    String mappedName = System.mapLibraryName(soName);

    sSoSourcesLock.readLock().lock();
    try {
      if (sSoSources != null) {
        for (int i = 0; i < sSoSources.length; ++i) {
          SoSource currentSource = sSoSources[i];
          try {
            File soFile = currentSource.getSoFileByName(mappedName);
            if (soFile != null) {
              return soFile;
            }
          } catch (IOException e) {
            // Failed to get the file, let's skip this so source.
          }
        }
      }
    } finally {
      sSoSourcesLock.readLock().unlock();
    }

    return null;
  }

  public static boolean loadLibrary(String shortName) {
    return loadLibrary(shortName, 0);
  }

  /**
   * Load a shared library, initializing any JNI binding it contains.
   *
   * @param shortName Name of library to find, without "lib" prefix or ".so" suffix
   * @param loadFlags Control flags for the loading behavior. See available flags under {@link
   *     SoSource} (LOAD_FLAG_XXX).
   * @return Whether the library was loaded as a result of this call (true), or was already loaded
   *     through a previous call to SoLoader (false).
   */
  public static boolean loadLibrary(String shortName, int loadFlags) throws UnsatisfiedLinkError {
    Boolean needsLoad = loadLibraryOnNonAndroid(shortName);
    if (needsLoad != null) {
      return needsLoad;
    }

    // This is to account for the fact that we want to load .so files from the apk itself when it is
    // a system app.
    if ((sAppType == AppType.SYSTEM_APP || sAppType == AppType.UPDATED_SYSTEM_APP)
        && sSystemLoadLibraryWrapper != null) {
      sSystemLoadLibraryWrapper.loadLibrary(shortName);
      return true;
    }

    String mergedLibName = MergedSoMapping.mapLibName(shortName);

    String soName = mergedLibName != null ? mergedLibName : shortName;

    return loadLibraryBySoName(
        System.mapLibraryName(soName), shortName, mergedLibName, loadFlags, null);
  }

  private static @Nullable Boolean loadLibraryOnNonAndroid(String shortName) {
    if (sSoSources == null) {
      sSoSourcesLock.readLock().lock();
      try {
        if (sSoSources == null) {
          // This should never happen during normal operation,
          // but if we're running in a non-Android environment,
          // fall back to System.loadLibrary.
          if ("http://www.android.com/".equals(System.getProperty("java.vendor.url"))) {
            // This will throw.
            assertInitialized();
          } else {
            // Not on an Android system.  Ask the JVM to load for us.
            synchronized (SoLoader.class) {
              boolean needsLoad = !sLoadedLibraries.contains(shortName);
              if (needsLoad) {
                if (sSystemLoadLibraryWrapper != null) {
                  sSystemLoadLibraryWrapper.loadLibrary(shortName);
                } else {
                  System.loadLibrary(shortName);
                }
              }
              return needsLoad;
            }
          }
        }
      } finally {
        sSoSourcesLock.readLock().unlock();
      }
    }
    return null;
  }

  /* package */ static void loadLibraryBySoName(
      String soName, int loadFlags, StrictMode.ThreadPolicy oldPolicy) {
    loadLibraryBySoNameImpl(soName, null, null, loadFlags, oldPolicy);
  }

  private static boolean loadLibraryBySoName(
      String soName,
      @Nullable String shortName,
      @Nullable String mergedLibName,
      int loadFlags,
      @Nullable StrictMode.ThreadPolicy oldPolicy) {
    boolean ret = false;
    boolean retry;
    do {
      retry = false;
      try {
        ret = loadLibraryBySoNameImpl(soName, shortName, mergedLibName, loadFlags, oldPolicy);
      } catch (UnsatisfiedLinkError e) {
        final int currentVersion = sSoSourcesVersion.get();
        sSoSourcesLock.writeLock().lock();
        try {
          if (sApplicationSoSource != null && sApplicationSoSource.checkAndMaybeUpdate()) {
            Log.w(
                TAG,
                "sApplicationSoSource updated during load: " + soName + ", attempting load again.");
            sSoSourcesVersion.getAndIncrement();
            retry = true;
          }
        } catch (IOException ex) {
          throw new RuntimeException(ex);
        } finally {
          sSoSourcesLock.writeLock().unlock();
        }

        if (sSoSourcesVersion.get() == currentVersion) {
          // nothing changed in soSource, Propagate original error
          throw e;
        }
      }
    } while (retry);
    return ret;
  }

  private static boolean loadLibraryBySoNameImpl(
      String soName,
      @Nullable String shortName,
      @Nullable String mergedLibName,
      int loadFlags,
      @Nullable StrictMode.ThreadPolicy oldPolicy) {
    // As an optimization, avoid taking locks if the library has already loaded. Without locks this
    // does not provide 100% coverage (e.g. another thread may currently be loading or initializing
    // the library), so we'll need to check again with locks held, below.
    if (!TextUtils.isEmpty(shortName) && sLoadedAndMergedLibraries.contains(shortName)) {
      return false;
    }

    // LoadingLibLock is used to ensure that doLoadLibraryBySoName and its corresponding JniOnload
    // are only executed once per library. It also guarantees that concurrent calls to loadLibrary
    // for the same library do not return until both its load and JniOnLoad have completed.
    Object loadingLibLock;
    boolean loaded = false;
    synchronized (SoLoader.class) {
      if (sLoadedLibraries.contains(soName)) {
        if (mergedLibName == null) {
          // Not a merged lib, no need to init
          return false;
        }
        loaded = true;
      }
      if (sLoadingLibraries.containsKey(soName)) {
        loadingLibLock = sLoadingLibraries.get(soName);
      } else {
        loadingLibLock = new Object();
        sLoadingLibraries.put(soName, loadingLibLock);
      }
    }

    // Note that both doLoadLibraryBySoName and invokeJniOnload (below) may re-enter loadLibrary
    // (or loadLibraryBySoNameImpl), recursively acquiring additional library and soSource locks.
    //
    // To avoid bi-directional lock usage (threadA takes loadingLibLock then sSoSourcesLock, threadB
    // takes sSoSourcesLock then loadingLibLock) and potential deadlock as this method recursively
    // calls loadLibrary, we must acquire sSoSourcesLock first.
    sSoSourcesLock.readLock().lock();
    try {
      synchronized (loadingLibLock) {
        if (!loaded) {
          synchronized (SoLoader.class) {
            if (sLoadedLibraries.contains(soName)) {
              // Library was successfully loaded by other thread while we waited
              if (mergedLibName == null) {
                // Not a merged lib, no need to init
                return false;
              }
              loaded = true;
            }
            // Else, load was not successful on other thread. We will try in this one.
          }

          if (!loaded) {
            try {
              if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "About to load: " + soName);
              }
              doLoadLibraryBySoName(soName, loadFlags, oldPolicy);
            } catch (UnsatisfiedLinkError ex) {
              String message = ex.getMessage();
              if (message != null && message.contains("unexpected e_machine:")) {
                String machine_msg =
                    message.substring(message.lastIndexOf("unexpected e_machine:"));
                throw new WrongAbiError(ex, machine_msg);
              }
              throw ex;
            }
            if (Log.isLoggable(TAG, Log.DEBUG)) {
              Log.d(TAG, "Loaded: " + soName);
            }
            synchronized (SoLoader.class) {
              sLoadedLibraries.add(soName);
            }
          }
        }

        if ((loadFlags & SOLOADER_SKIP_MERGED_JNI_ONLOAD) == 0) {
          // MergedSoMapping#invokeJniOnload does not necessarily handle concurrent nor redundant
          // invocation. sLoadedAndMergedLibraries is used in conjunction with loadingLibLock to
          // ensure one invocation per library.
          boolean isAlreadyMerged =
              !TextUtils.isEmpty(shortName) && sLoadedAndMergedLibraries.contains(shortName);
          if (mergedLibName != null && !isAlreadyMerged) {
            if (SYSTRACE_LIBRARY_LOADING) {
              Api18TraceUtils.beginTraceSection("MergedSoMapping.invokeJniOnload[", shortName, "]");
            }
            try {
              if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "About to merge: " + shortName + " / " + soName);
              }
              MergedSoMapping.invokeJniOnload(shortName);
              sLoadedAndMergedLibraries.add(shortName);
            } catch (UnsatisfiedLinkError e) {
              // If you are seeing this exception, first make sure your library sets
              // allow_jni_merging=True.  Trying to merge a library without that
              // will trigger this error.  If that's already in place, you're probably
              // not defining JNI_OnLoad.  Calling SoLoader.loadLibrary on a library
              // that doesn't define JNI_OnLoad is a no-op when that library is not merged.
              // Once you enable merging, it throws an UnsatisfiedLinkError.
              // There are three main reasons a library might not define JNI_OnLoad,
              // and the solution depends on which case you have.
              // - You might be using implicit registration (native methods defined like
              //   `Java_com_facebook_Foo_bar(JNIEnv* env)`).  This is not safe on Android
              //   https://fb.workplace.com/groups/442333009148653/permalink/651212928260659/
              //   and is not compatible with FBJNI.  Stop doing it.  Use FBJNI registerNatives.
              // - You might have a C++-only library with no JNI bindings and no static
              //   initializers with side-effects.  You can just delete the loadLibrary call.
              // - You might have a C++-only library that needs to be loaded explicitly because
              //   it has static initializers whose side-effects are needed.  In that case,
              //   pass the SOLOADER_SKIP_MERGED_JNI_ONLOAD flag to loadLibrary.
              throw new RuntimeException(
                  "Failed to call JNI_OnLoad from '"
                      + shortName
                      + "', which has been merged into '"
                      + soName
                      + "'.  See comment for details.",
                  e);
            } finally {
              if (SYSTRACE_LIBRARY_LOADING) {
                Api18TraceUtils.endSection();
              }
            }
          }
        }
      }
    } finally {
      sSoSourcesLock.readLock().unlock();
    }
    return !loaded;
  }

  /**
   * Unpack library and its dependencies, returning the location of the unpacked library file. All
   * non-system dependencies of the given library will either be on LD_LIBRARY_PATH or will be in
   * the same directory as the returned File.
   *
   * @param shortName Name of library to find, without "lib" prefix or ".so" suffix
   * @return Unpacked DSO location
   */
  public static File unpackLibraryAndDependencies(String shortName) throws UnsatisfiedLinkError {
    assertInitialized();
    try {
      return unpackLibraryBySoName(System.mapLibraryName(shortName));
    } catch (IOException ex) {
      throw new RuntimeException(ex);
    }
  }

  private static void doLoadLibraryBySoName(
      String soName, int loadFlags, @Nullable StrictMode.ThreadPolicy oldPolicy)
      throws UnsatisfiedLinkError {

    int result = SoSource.LOAD_RESULT_NOT_FOUND;
    sSoSourcesLock.readLock().lock();
    try {
      if (sSoSources == null) {
        Log.e(TAG, "Could not load: " + soName + " because no SO source exists");
        throw new UnsatisfiedLinkError("couldn't find DSO to load: " + soName);
      }
    } finally {
      sSoSourcesLock.readLock().unlock();
    }

    // This way, we set the thread policy only one per loadLibrary no matter how many
    // dependencies we load.  Each call to StrictMode.allowThreadDiskWrites allocates.
    boolean restoreOldPolicy = false;
    if (oldPolicy == null) {
      oldPolicy = StrictMode.allowThreadDiskReads();
      restoreOldPolicy = true;
    }

    if (SYSTRACE_LIBRARY_LOADING) {
      Api18TraceUtils.beginTraceSection("SoLoader.loadLibrary[", soName, "]");
    }

    Throwable error = null;
    try {
      sSoSourcesLock.readLock().lock();
      try {
        for (int i = 0; result == SoSource.LOAD_RESULT_NOT_FOUND && i < sSoSources.length; ++i) {
          SoSource currentSource = sSoSources[i];
          result = currentSource.loadLibrary(soName, loadFlags, oldPolicy);
          if (result == SoSource.LOAD_RESULT_CORRUPTED_LIB_FILE && sBackupSoSources != null) {
            // Let's try from the backup source
            if (Log.isLoggable(TAG, Log.DEBUG)) {
              Log.d(TAG, "Trying backup SoSource for " + soName);
            }
            for (UnpackingSoSource backupSoSource : sBackupSoSources) {
              backupSoSource.prepare(soName);
              int resultFromBackup = backupSoSource.loadLibrary(soName, loadFlags, oldPolicy);
              if (resultFromBackup == SoSource.LOAD_RESULT_LOADED) {
                result = resultFromBackup;
                break;
              }
            }
            break;
          }
        }
      } finally {
        sSoSourcesLock.readLock().unlock();
      }
    } catch (Throwable t) {
      error = t;
    } finally {
      if (SYSTRACE_LIBRARY_LOADING) {
        Api18TraceUtils.endSection();
      }

      if (restoreOldPolicy) {
        StrictMode.setThreadPolicy(oldPolicy);
      }
      if (result == SoSource.LOAD_RESULT_NOT_FOUND
          || result == SoSource.LOAD_RESULT_CORRUPTED_LIB_FILE) {
        StringBuilder sb = new StringBuilder().append("couldn't find DSO to load: ").append(soName);
        if (error != null) {
          String cause = error.getMessage();
          if (cause == null) {
            cause = error.toString();
          }
          sb.append(" caused by: ").append(cause);
          error.printStackTrace();
        } else {
          // load failure wasn't caused by dependent libraries.
          // Print the sources and current native library directory
          sSoSourcesLock.readLock().lock();
          for (int i = 0; i < sSoSources.length; ++i) {
            sb.append("\n\tSoSource ").append(i).append(": ").append(sSoSources[i].toString());
          }
          if (sApplicationSoSource != null) {
            Context updatedContext = sApplicationSoSource.getUpdatedContext();
            File updatedNativeLibDir =
                ApplicationSoSource.getNativeLibDirFromContext(updatedContext);
            sb.append("\n\tNative lib dir: ")
                .append(updatedNativeLibDir.getAbsolutePath())
                .append("\n");
          }
          sSoSourcesLock.readLock().unlock();
        }
        sb.append(" result: ").append(result);
        final String message = sb.toString();
        Log.e(TAG, message);
        UnsatisfiedLinkError err = new UnsatisfiedLinkError(message);
        if (error != null) {
          err.initCause(error);
        }
        throw err;
      }
    }
  }

  @Nullable
  public static String makeNonZipPath(final String localLdLibraryPath) {
    if (localLdLibraryPath == null) {
      return null;
    }

    final String[] paths = localLdLibraryPath.split(":");
    final ArrayList<String> pathsWithoutZip = new ArrayList<String>(paths.length);
    for (final String path : paths) {
      if (path.contains("!")) {
        continue;
      }
      pathsWithoutZip.add(path);
    }

    return TextUtils.join(":", pathsWithoutZip);
  }

  /* package */ static File unpackLibraryBySoName(String soName) throws IOException {
    sSoSourcesLock.readLock().lock();
    try {
      for (SoSource soSource : sSoSources) {
        File unpacked = soSource.unpackLibrary(soName);
        if (unpacked != null) {
          return unpacked;
        }
      }
    } finally {
      sSoSourcesLock.readLock().unlock();
    }

    throw new FileNotFoundException(soName);
  }

  private static void assertInitialized() {
    if (!isInitialized()) {
      throw new IllegalStateException("SoLoader.init() not yet called");
    }
  }

  public static boolean isInitialized() {
    sSoSourcesLock.readLock().lock();
    try {
      return sSoSources != null;
    } finally {
      sSoSourcesLock.readLock().unlock();
    }
  }

  public static int getSoSourcesVersion() {
    return sSoSourcesVersion.get();
  }

  /**
   * Add a new source of native libraries. SoLoader consults the new source before any
   * currently-installed source.
   *
   * @param extraSoSource The SoSource to install
   */
  public static void prependSoSource(SoSource extraSoSource) throws IOException {
    sSoSourcesLock.writeLock().lock();
    try {
      assertInitialized();
      extraSoSource.prepare(makePrepareFlags());
      SoSource[] newSoSources = new SoSource[sSoSources.length + 1];
      newSoSources[0] = extraSoSource;
      System.arraycopy(sSoSources, 0, newSoSources, 1, sSoSources.length);
      sSoSources = newSoSources;
      sSoSourcesVersion.getAndIncrement();
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "Prepended to SO sources: " + extraSoSource);
      }
    } finally {
      sSoSourcesLock.writeLock().unlock();
    }
  }

  /**
   * Retrieve an LD_LIBRARY_PATH value suitable for using the native linker to resolve our shared
   * libraries.
   */
  public static String makeLdLibraryPath() {
    sSoSourcesLock.readLock().lock();
    try {
      assertInitialized();
      ArrayList<String> pathElements = new ArrayList<>();
      SoSource[] soSources = sSoSources;
      if (soSources != null) {
        for (SoSource soSource : soSources) {
          soSource.addToLdLibraryPath(pathElements);
        }
      }
      String joinedPaths = TextUtils.join(":", pathElements);
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "makeLdLibraryPath final path: " + joinedPaths);
      }
      return joinedPaths;
    } finally {
      sSoSourcesLock.readLock().unlock();
    }
  }

  /**
   * This function ensure that every SoSources Abi is supported for at least one abi in
   * SysUtil.getSupportedAbis
   *
   * @return true if all SoSources have their Abis supported
   */
  public static boolean areSoSourcesAbisSupported() {
    sSoSourcesLock.readLock().lock();
    try {
      if (sSoSources == null) {
        return false;
      }

      String[] supportedAbis = SysUtil.getSupportedAbis();
      for (SoSource soSource : sSoSources) {
        String[] soSourceAbis = soSource.getSoSourceAbis();
        for (String soSourceAbi : soSourceAbis) {
          boolean soSourceSupported = false;
          for (int k = 0; k < supportedAbis.length && !soSourceSupported; ++k) {
            soSourceSupported = soSourceAbi.equals(supportedAbis[k]);
          }
          if (!soSourceSupported) {
            Log.e(TAG, "abi not supported: " + soSourceAbi);
            return false;
          }
        }
      }

      return true;
    } finally {
      sSoSourcesLock.readLock().unlock();
    }
  }

  public static boolean useDepsFile(Context context, String depsFilePath) throws IOException {
    File apkFile = new File(context.getApplicationInfo().sourceDir);
    byte[] apkId = SysUtil.makeApkDepBlock(apkFile, context);
    return NativeDeps.useDepsFile(apkId, depsFilePath);
  }

  @DoNotOptimize
  @TargetApi(14)
  /* package */ static class Api14Utils {
    public static String getClassLoaderLdLoadLibrary() {
      final ClassLoader classLoader = SoLoader.class.getClassLoader();

      if (classLoader != null && !(classLoader instanceof BaseDexClassLoader)) {
        throw new IllegalStateException(
            "ClassLoader "
                + classLoader.getClass().getName()
                + " should be of type BaseDexClassLoader");
      }
      try {
        final BaseDexClassLoader baseDexClassLoader = (BaseDexClassLoader) classLoader;
        final Method getLdLibraryPathMethod =
            BaseDexClassLoader.class.getMethod("getLdLibraryPath");

        return (String) getLdLibraryPathMethod.invoke(baseDexClassLoader);
      } catch (Exception e) {
        throw new RuntimeException("Cannot call getLdLibraryPath", e);
      }
    }
  }
}
