content: /*
   * Copyright © 2017-2023 WireGuard LLC. All Rights Reserved.
   * SPDX-License-Identifier: Apache-2.0
   */
  
  package org.amnezia.awg.util;
  
  import android.content.Context;
  import android.system.OsConstants;
  import android.util.Log;
  
  import org.amnezia.awg.util.RootShell.RootShellException;
  import org.amnezia.awg.util.NonNullForAll;
  
  import java.io.File;
  import java.io.FileNotFoundException;
  import java.io.IOException;
  import java.util.Arrays;
  import java.util.List;
  
  import androidx.annotation.Nullable;
  import androidx.annotation.RestrictTo;
  import androidx.annotation.RestrictTo.Scope;
  
  /**
   * Helper to install AmneziaWG tools to the system partition.
   */
  
  @NonNullForAll
  public final class ToolsInstaller {
      public static final int ERROR = 0x0;
      public static final int MAGISK = 0x4;
      public static final int NO = 0x2;
      public static final int SYSTEM = 0x8;
      public static final int YES = 0x1;
      private static final String[] EXECUTABLES = {"awg", "awg-quick"};
      private static final File[] INSTALL_DIRS = {
              new File("/system/xbin"),
              new File("/system/bin"),
      };
      @Nullable private static final File INSTALL_DIR = getInstallDir();
      private static final String TAG = "AmneziaWG/ToolsInstaller";
  
      private final Context context;
      private final File localBinaryDir;
      private final Object lock = new Object();
      private final RootShell rootShell;
      @Nullable private Boolean areToolsAvailable;
      @Nullable private Boolean installAsMagiskModule;
  
      public ToolsInstaller(final Context context, final RootShell rootShell) {
          localBinaryDir = new File(context.getCodeCacheDir(), "bin");
          this.context = context;
          this.rootShell = rootShell;
      }
  
      @Nullable
      private static File getInstallDir() {
          final String path = System.getenv("PATH");
          if (path == null)
              return INSTALL_DIRS[0];
          final List<String> paths = Arrays.asList(path.split(":"));
          for (final File dir : INSTALL_DIRS) {
              if (paths.contains(dir.getPath()) && dir.isDirectory())
                  return dir;
          }
          return null;
      }
  
      public int areInstalled() throws RootShellException {
          if (INSTALL_DIR == null)
              return ERROR;
          final StringBuilder script = new StringBuilder();
          for (final String name : EXECUTABLES) {
              script.append(String.format("cmp -s '%s' '%s' && ",
                      new File(localBinaryDir, name).getAbsolutePath(),
                      new File(INSTALL_DIR, name).getAbsolutePath()));
          }
          script.append("exit ").append(OsConstants.EALREADY).append(';');
          try {
              final int ret = rootShell.run(null, script.toString());
              if (ret == OsConstants.EALREADY)
                  return willInstallAsMagiskModule() ? YES | MAGISK : YES | SYSTEM;
              else
                  return willInstallAsMagiskModule() ? NO | MAGISK : NO | SYSTEM;
          } catch (final IOException ignored) {
              return ERROR;
          } catch (final RootShellException e) {
              if (e.isIORelated())
                  return ERROR;
              throw e;
          }
      }
  
      public void ensureToolsAvailable() throws FileNotFoundException {
          synchronized (lock) {
              if (areToolsAvailable == null) {
                  try {
                      Log.d(TAG, extract() ? "Tools are now extracted into our private binary dir" :
                              "Tools were already extracted into our private binary dir");
                      areToolsAvailable = true;
                  } catch (final IOException e) {
                      Log.e(TAG, "The awg and awg-quick tools are not available", e);
                      areToolsAvailable = false;
                  }
              }
              if (!areToolsAvailable)
                  throw new FileNotFoundException("Required tools unavailable");
          }
      }
  
      public boolean extract() throws IOException {
          localBinaryDir.mkdirs();
          final File[] files = new File[EXECUTABLES.length];
          final File[] tempFiles = new File[EXECUTABLES.length];
          boolean allExist = true;
          for (int i = 0; i < files.length; ++i) {
              files[i] = new File(localBinaryDir, EXECUTABLES[i]);
              tempFiles[i] = new File(localBinaryDir, EXECUTABLES[i] + ".tmp");
              allExist &= files[i].exists();
          }
          if (allExist)
              return false;
          for (int i = 0; i < files.length; ++i) {
              if (!SharedLibraryLoader.extractLibrary(context, EXECUTABLES[i], tempFiles[i]))
                  throw new FileNotFoundException("Unable to find " + EXECUTABLES[i]);
              if (!tempFiles[i].setExecutable(true, false))
                  throw new IOException("Unable to mark " + tempFiles[i].getAbsolutePath() + " as executable");
              if (!tempFiles[i].renameTo(files[i]))
                  throw new IOException("Unable to rename " + tempFiles[i].getAbsolutePath() + " to " + files[i].getAbsolutePath());
          }
          return true;
      }
  
      @RestrictTo(Scope.LIBRARY_GROUP)
      public int install() throws RootShellException, IOException {
          if (!context.getPackageName().startsWith("org.amnezia."))
              throw new SecurityException("The tools may only be installed system-wide from the main AmneziaWG app.");
          return willInstallAsMagiskModule() ? installMagisk() : installSystem();
      }
  
      private int installMagisk() throws RootShellException, IOException {
          extract();
          final StringBuilder script = new StringBuilder("set -ex; ");
  
          script.append("trap 'rm -rf /data/adb/modules/amneziawg' INT TERM EXIT; ");
          script.append(String.format("rm -rf /data/adb/modules/amneziawg/; mkdir -p /data/adb/modules/amneziawg%s; ", INSTALL_DIR));
          script.append("printf 'id=amneziawg\nname=AmneziaWG Command Line Tools\nversion=1.0\nversionCode=1\nauthor=amnezia\ndescription=Command line tools for AmneziaWG\nminMagisk=1500\n' > /data/adb/modules/amneziawg/module.prop; ");
          script.append("touch /data/adb/modules/amneziawg/auto_mount; ");
          for (final String name : EXECUTABLES) {
              final File destination = new File("/data/adb/modules/amneziawg" + INSTALL_DIR, name);
              script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; chcon 'u:object_r:system_file:s0' '%s' || true; ",
                      new File(localBinaryDir, name), destination, destination, destination));
          }
          script.append("trap - INT TERM EXIT;");
  
          try {
              return rootShell.run(null, script.toString()) == 0 ? YES | MAGISK : ERROR;
          } catch (final IOException ignored) {
              return ERROR;
          } catch (final RootShellException e) {
              if (e.isIORelated())
                  return ERROR;
              throw e;
          }
      }
  
      private int installSystem() throws RootShellException, IOException {
          if (INSTALL_DIR == null)
              return OsConstants.ENOENT;
          extract();
          final StringBuilder script = new StringBuilder("set -ex; ");
          script.append("trap 'mount -o ro,remount /system' EXIT; mount -o rw,remount /system; ");
          for (final String name : EXECUTABLES) {
              final File destination = new File(INSTALL_DIR, name);
              script.append(String.format("cp '%s' '%s'; chmod 755 '%s'; restorecon '%s' || true; ",
                      new File(localBinaryDir, name), destination, destination, destination));
          }
          try {
              return rootShell.run(null, script.toString()) == 0 ? YES | SYSTEM : ERROR;
          } catch (final IOException ignored) {
              return ERROR;
          } catch (final RootShellException e) {
              if (e.isIORelated())
                  return ERROR;
              throw e;
          }
      }
  
      private boolean willInstallAsMagiskModule() {
          synchronized (lock) {
              if (installAsMagiskModule == null) {
                  try {
                      installAsMagiskModule = rootShell.run(null, "[ -d /data/adb/modules -a ! -f /cache/.disable_magisk ]") == OsConstants.EXIT_SUCCESS;
                  } catch (final Exception ignored) {
                      installAsMagiskModule = false;
                  }
              }
              return installAsMagiskModule;
          }
      }
  }
  