/*
 * Copyright (c) Facebook, Inc. and its 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.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Parcel;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.util.Log;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.TreeSet;

public final class SysUtil {

  private static final String TAG = "SysUtil";

  private static final byte APK_SIGNATURE_VERSION = 1;

  /**
   * Determine how preferred a given ABI is on this system.
   *
   * @param supportedAbis ABIs on this system
   * @param abi ABI of a shared library we might want to unpack
   * @return -1 if not supported or an integer, smaller being more preferred
   */
  public static int findAbiScore(String[] supportedAbis, String abi) {
    for (int i = 0; i < supportedAbis.length; ++i) {
      if (supportedAbis[i] != null && abi.equals(supportedAbis[i])) {
        return i;
      }
    }

    return -1;
  }

  public static void deleteOrThrow(File file) throws IOException {
    if (!file.delete()) {
      throw new IOException("could not delete file " + file);
    }
  }

  /**
   * Return an list of ABIs we supported on this device ordered according to preference. Use a
   * separate inner class to isolate the version-dependent call where it won't cause the whole class
   * to fail preverification.
   *
   * @return Ordered array of supported ABIs
   */
  public static String[] getSupportedAbis() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      return MarshmallowSysdeps.getSupportedAbis();
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      return LollipopSysdeps.getSupportedAbis();
    } else {
      return new String[] {Build.CPU_ABI, Build.CPU_ABI2};
    }
  }

  /**
   * Pre-allocate disk space for a file if we can do that on this version of the OS.
   *
   * @param fd File descriptor for file
   * @param length Number of bytes to allocate.
   */
  public static void fallocateIfSupported(FileDescriptor fd, long length) throws IOException {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      LollipopSysdeps.fallocateIfSupported(fd, length);
    }
  }

  /**
   * Delete a directory and its contents.
   *
   * <p>WARNING: Java APIs do not let us distinguish directories from symbolic links to directories.
   * Consequently, if the directory contains symbolic links to directories, we will attempt to
   * delete the contents of pointed-to directories.
   *
   * @param file File or directory to delete
   */
  public static void dumbDeleteRecursive(File file) throws IOException {
    if (file.isDirectory()) {
      File[] fileList = file.listFiles();
      if (fileList == null) {
        // If file is not a directory, listFiles() will return null
        return;
      }
      for (File entry : fileList) {
        dumbDeleteRecursive(entry);
      }
    }

    if (!file.delete() && file.exists()) {
      throw new IOException("could not delete: " + file);
    }
  }

  /**
   * Encapsulate Lollipop-specific calls into an independent class so we don't fail preverification
   * downlevel.
   */
  @TargetApi(Build.VERSION_CODES.LOLLIPOP)
  @DoNotOptimize
  private static final class LollipopSysdeps {
    @DoNotOptimize
    public static String[] getSupportedAbis() {
      String[] supportedAbis = Build.SUPPORTED_ABIS;
      TreeSet<String> allowedAbis = new TreeSet<>();
      try {
        // Some devices report both 64-bit and 32-bit ABIs but *actually* run
        // the process in 32-bit mode.
        //
        // Determine the current process bitness and use that to filter
        // out incompatible ABIs from SUPPORTED_ABIS.
        if (is64Bit()) {
          allowedAbis.add(MinElf.ISA.AARCH64.toString());
          allowedAbis.add(MinElf.ISA.X86_64.toString());
        } else {
          allowedAbis.add(MinElf.ISA.ARM.toString());
          allowedAbis.add(MinElf.ISA.X86.toString());
        }
      } catch (ErrnoException e) {
        Log.e(
            TAG,
            String.format(
                "Could not read /proc/self/exe. Falling back to default ABI list: %s. errno: %d Err msg: %s",
                Arrays.toString(supportedAbis), e.errno, e.getMessage()));
        return Build.SUPPORTED_ABIS;
      }
      // Filter out the incompatible ABIs from the list of supported ABIs,
      // retaining the original order.
      ArrayList<String> compatibleSupportedAbis = new ArrayList<>();
      for (String abi : supportedAbis) {
        if (allowedAbis.contains(abi)) {
          compatibleSupportedAbis.add(abi);
        }
      }

      String[] finalAbis = new String[compatibleSupportedAbis.size()];
      finalAbis = compatibleSupportedAbis.toArray(finalAbis);

      return finalAbis;
    }

    @DoNotOptimize
    public static void fallocateIfSupported(FileDescriptor fd, long length) throws IOException {
      try {
        Os.posix_fallocate(fd, 0, length);
      } catch (ErrnoException ex) {
        if (ex.errno != OsConstants.EOPNOTSUPP
            && ex.errno != OsConstants.ENOSYS
            && ex.errno != OsConstants.EINVAL) {
          throw new IOException(ex.toString(), ex);
        }
      }
    }

    @DoNotOptimize
    public static boolean is64Bit() throws ErrnoException {
      return Os.readlink("/proc/self/exe").contains("64");
    }
  }

  @TargetApi(Build.VERSION_CODES.M)
  @DoNotOptimize
  private static final class MarshmallowSysdeps {
    @DoNotOptimize
    public static String[] getSupportedAbis() {
      String[] supportedAbis = Build.SUPPORTED_ABIS;
      TreeSet<String> allowedAbis = new TreeSet<>();
      // Some devices report both 64-bit and 32-bit ABIs but *actually* run
      // the process in 32-bit mode.
      //
      // Determine the current process bitness and use that to filter
      // out incompatible ABIs from SUPPORTED_ABIS.
      if (is64Bit()) {
        allowedAbis.add(MinElf.ISA.AARCH64.toString());
        allowedAbis.add(MinElf.ISA.X86_64.toString());
      } else {
        allowedAbis.add(MinElf.ISA.ARM.toString());
        allowedAbis.add(MinElf.ISA.X86.toString());
      }
      // Filter out the incompatible ABIs from the list of supported ABIs,
      // retaining the original order.
      ArrayList<String> compatibleSupportedAbis = new ArrayList<>();
      for (String abi : supportedAbis) {
        if (allowedAbis.contains(abi)) {
          compatibleSupportedAbis.add(abi);
        }
      }

      String[] finalAbis = new String[compatibleSupportedAbis.size()];
      finalAbis = compatibleSupportedAbis.toArray(finalAbis);

      return finalAbis;
    }

    @DoNotOptimize
    public static boolean is64Bit() {
      return android.os.Process.is64Bit();
    }
  }

  /**
   * Like File.mkdirs, but throws on error. Succeeds even if File.mkdirs "fails", but dir still
   * names a directory.
   *
   * @param dir Directory to create. All parents created as well.
   */
  static void mkdirOrThrow(File dir) throws IOException {
    if (!dir.mkdirs() && !dir.isDirectory()) {
      throw new IOException("cannot mkdir: " + dir);
    }
  }

  /**
   * Copy up to byteLimit bytes from the input stream to the output stream.
   *
   * @param os Destination stream
   * @param is Input stream
   * @param byteLimit Maximum number of bytes to copy
   * @param buffer IO buffer to use
   * @return Number of bytes actually copied
   */
  static int copyBytes(RandomAccessFile os, InputStream is, int byteLimit, byte[] buffer)
      throws IOException {
    // Yes, this method is exactly the same as the above, just with a different type for `os'.
    int bytesCopied = 0;
    int nrRead;
    while (bytesCopied < byteLimit
        && (nrRead = is.read(buffer, 0, Math.min(buffer.length, byteLimit - bytesCopied))) != -1) {
      os.write(buffer, 0, nrRead);
      bytesCopied += nrRead;
    }
    return bytesCopied;
  }

  static void fsyncRecursive(File fileName) throws IOException {
    if (fileName.isDirectory()) {
      File[] files = fileName.listFiles();
      if (files == null) {
        throw new IOException("cannot list directory " + fileName);
      }
      for (int i = 0; i < files.length; ++i) {
        fsyncRecursive(files[i]);
      }
    } else if (fileName.getPath().endsWith("_lock")) {
      /* Do not sync! Any close(2) of a locked file counts as releasing the file for the whole
       * process! */
    } else {
      try (RandomAccessFile file = new RandomAccessFile(fileName, "r")) {
        file.getFD().sync();
      }
    }
  }

  public static byte[] makeApkDepBlock(File apkFile, Context context) throws IOException {
    apkFile = apkFile.getCanonicalFile();
    Parcel parcel = Parcel.obtain();
    try {
      parcel.writeByte(APK_SIGNATURE_VERSION);
      parcel.writeString(apkFile.getPath());
      parcel.writeLong(apkFile.lastModified());
      parcel.writeInt(getAppVersionCode(context));
      return parcel.marshall();
    } finally {
      parcel.recycle();
    }
  }

  public static int getAppVersionCode(Context context) {
    final PackageManager pm = context.getPackageManager();
    if (pm != null) {
      try {
        PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
        return pi.versionCode;
      } catch (PackageManager.NameNotFoundException e) {
        // That should not happen
      } catch (RuntimeException e) {
        // To catch RuntimeException("Package manager has died") that can occur
        // on some version of Android, when the remote PackageManager is
        // unavailable. I suspect this sometimes occurs when the App is being reinstalled.
      }
    }
    return 0;
  }

  @SuppressLint("CatchGeneralException")
  public static boolean is64Bit() {
    boolean is64bit = false;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      is64bit = MarshmallowSysdeps.is64Bit();
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      try {
        is64bit = LollipopSysdeps.is64Bit();
      } catch (Exception e) {
        Log.e(TAG, String.format("Could not read /proc/self/exe. Err msg: %s", e.getMessage()));
      }
    }
    return is64bit;
  }
}