// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/base64url.h"

#include <stddef.h>

#include <string_view>

#include "base/base64.h"
#include "base/numerics/safe_math.h"
#include "base/strings/string_util.h"
#include "third_party/modp_b64/modp_b64.h"

namespace base {

namespace {

const char kPaddingChar = '=';

// Base64url maps {+, /} to {-, _} in order for the encoded content to be safe
// to use in a URL. These characters will be translated by this implementation.
const char kBase64Chars[] = "+/";
const char kBase64UrlSafeChars[] = "-_";

class StringViewOrString {
 public:
  explicit StringViewOrString(std::string_view piece) : piece_(piece) {}
  explicit StringViewOrString(std::string str) : str_(std::move(str)) {}

  std::string_view get() const {
    if (str_) {
      return *str_;
    }
    return piece_;
  }

 private:
  const std::optional<std::string> str_;
  const std::string_view piece_;
};

// Converts the base64url `input` into a plain base64 string.
std::optional<StringViewOrString> Base64UrlToBase64(
    std::string_view input,
    Base64UrlDecodePolicy policy) {
  // Characters outside of the base64url alphabet are disallowed, which includes
  // the {+, /} characters found in the conventional base64 alphabet.
  if (input.find_first_of(kBase64Chars) != std::string::npos) {
    return std::nullopt;
  }

  const size_t required_padding_characters = input.size() % 4;
  const bool needs_replacement =
      input.find_first_of(kBase64UrlSafeChars) != std::string::npos;

  switch (policy) {
    case Base64UrlDecodePolicy::REQUIRE_PADDING:
      // Fail if the required padding is not included in |input|.
      if (required_padding_characters > 0) {
        return std::nullopt;
      }
      break;
    case Base64UrlDecodePolicy::IGNORE_PADDING:
      // Missing padding will be silently appended.
      break;
    case Base64UrlDecodePolicy::DISALLOW_PADDING:
      // Fail if padding characters are included in |input|.
      if (input.find_first_of(kPaddingChar) != std::string::npos) {
        return std::nullopt;
      }
      break;
  }

  if (required_padding_characters == 0 && !needs_replacement) {
    return StringViewOrString(input);
  }

  // If the string either needs replacement of URL-safe characters to normal
  // base64 ones, or additional padding, a copy of |input| needs to be made in
  // order to make these adjustments without side effects.
  CheckedNumeric<size_t> out_size = input.size();
  if (required_padding_characters > 0) {
    out_size += 4 - required_padding_characters;
  }

  std::string base64_input;
  base64_input.reserve(out_size.ValueOrDie());
  base64_input.append(input);

  // Substitute the base64url URL-safe characters to their base64 equivalents.
  ReplaceChars(base64_input, "-", "+", &base64_input);
  ReplaceChars(base64_input, "_", "/", &base64_input);

  // Append the necessary padding characters.
  base64_input.resize(out_size.ValueOrDie(), '=');

  return StringViewOrString(std::move(base64_input));
}

}  // namespace

void Base64UrlEncode(span<const uint8_t> input,
                     Base64UrlEncodePolicy policy,
                     std::string* output) {
  *output = Base64Encode(input);

  ReplaceChars(*output, "+", "-", output);
  ReplaceChars(*output, "/", "_", output);

  switch (policy) {
    case Base64UrlEncodePolicy::INCLUDE_PADDING:
      // The padding included in |*output| will not be amended.
      break;
    case Base64UrlEncodePolicy::OMIT_PADDING:
      // The padding included in |*output| will be removed.
      const size_t last_non_padding_pos =
          output->find_last_not_of(kPaddingChar);
      if (last_non_padding_pos != std::string::npos) {
        output->resize(last_non_padding_pos + 1);
      }

      break;
  }
}

void Base64UrlEncode(std::string_view input,
                     Base64UrlEncodePolicy policy,
                     std::string* output) {
  Base64UrlEncode(base::as_byte_span(input), policy, output);
}

bool Base64UrlDecode(std::string_view input,
                     Base64UrlDecodePolicy policy,
                     std::string* output) {
  std::optional<StringViewOrString> base64_input =
      Base64UrlToBase64(input, policy);
  if (!base64_input) {
    return false;
  }
  return Base64Decode(base64_input->get(), output);
}

std::optional<std::vector<uint8_t>> Base64UrlDecode(
    std::string_view input,
    Base64UrlDecodePolicy policy) {
  std::optional<StringViewOrString> base64_input =
      Base64UrlToBase64(input, policy);
  if (!base64_input) {
    return std::nullopt;
  }
  return Base64Decode(base64_input->get());
}

}  // namespace base
