// Copyright 1999-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 <openssl/asn1.h>

#include <assert.h>
#include <stdlib.h>
#include <string.h>

#include <iterator>

#include <openssl/err.h>
#include <openssl/mem.h>
#include <openssl/obj.h>

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


using namespace bssl;

BSSL_NAMESPACE_BEGIN

DEFINE_LHASH_OF(ASN1_STRING_TABLE)

BSSL_NAMESPACE_END

static LHASH_OF(ASN1_STRING_TABLE) *string_tables = nullptr;
static StaticMutex string_tables_lock;

void ASN1_STRING_set_default_mask(unsigned long mask) {}

unsigned long ASN1_STRING_get_default_mask() { return B_ASN1_UTF8STRING; }

int ASN1_STRING_set_default_mask_asc(const char *p) { return 1; }

static const ASN1_STRING_TABLE *asn1_string_table_get(int nid);

// The following function generates an ASN1_STRING based on limits in a
// table. Frequently the types and length of an ASN1_STRING are restricted by
// a corresponding OID. For example certificates and certificate requests.

ASN1_STRING *ASN1_STRING_set_by_NID(ASN1_STRING **out, const unsigned char *in,
                                    ossl_ssize_t len, int inform, int nid) {
  ASN1_STRING *str = nullptr;
  int ret;
  if (!out) {
    out = &str;
  }
  const ASN1_STRING_TABLE *tbl = asn1_string_table_get(nid);
  if (tbl != nullptr) {
    unsigned long mask = tbl->mask;
    if (!(tbl->flags & STABLE_NO_MASK)) {
      mask &= B_ASN1_UTF8STRING;
    }
    ret = ASN1_mbstring_ncopy(out, in, len, inform, mask, tbl->minsize,
                              tbl->maxsize);
  } else {
    ret = ASN1_mbstring_copy(out, in, len, inform, B_ASN1_UTF8STRING);
  }
  if (ret <= 0) {
    return nullptr;
  }
  return *out;
}

// Now the tables and helper functions for the string table:

// See RFC 5280.
#define ub_name 32768
#define ub_common_name 64
#define ub_locality_name 128
#define ub_state_name 128
#define ub_organization_name 64
#define ub_organization_unit_name 64
#define ub_email_address 128
#define ub_serial_number 64

// This table must be kept in NID order

static const ASN1_STRING_TABLE tbl_standard[] = {
    {NID_commonName, 1, ub_common_name, DIRSTRING_TYPE, 0},
    {NID_countryName, 2, 2, B_ASN1_PRINTABLESTRING, STABLE_NO_MASK},
    {NID_localityName, 1, ub_locality_name, DIRSTRING_TYPE, 0},
    {NID_stateOrProvinceName, 1, ub_state_name, DIRSTRING_TYPE, 0},
    {NID_organizationName, 1, ub_organization_name, DIRSTRING_TYPE, 0},
    {NID_organizationalUnitName, 1, ub_organization_unit_name, DIRSTRING_TYPE,
     0},
    {NID_pkcs9_emailAddress, 1, ub_email_address, B_ASN1_IA5STRING,
     STABLE_NO_MASK},
    {NID_pkcs9_unstructuredName, 1, -1, PKCS9STRING_TYPE, 0},
    {NID_pkcs9_challengePassword, 1, -1, PKCS9STRING_TYPE, 0},
    {NID_pkcs9_unstructuredAddress, 1, -1, DIRSTRING_TYPE, 0},
    {NID_givenName, 1, ub_name, DIRSTRING_TYPE, 0},
    {NID_surname, 1, ub_name, DIRSTRING_TYPE, 0},
    {NID_initials, 1, ub_name, DIRSTRING_TYPE, 0},
    {NID_serialNumber, 1, ub_serial_number, B_ASN1_PRINTABLESTRING,
     STABLE_NO_MASK},
    {NID_friendlyName, -1, -1, B_ASN1_BMPSTRING, STABLE_NO_MASK},
    {NID_name, 1, ub_name, DIRSTRING_TYPE, 0},
    {NID_dnQualifier, -1, -1, B_ASN1_PRINTABLESTRING, STABLE_NO_MASK},
    {NID_domainComponent, 1, -1, B_ASN1_IA5STRING, STABLE_NO_MASK},
    {NID_ms_csp_name, -1, -1, B_ASN1_BMPSTRING, STABLE_NO_MASK}};

static int table_cmp(const ASN1_STRING_TABLE *a, const ASN1_STRING_TABLE *b) {
  if (a->nid < b->nid) {
    return -1;
  }
  if (a->nid > b->nid) {
    return 1;
  }
  return 0;
}

static int table_cmp_void(const void *a, const void *b) {
  return table_cmp(reinterpret_cast<const ASN1_STRING_TABLE *>(a),
                   reinterpret_cast<const ASN1_STRING_TABLE *>(b));
}

static uint32_t table_hash(const ASN1_STRING_TABLE *tbl) {
  return OPENSSL_hash32(&tbl->nid, sizeof(tbl->nid));
}

static const ASN1_STRING_TABLE *asn1_string_table_get(int nid) {
  ASN1_STRING_TABLE key;
  key.nid = nid;
  const ASN1_STRING_TABLE *tbl = reinterpret_cast<ASN1_STRING_TABLE *>(
      bsearch(&key, tbl_standard, std::size(tbl_standard),
              sizeof(ASN1_STRING_TABLE), table_cmp_void));
  if (tbl != nullptr) {
    return tbl;
  }

  string_tables_lock.LockRead();
  if (string_tables != nullptr) {
    tbl = lh_ASN1_STRING_TABLE_retrieve(string_tables, &key);
  }
  string_tables_lock.UnlockRead();
  // Note returning `tbl` without the lock is only safe because
  // `ASN1_STRING_TABLE_add` cannot modify or delete existing entries. If we
  // wish to support that, this function must copy the result under a lock.
  return tbl;
}

int ASN1_STRING_TABLE_add(int nid, long minsize, long maxsize,
                          unsigned long mask, unsigned long flags) {
  // Existing entries cannot be overwritten.
  if (asn1_string_table_get(nid) != nullptr) {
    OPENSSL_PUT_ERROR(ASN1, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
    return 0;
  }

  MutexWriteLock lock(&string_tables_lock);
  ASN1_STRING_TABLE *tbl = nullptr;
  if (string_tables == nullptr) {
    string_tables = lh_ASN1_STRING_TABLE_new(table_hash, table_cmp);
    if (string_tables == nullptr) {
      return 0;
    }
  } else {
    // Check again for an existing entry. One may have been added while
    // unlocked.
    ASN1_STRING_TABLE key;
    key.nid = nid;
    if (lh_ASN1_STRING_TABLE_retrieve(string_tables, &key) != nullptr) {
      OPENSSL_PUT_ERROR(ASN1, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
      return 0;
    }
  }

  tbl = New<ASN1_STRING_TABLE>();
  if (tbl == nullptr) {
    return 0;
  }
  tbl->nid = nid;
  tbl->flags = flags;
  tbl->minsize = minsize;
  tbl->maxsize = maxsize;
  tbl->mask = mask;
  ASN1_STRING_TABLE *old_tbl;
  if (!lh_ASN1_STRING_TABLE_insert(string_tables, &old_tbl, tbl)) {
    Delete(tbl);
    return 0;
  }
  assert(old_tbl == nullptr);
  return 1;
}

void ASN1_STRING_TABLE_cleanup() {}

void bssl::asn1_get_string_table_for_testing(const ASN1_STRING_TABLE **out_ptr,
                                             size_t *out_len) {
  *out_ptr = tbl_standard;
  *out_len = std::size(tbl_standard);
}
