/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * 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.
 */

#include "cuttlefish/common/libs/utils/users.h"

#include <grp.h>
#include <pwd.h>
#include <sys/types.h>
#include <unistd.h>

#include <cerrno>
#include <cstring>
#include <mutex>
#include <string>
#include <vector>

#include <android-base/file.h>
#include "absl/log/log.h"

#include "cuttlefish/common/libs/utils/contains.h"
#include "cuttlefish/result/result.h"

namespace cuttlefish {
namespace {
std::vector<gid_t> GetSuplementaryGroups() {
  int num_groups = getgroups(0, nullptr);
  if (num_groups < 0) {
    LOG(ERROR) << "Unable to get number of supplementary groups: "
               << std::strerror(errno);
    return {};
  }
  std::vector<gid_t> groups(num_groups + 1);
  int retval = getgroups(groups.size(), groups.data());
  if (retval < 0) {
    LOG(ERROR) << "Error obtaining list of supplementary groups (list size: "
               << groups.size() << "): " << std::strerror(errno);
    return {};
  }
  return groups;
}
}  // namespace

gid_t GroupIdFromName(const std::string& group_name) {
  struct group grp{};
  struct group* grp_p{};
  std::vector<char> buffer(100);
  int result = 0;
  while(true) {
    result = getgrnam_r(group_name.c_str(), &grp, buffer.data(), buffer.size(),
                        &grp_p);
    if (result != ERANGE) {
      break;
    }
    buffer.resize(2*buffer.size());
  }
  if (result == 0) {
    if (grp_p != nullptr) {
      return grp.gr_gid;
    } else {
      // Caller may be checking with non-existent group name
      return -1;
    }
  } else {
    LOG(ERROR) << "Unable to get group id for group " << group_name << ": "
               << std::strerror(result);
    return -1;
  }
}

bool InGroup(const std::string& group) {
  auto gid = GroupIdFromName(group);
  if (gid == static_cast<gid_t>(-1)) {
    return false;
  }

  if (gid == getegid()) {
    return true;
  }

  auto groups = GetSuplementaryGroups();
  return Contains(groups, gid);
}

Result<std::string> SystemWideUserHome() {
  auto uid = getuid();
  // getpwuid() is not thread-safe, so we need a lock across all calls
  static std::mutex getpwuid_mutex;
  std::string home_dir;
  {
    std::lock_guard<std::mutex> lock(getpwuid_mutex);
    const auto entry = getpwuid(uid);
    if (entry) {
      home_dir = entry->pw_dir;
    }
    endpwent();
    if (home_dir.empty()) {
      return CF_ERRNO("Failed to find the home directory using " << uid);
    }
  }
  return home_dir;
}

Result<std::string> CurrentUserName() {
  uid_t uid = getuid();
  struct passwd pwd;
  struct passwd* result;
  long val = sysconf(_SC_GETPW_R_SIZE_MAX);
  size_t bufsize = (val == -1) ? 16384 : val;
  std::vector<char> buffer(bufsize);
  int s = getpwuid_r(uid, &pwd, buffer.data(), buffer.size(), &result);
  CF_EXPECT(s == 0, "getpwuid_r failed: " << strerror(s));
  CF_EXPECT(result != nullptr, "User not found for uid " << uid);
  return std::string(pwd.pw_name);
}

bool IsKvmAccessible() {
#ifdef __linux__
  return access("/dev/kvm", R_OK | W_OK) == 0 || InGroup("kvm");
#else
  return false;
#endif
}

} // namespace cuttlefish
