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

#include "extensions/browser/api/web_request/form_data_parser.h"

#include <stddef.h>

#include <string_view>

#include "base/check.h"
#include "extensions/buildflags/buildflags.h"
#include "testing/gtest/include/gtest/gtest.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

namespace {

// Attempts to create a parser corresponding to the |content_type_header|.
// On success, returns the parser.
std::unique_ptr<FormDataParser> InitParser(
    const std::string& content_type_header) {
  std::unique_ptr<FormDataParser> parser(
      FormDataParser::CreateFromContentTypeHeader(&content_type_header));
  if (parser.get() == nullptr) {
    return nullptr;
  }
  return parser;
}

// Attempts to run the parser corresponding to the |content_type_header|
// on the source represented by the concatenation of blocks from |bytes|.
// On success, returns true and the parsed |output|, else false.
// Parsed |output| has names on even positions (0, 2, ...), values on odd ones.
bool RunParser(const std::string& content_type_header,
               const std::vector<const std::string_view*>& bytes,
               std::vector<std::string>* output) {
  DCHECK(output);
  output->clear();
  std::unique_ptr<FormDataParser> parser(InitParser(content_type_header));
  if (!parser.get()) {
    return false;
  }
  FormDataParser::Result result;
  for (size_t block = 0; block < bytes.size(); ++block) {
    if (!parser->SetSource(*(bytes[block]))) {
      return false;
    }
    while (parser->GetNextNameValue(&result)) {
      output->push_back(result.name());
      base::Value value = result.take_value();
      if (value.is_string()) {
        output->push_back(value.GetString());
      } else {
        const auto& blob = value.GetBlob();
        output->push_back(std::string(blob.begin(), blob.end()));
      }
    }
  }
  return parser->AllDataReadOK();
}

// Attempts to run the parser corresponding to the |content_type_header|
// on the source represented by the concatenation of blocks from |bytes|.
// Checks that the parser fails parsing.
bool CheckParserFails(const std::string& content_type_header,
                      const std::vector<const std::string_view*>& bytes) {
  std::vector<std::string> output;
  std::unique_ptr<FormDataParser> parser(InitParser(content_type_header));
  if (!parser.get()) {
    return false;
  }
  FormDataParser::Result result;
  for (size_t block = 0; block < bytes.size(); ++block) {
    if (!parser->SetSource(*(bytes[block]))) {
      break;
    }
    while (parser->GetNextNameValue(&result)) {
      output.push_back(result.name());
      base::Value value = result.take_value();
      if (value.is_string()) {
        output.push_back(value.GetString());
      } else {
        const auto& blob = value.GetBlob();
        output.push_back(std::string(blob.begin(), blob.end()));
      }
    }
  }
  return !parser->AllDataReadOK();
}

}  // namespace

TEST(WebRequestFormDataParserTest, Parsing) {
  // We verify that POST data parsers cope with various formats of POST data.
  // Construct the test data.
  const std::string kBoundary = "THIS_IS_A_BOUNDARY";
  const std::string kBlockStr1 =
      std::string("--") + kBoundary +
      "\r\n"
      "Content-Disposition: form-data; name=\"text\"\r\n"
      "\r\n"
      "test\rtext\nwith non-CRLF line breaks\r\n"
      "--" +
      kBoundary +
      "\r\n"
      "Content-Disposition: form-data; name=\"file\"; filename=\"test\"\r\n"
      "Content-Type: application/octet-stream\r\n"
      "\r\n";
  const std::string kBlockStr2 =
      std::string("\r\n--") + kBoundary +
      "\r\n"
      "Content-Disposition: form-data; name=\"password\"\r\n"
      "\r\n"
      "test password\r\n"
      "--" +
      kBoundary +
      "\r\n"
      "Content-Disposition: form-data; name=\"radio\"\r\n"
      "\r\n"
      "Yes\r\n"
      "--" +
      kBoundary +
      "\r\n"
      "Content-Disposition: form-data; name=\"check\"\r\n"
      "\r\n"
      "option A\r\n"
      "--" +
      kBoundary +
      "\r\n"
      "Content-Disposition: form-data; name=\"check\"\r\n"
      "\r\n"
      "option B\r\n"
      "--" +
      kBoundary +
      "\r\n"
      "Content-Disposition: form-data; name=\"txtarea\"\r\n"
      "\r\n"
      "Some text.\r\n"
      "Other.\r\n"
      "\r\n"
      "--" +
      kBoundary +
      "\r\n"
      "Content-Disposition: form-data; name=\"select\"\r\n"
      "\r\n"
      "one\r\n"
      "--" +
      kBoundary + "\r\n";
  const std::string kBlockStr3 =
      "Content-Disposition: form-data; name=\"binary\"\r\n"
      "Content-Type: application/octet-stream\r\n"
      "\r\n"
      "\u0420\u043e\u0434\u0436\u0435\u0440 "
      "\u0416\u0435\u043b\u044f\u0437\u043d\u044b"
      "\r\n"
      "--" +
      kBoundary + "--";
  // POST data input.
  const std::string kBigBlock = kBlockStr1 + kBlockStr2 + kBlockStr3;
  const std::string kUrlEncodedBlock =
      "text=test%0Dtext%0Awith+non-CRLF+line+breaks"
      "&file=test&password=test+password&radio=Yes&check=option+A"
      "&check=option+B&txtarea=Some+text.%0D%0AOther.%0D%0A&select=one"
      "&binary=%D0%A0%D0%BE%D0%B4%D0%B6%D0%B5%D1%80%20%D0%96%D0%B5%D0%BB%D1%8F%"
      "D0%B7%D0%BD%D1%8B";
  const std::string_view kMultipartBytes(kBigBlock);
  const std::string_view kMultipartBytesSplit1(kBlockStr1);
  const std::string_view kMultipartBytesSplit2(kBlockStr2);
  const std::string_view kMultipartBytesSplit3(kBlockStr3);
  const std::string_view kUrlEncodedBytes(kUrlEncodedBlock);
  const std::string kPlainBlock = "abc";
  const std::string_view kTextPlainBytes(kPlainBlock);
  // Headers.
  const std::string kUrlEncoded = "application/x-www-form-urlencoded";
  const std::string kTextPlain = "text/plain";
  const std::string kMultipart =
      std::string("multipart/form-data; boundary=") + kBoundary;
  // Expected output.
  const std::vector<std::string> kExpected = {
      "text",
      "test\rtext\nwith non-CRLF line breaks",
      "file",
      "test",
      "password",
      "test password",
      "radio",
      "Yes",
      "check",
      "option A",
      "check",
      "option B",
      "txtarea",
      "Some text.\r\nOther.\r\n",
      "select",
      "one",
      "binary",
      "\u0420\u043e\u0434\u0436\u0435\u0440 "
      "\u0416\u0435\u043b\u044f\u0437\u043d\u044b"};

  std::vector<const std::string_view*> input;
  std::vector<std::string> output;

  // First test: multipart POST data in one lump.
  input.push_back(&kMultipartBytes);
  EXPECT_TRUE(RunParser(kMultipart, input, &output));
  EXPECT_EQ(kExpected, output);

  // Second test: multipart POST data in several lumps.
  input.clear();
  input.push_back(&kMultipartBytesSplit1);
  input.push_back(&kMultipartBytesSplit2);
  input.push_back(&kMultipartBytesSplit3);
  EXPECT_TRUE(RunParser(kMultipart, input, &output));
  EXPECT_EQ(kExpected, output);

  // Third test: URL-encoded POST data.
  input.clear();
  input.push_back(&kUrlEncodedBytes);
  EXPECT_TRUE(RunParser(kUrlEncoded, input, &output));
  EXPECT_EQ(kExpected, output);

  // Fourth test: text/plain POST data in one lump.
  input.clear();
  input.push_back(&kTextPlainBytes);
  // This should fail, text/plain is ambiguous and thus unparseable.
  EXPECT_FALSE(RunParser(kTextPlain, input, &output));
}

TEST(WebRequestFormDataParserTest, MalformedPayload) {
  // We verify that POST data parsers reject malformed data.
  // Construct the test data.
  const std::string kBoundary = "THIS_IS_A_BOUNDARY";
  const std::string kBlockStr =
      std::string("--") + kBoundary +
      "\r\n"
      "Content-Disposition: form-data; name=\"text\"\r\n"
      "\r\n"
      "test\rtext\nwith non-CRLF line breaks\r\n"
      "-" +
      kBoundary +
      "\r\n" /* Missing '-'. */
      "Content-Disposition: form-data; name=\"file\"; filename=\"test\"\r\n"
      "Content-Type: application/octet-stream\r\n"
      /* Two CRLF missing. */
      "--" +
      kBoundary +
      "\r\n"
      "Content-Disposition: form-data; name=\"select\"\r\n"
      "\r\n"
      "one\r\n"
      "--" +
      kBoundary + "-" /* Missing '-' at the end. */;
  // POST data input.
  // The following block is corrupted -- contains a "==" substring.
  const std::string kUrlEncodedBlock =
      "text=test%0Dtext%0Awith+non-CRLF+line+breaks"
      "&file==test&password=test+password&radio=Yes&check=option+A"
      "&check=option+B&txtarea=Some+text.%0D%0AOther.%0D%0A&select=one";
  const std::string_view kMultipartBytes(kBlockStr);
  const std::string_view kMultipartBytesEmpty("");
  const std::string_view kUrlEncodedBytes(kUrlEncodedBlock);
  const std::string_view kUrlEncodedBytesEmpty("");
  // Headers.
  const std::string kUrlEncoded = "application/x-www-form-urlencoded";
  const std::string kMultipart =
      std::string("multipart/form-data; boundary=") + kBoundary;

  std::vector<const std::string_view*> input;

  // First test: malformed multipart POST data.
  input.push_back(&kMultipartBytes);
  EXPECT_TRUE(CheckParserFails(kMultipart, input));

  // Second test: empty multipart POST data.
  input.clear();
  input.push_back(&kMultipartBytesEmpty);
  EXPECT_TRUE(CheckParserFails(kMultipart, input));

  // Third test: malformed URL-encoded POST data.
  input.clear();
  input.push_back(&kUrlEncodedBytes);
  EXPECT_TRUE(CheckParserFails(kUrlEncoded, input));

  // Fourth test: empty URL-encoded POST data. Note that an empty string is a
  // valid url-encoded value, so this should parse correctly.
  std::vector<std::string> output;
  input.clear();
  input.push_back(&kUrlEncodedBytesEmpty);
  EXPECT_TRUE(RunParser(kUrlEncoded, input, &output));
  EXPECT_EQ(0u, output.size());
}

}  // namespace extensions
