// Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved.
//
// 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
//
//     https://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 <inttypes.h>
#include <string.h>

#include <algorithm>
#include <string_view>

#include <openssl/buf.h>
#include <openssl/err.h>
#include <openssl/mem.h>
#include <openssl/x509.h>

#include "../internal.h"
#include "../mem_internal.h"
#include "internal.h"


BSSL_NAMESPACE_BEGIN

// A ByDirEntry tracks state for a single directory, notably the starting suffix
// for CRL lookups.
class ByDirEntry {
 public:
  static constexpr bool kAllowUniquePtr = true;

  ByDirEntry() = default;

  static UniquePtr<ByDirEntry> Create(int dir_type, std::string_view dir) {
    auto ret = MakeUnique<ByDirEntry>();
    ret->dir_type_ = dir_type;
    ret->dir_.reset(OPENSSL_strndup(dir.data(), dir.size()));
    if (ret->dir_ == nullptr) {
      return nullptr;
    }
    return ret;
  }

  int dir_type() const { return dir_type_; }
  const char *dir() const { return dir_.get(); }

  int GetCRLSuffix(uint32_t hash) const {
    MutexReadLock lock(&lock_);
    auto it = std::lower_bound(crl_suffixes_.begin(), crl_suffixes_.end(), hash);
    if (it == crl_suffixes_.end() || it->hash != hash) {
      return 0;
    }
    return it->suffix;
  }

  bool UpdateCRLSuffix(uint32_t hash, int suffix) {
    MutexWriteLock lock(&lock_);
    auto it = std::lower_bound(crl_suffixes_.begin(), crl_suffixes_.end(), hash);
    if (it != crl_suffixes_.end() && it->hash == hash) {
      it->suffix = std::max(suffix, it->suffix);
      return true;
    }
    if (!crl_suffixes_.Push(CRLSuffix{hash, suffix})) {
      return false;
    }
    std::sort(crl_suffixes_.begin(), crl_suffixes_.end());
    return true;
  }

 private:
  struct CRLSuffix {
    uint32_t hash;
    int suffix;
    bool operator<(uint32_t h) const { return hash < h; }
    bool operator<(const CRLSuffix &other) const { return hash < other.hash; }
  };

  UniquePtr<char> dir_;
  int dir_type_ = 0;
  mutable Mutex lock_;
  // crl_suffixes_ is kept sorted.
  // TODO(davidben): This should be a hash table. Insertions are O(N log N).
  Vector<CRLSuffix> crl_suffixes_;
};

struct ByDir {
  Vector<UniquePtr<ByDirEntry>> dirs;
};

static int dir_ctrl(X509_LOOKUP *ctx, int cmd, const char *argp, long argl,
                    char **ret);
static int new_dir(X509_LOOKUP *lu);
static void free_dir(X509_LOOKUP *lu);
static int add_cert_dir(ByDir *ctx, const char *dir, int type);
static int get_cert_by_subject(X509_LOOKUP *xl, int type, const X509_NAME *name,
                               X509_OBJECT *ret);
static const X509_LOOKUP_METHOD x509_dir_lookup = {
    new_dir,              // new
    free_dir,             // free
    dir_ctrl,             // ctrl
    get_cert_by_subject,  // get_by_subject
};

static int dir_ctrl(X509_LOOKUP *ctx, int cmd, const char *argp, long argl,
                    char **retp) {
  ByDir *ld = reinterpret_cast<ByDir *>(ctx->method_data);
  switch (cmd) {
    case X509_L_ADD_DIR:
      if (argl == X509_FILETYPE_DEFAULT) {
        const char *dir = getenv(X509_get_default_cert_dir_env());
        if (!add_cert_dir(ld, dir ? dir : X509_get_default_cert_dir(),
                          X509_FILETYPE_PEM)) {
          OPENSSL_PUT_ERROR(X509, X509_R_LOADING_CERT_DIR);
          return 0;
        }
        return 1;
      }
      return add_cert_dir(ld, argp, (int)argl);
  }
  return 0;
}

static int new_dir(X509_LOOKUP *lu) {
  ByDir *a = New<ByDir>();
  if (a == nullptr) {
    return 0;
  }
  lu->method_data = a;
  return 1;
}

static void free_dir(X509_LOOKUP *lu) {
  Delete(reinterpret_cast<ByDir *>(lu->method_data));
}

#if defined(OPENSSL_WINDOWS)
#define DIR_HASH_SEPARATOR ';'
#else
#define DIR_HASH_SEPARATOR ':'
#endif

static int add_cert_dir(ByDir *ctx, const char *inp, int type) {
  if (inp == nullptr || !*inp) {
    OPENSSL_PUT_ERROR(X509, X509_R_INVALID_DIRECTORY);
    return 0;
  }

  std::string_view rest = inp;
  do {
    // Split by `DIR_HASH_SEPARATOR`.
    size_t sep = rest.find(DIR_HASH_SEPARATOR);
    std::string_view dir;
    if (sep == std::string_view::npos) {
      dir = rest;
      rest = std::string_view();
    } else {
      dir = rest.substr(0, sep);
      rest = rest.substr(sep + 1);
    }
    if (dir.empty()) {
      continue;
    }
    // Ignore duplicates.
    if (std::any_of(ctx->dirs.begin(), ctx->dirs.end(),
                    [&](const auto &ent) { return ent->dir() == dir; })) {
      continue;
    }
    auto ent = ByDirEntry::Create(type, dir);
    if (ent == nullptr || !ctx->dirs.Push(std::move(ent))) {
      return 0;
    }
  } while (!rest.empty());
  return 1;
}

static int get_cert_by_subject(X509_LOOKUP *xl, int type, const X509_NAME *name,
                               X509_OBJECT *ret) {
  if (name == nullptr) {
    return 0;
  }

  // Set up an `X509_OBJECT` to compare against.
  UniquePtr<X509> lookup_cert;
  UniquePtr<X509_CRL> lookup_crl;
  X509_OBJECT stmp;
  const char *postfix = "";
  stmp.type = type;
  ByDir *ctx = reinterpret_cast<ByDir *>(xl->method_data);
  if (type == X509_LU_X509) {
    lookup_cert.reset(X509_new());
    if (lookup_cert == nullptr ||
        !X509_set_subject_name(lookup_cert.get(), name)) {
      return 0;
    }
    stmp.data.x509 = lookup_cert.get();
    postfix = "";
  } else if (type == X509_LU_CRL) {
    lookup_crl.reset(X509_CRL_new());
    if (lookup_crl == nullptr ||
        !X509_CRL_set_issuer_name(lookup_crl.get(), name)) {
      return 0;
    }
    stmp.data.crl = lookup_crl.get();
    postfix = "r";
  } else {
    OPENSSL_PUT_ERROR(X509, X509_R_WRONG_LOOKUP_TYPE);
    return 0;
  }

  // Try both new and old hashes.
  const uint32_t hashes[] = {X509_NAME_hash(name), X509_NAME_hash_old(name)};
  for (uint32_t hash : hashes) {
    for (UniquePtr<ByDirEntry> &ent : ctx->dirs) {
      // If a CRL, start from the previously saved suffix. Updated CRLs are
      // expected to be added until new filenames.
      // TODO(crbug.com/42290566): Is this what we want?
      int suffix = 0;
      if (type == X509_LU_CRL) {
        suffix = ent->GetCRLSuffix(hash);
      }

      // The directory format handles hash collections by incrementing a suffix
      // on the file name. Load every suffix into the cache.
      for (;;) {
        char *path = nullptr;
        if (OPENSSL_asprintf(&path, "%s/%08" PRIx32 ".%s%d", ent->dir(), hash,
                             postfix, suffix) == -1) {
          OPENSSL_PUT_ERROR(X509, ERR_R_BUF_LIB);
          return 0;
        }
        UniquePtr<char> free_path(path);
        if (type == X509_LU_X509) {
          if ((X509_load_cert_file(xl, path, ent->dir_type())) == 0) {
            // Don't expose the lower level error, All of these boil down to "we
            // could not find a CA".
            ERR_clear_error();
            break;
          }
        } else if (type == X509_LU_CRL) {
          if ((X509_load_crl_file(xl, path, ent->dir_type())) == 0) {
            // Don't expose the lower level error, All of these boil down to "we
            // could not find a CRL".
            ERR_clear_error();
            break;
          }
        }
        // The lack of a CA or CRL will be caught higher up.
        suffix++;
      }

      // We have added it to the cache so now pull it out again.
      auto *store_impl = FromOpaque(xl->store_ctx);
      store_impl->objs_lock.LockWrite();
      const X509_OBJECT *found = nullptr;
      sk_X509_OBJECT_sort(store_impl->objs.get());
      size_t idx;
      if (sk_X509_OBJECT_find(store_impl->objs.get(), &idx, &stmp)) {
        found = sk_X509_OBJECT_value(store_impl->objs.get(), idx);
      }
      store_impl->objs_lock.UnlockWrite();

      // If a CRL, store the last suffix we saw, to skip already loaded files
      // next time.
      // TODO(crbug.com/42290566): Is this what we want?
      if (type == X509_LU_CRL && !ent->UpdateCRLSuffix(hash, suffix)) {
        return 0;
      }

      if (found != nullptr) {
        // Clear any errors that might have been raised processing empty or
        // malformed files.
        ERR_clear_error();

        // TODO(crbug.com/42290561): This should manage the reference counts
        // correctly but does not.
        ret->type = found->type;
        OPENSSL_memcpy(&ret->data, &found->data, sizeof(ret->data));
        return 1;
      }
    }
  }

  return 0;
}

BSSL_NAMESPACE_END

const X509_LOOKUP_METHOD *X509_LOOKUP_hash_dir() {
  return &bssl::x509_dir_lookup;
}

int X509_LOOKUP_add_dir(X509_LOOKUP *lookup, const char *name, int type) {
  return X509_LOOKUP_ctrl(lookup, X509_L_ADD_DIR, name, type, nullptr);
}
