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

#include <stdint.h>

#include <string>
#include <vector>

#include "base/base64url.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/stl_util.h"
#include "build/build_config.h"
#include "extensions/browser/verified_contents.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_paths.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace extensions {

namespace {

const char kContentVerifierDirectory[] = "content_verifier/";
const char kPublicKeyPem[] = "public_key.pem";

// Whether or not dot and space suffixes of filename are ignored in the
// current OS.
const bool kDotSpaceSuffixIgnored =
#if defined(OS_WIN)
    true;
#else
    false;
#endif  // defined(OS_WIN)

std::string DecodeBase64Url(const std::string& encoded) {
  std::string decoded;
  if (!base::Base64UrlDecode(
          encoded, base::Base64UrlDecodePolicy::IGNORE_PADDING, &decoded))
    return std::string();

  return decoded;
}

bool GetPublicKey(const base::FilePath& path, std::string* public_key) {
  std::string public_key_pem;
  if (!base::ReadFileToString(path, &public_key_pem))
    return false;
  if (!Extension::ParsePEMKeyBytes(public_key_pem, public_key))
    return false;
  return true;
}

base::FilePath GetTestDir(const char* sub_dir) {
  base::FilePath path;
  base::PathService::Get(DIR_TEST_DATA, &path);
  return path.AppendASCII(kContentVerifierDirectory).AppendASCII(sub_dir);
}

// Loads verified_contents file from a sub directory under
// kContentVerifierDirectory.
std::unique_ptr<VerifiedContents> CreateTestVerifiedContents(
    const char* sub_dir,
    const char* verified_contents_filename) {
  // Figure out our test data directory.
  base::FilePath path = GetTestDir(sub_dir);

  // Initialize the VerifiedContents object.
  std::string public_key;
  if (!GetPublicKey(path.AppendASCII(kPublicKeyPem), &public_key))
    return nullptr;

  auto contents = std::make_unique<VerifiedContents>(
      base::as_bytes(base::make_span(public_key)));
  base::FilePath verified_contents_path =
      path.AppendASCII(verified_contents_filename);
  if (!contents->InitFrom(verified_contents_path))
    return nullptr;
  return contents;
}

}  // namespace

TEST(VerifiedContents, Simple) {
  std::unique_ptr<VerifiedContents> verified_contents =
      CreateTestVerifiedContents("simple", "verified_contents.json");
  ASSERT_TRUE(verified_contents);
  const VerifiedContents& contents = *verified_contents;

  // Make sure we get expected values.
  EXPECT_EQ(contents.block_size(), 4096);
  EXPECT_EQ(contents.extension_id(), "abcdefghijklmnopabcdefghijklmnop");
  EXPECT_EQ("1.2.3", contents.version().GetString());

  EXPECT_TRUE(contents.TreeHashRootEquals(
      base::FilePath::FromUTF8Unsafe("manifest.json"),
      DecodeBase64Url("-vyyIIn7iSCzg7X3ICUI5wZa3tG7w7vyiCckxZdJGfs")));

  EXPECT_TRUE(contents.TreeHashRootEquals(
      base::FilePath::FromUTF8Unsafe("background.js"),
      DecodeBase64Url("txHiG5KQvNoPOSH5FbQo9Zb5gJ23j3oFB0Ru9DOnziw")));

  base::FilePath foo_bar_html =
      base::FilePath(FILE_PATH_LITERAL("foo")).AppendASCII("bar.html");
  EXPECT_FALSE(foo_bar_html.IsAbsolute());
  EXPECT_TRUE(contents.TreeHashRootEquals(
      foo_bar_html,
      DecodeBase64Url("L37LFbT_hmtxRL7AfGZN9YTpW6yoz_ZiQ1opLJn1NZU")));

  base::FilePath nonexistent = base::FilePath::FromUTF8Unsafe("nonexistent");
  EXPECT_FALSE(contents.HasTreeHashRoot(nonexistent));

  EXPECT_TRUE(contents.TreeHashRootEquals(
      base::FilePath::FromUTF8Unsafe("lowercase.html"),
      DecodeBase64Url("HpLotLGCmmOdKYvGQmD3OkXMKGs458dbanY4WcfAZI0")));
  EXPECT_TRUE(contents.TreeHashRootEquals(
      base::FilePath::FromUTF8Unsafe("Lowercase.Html"),
      DecodeBase64Url("HpLotLGCmmOdKYvGQmD3OkXMKGs458dbanY4WcfAZI0")));
  EXPECT_TRUE(contents.TreeHashRootEquals(
      base::FilePath::FromUTF8Unsafe("LOWERCASE.HTML"),
      DecodeBase64Url("HpLotLGCmmOdKYvGQmD3OkXMKGs458dbanY4WcfAZI0")));

  EXPECT_TRUE(contents.TreeHashRootEquals(
      base::FilePath::FromUTF8Unsafe("ALLCAPS.HTML"),
      DecodeBase64Url("bl-eV8ENowvtw6P14D4X1EP0mlcMoG-_aOx5o9C1364")));
  EXPECT_TRUE(contents.TreeHashRootEquals(
      base::FilePath::FromUTF8Unsafe("AllCaps.Html"),
      DecodeBase64Url("bl-eV8ENowvtw6P14D4X1EP0mlcMoG-_aOx5o9C1364")));
  EXPECT_TRUE(contents.TreeHashRootEquals(
      base::FilePath::FromUTF8Unsafe("allcaps.html"),
      DecodeBase64Url("bl-eV8ENowvtw6P14D4X1EP0mlcMoG-_aOx5o9C1364")));

  EXPECT_TRUE(contents.TreeHashRootEquals(
      base::FilePath::FromUTF8Unsafe("MixedCase.Html"),
      DecodeBase64Url("zEAO9FwciigMNy3NtU2XNb-dS5TQMmVNx0T9h7WvXbQ")));
  EXPECT_TRUE(contents.TreeHashRootEquals(
      base::FilePath::FromUTF8Unsafe("MIXEDCASE.HTML"),
      DecodeBase64Url("zEAO9FwciigMNy3NtU2XNb-dS5TQMmVNx0T9h7WvXbQ")));
  EXPECT_TRUE(contents.TreeHashRootEquals(
      base::FilePath::FromUTF8Unsafe("mixedcase.html"),
      DecodeBase64Url("zEAO9FwciigMNy3NtU2XNb-dS5TQMmVNx0T9h7WvXbQ")));
  EXPECT_TRUE(contents.TreeHashRootEquals(
      base::FilePath::FromUTF8Unsafe("mIxedcAse.Html"),
      DecodeBase64Url("zEAO9FwciigMNy3NtU2XNb-dS5TQMmVNx0T9h7WvXbQ")));

  EXPECT_TRUE(contents.TreeHashRootEquals(
      base::FilePath::FromUTF8Unsafe("mIxedcAse.Html"),
      DecodeBase64Url("nKRqUcJg1_QZWAeCb4uFd5ouC0McuGavKp8TFDRqBgg")));
  EXPECT_TRUE(contents.TreeHashRootEquals(
      base::FilePath::FromUTF8Unsafe("MIXEDCASE.HTML"),
      DecodeBase64Url("nKRqUcJg1_QZWAeCb4uFd5ouC0McuGavKp8TFDRqBgg")));
  EXPECT_TRUE(contents.TreeHashRootEquals(
      base::FilePath::FromUTF8Unsafe("mixedcase.html"),
      DecodeBase64Url("nKRqUcJg1_QZWAeCb4uFd5ouC0McuGavKp8TFDRqBgg")));
  EXPECT_TRUE(contents.TreeHashRootEquals(
      base::FilePath::FromUTF8Unsafe("MixedCase.Html"),
      DecodeBase64Url("nKRqUcJg1_QZWAeCb4uFd5ouC0McuGavKp8TFDRqBgg")));

  // Regression test for https://crbug.com/776609.
  EXPECT_FALSE(contents.TreeHashRootEquals(
      base::FilePath::FromUTF8Unsafe("allcaps.html"),
      // This is the hash of "mixedcase.html".
      DecodeBase64Url("zEAO9FwciigMNy3NtU2XNb-dS5TQMmVNx0T9h7WvXbQ")));
}

TEST(VerifiedContents, FailsOnBase64) {
  // Accepting base64-encoded input where base64url-encoded input is expected
  // will be considered to be invalid data. Verify that it gets rejected.
  ASSERT_FALSE(
      CreateTestVerifiedContents("simple", "verified_contents_base64.json"));
}

// Tests behavior of verified contents with filenames that have "." and " "
// suffixes appened to them.
// Regression test for https://crbug.com/696208.
TEST(VerifiedContents, DotSpaceSuffixedFiles) {
  std::unique_ptr<VerifiedContents> contents =
      CreateTestVerifiedContents("dot_space_suffix", "verified_contents.json");
  ASSERT_TRUE(contents);

  // Make sure we get expected values.
  EXPECT_EQ(contents->block_size(), 4096);
  EXPECT_EQ(contents->extension_id(), "abcdabcdabcdabcdabcdabcdabcdabcd");
  EXPECT_EQ("1.2.3.4", contents->version().GetString());

  auto has_tree_hash_root = [&contents](const std::string& file_path_str) {
    return contents->HasTreeHashRoot(
        base::FilePath::FromUTF8Unsafe(file_path_str));
  };
  auto tree_hash_root_equals = [&contents](const std::string& file_path_str,
                                           const char* expected_hash) {
    return contents->TreeHashRootEquals(
        base::FilePath::FromUTF8Unsafe(file_path_str),
        DecodeBase64Url(expected_hash));
  };

  // Non-existent files won't be found in tree hash.
  EXPECT_FALSE(has_tree_hash_root("non-existent.js"));
  EXPECT_FALSE(has_tree_hash_root(""));

  struct TestFileInfo {
    const char* filename;
    const char* root_hashes;
  };
  std::vector<TestFileInfo> info_list{
      {
          "manifest.json", "ysCDJuQ1s7vWF4yUZTRB2_XDE6vfFyQcIPSmyvNvqEw",
      },
      {
          "background.js", "uYeF7eHzVgKpiBg5fikv2NTctmJnxCfX1UhhlrizvNE",
      },
      {
          "mixedcase.html", "S1lnRa4Yu1CM2dCwJoFYKfAqRkFC7SSI4tzyIOzO7hA",
      },
      {
          "mixedCase.html", "FVncNmt1wBfFn3aZVTnMB9CFRTRIl0Z4YFqm14Wmrhs",
      },
      {
          "doT.html.", "jEsJEk-0azFYx7G91rSUPuzPBXp95863lG4MDwZcSog",
      },
  };

  std::vector<std::string> kSuffixes{
      // Only spaces.
      " ", "  ", "   ",
      // Only dots.
      ".", "..", "...",
      // Mix of dots and spaces.
      ". ", ".  ", ".. ", "... ", " .", "  .", "   .", " . ", " ..", " ...",
      " .. ",
  };

  for (const TestFileInfo& info : info_list) {
    // The original filenames' hashes must exist.
    EXPECT_TRUE(has_tree_hash_root(info.filename));
    EXPECT_TRUE(tree_hash_root_equals(info.filename, info.root_hashes));

    // Verify that the discovery of tree hashes is also correct when the
    // filenames are appended with dot and space characters:
    //   - they should still succeed on windows (kDotSpaceSuffixIgnored = true).
    //   - they should fail otherwise (kDotSpaceSuffixIgnored = false).
    for (const std::string& suffix : kSuffixes) {
      std::string path_with_suffix = std::string(info.filename).append(suffix);
      EXPECT_EQ(kDotSpaceSuffixIgnored, has_tree_hash_root(path_with_suffix));
    }
  }

  // For background.js, additionally verify that reading the file with and
  // without the suffixes described above matches our expectations, taking
  // kDotSpaceSuffixIgnored into account.
  const char* kBackgroundJSFilename = "background.js";
  const char* kBackgroundJSContents = "console.log('hello');\n";
  base::FilePath test_dir = GetTestDir("dot_space_suffix");
  {
    // Case 1/2: background.js without suffix.
    base::FilePath background_js_path =
        test_dir.AppendASCII(kBackgroundJSFilename);
    EXPECT_TRUE(base::PathExists(background_js_path));
    std::string background_js_contents;
    EXPECT_TRUE(
        base::ReadFileToString(background_js_path, &background_js_contents));
    EXPECT_EQ(kBackgroundJSContents, background_js_contents);
  }
  {
    // Case 2/2: background.js with dot/space suffixes.
    for (const std::string& suffix : kSuffixes) {
      base::FilePath background_js_suffix_path = test_dir.AppendASCII(
          std::string(kBackgroundJSFilename).append(suffix));
      EXPECT_EQ(kDotSpaceSuffixIgnored,
                base::PathExists(background_js_suffix_path));
      if (kDotSpaceSuffixIgnored) {
        std::string background_js_suffix_contents;
        EXPECT_TRUE(base::ReadFileToString(background_js_suffix_path,
                                           &background_js_suffix_contents));
        EXPECT_EQ(kBackgroundJSContents, background_js_suffix_contents);
      }
    }
  }
}

// Tests behavior of verified_contents.json file containing keys already with
// "." suffix.
// Regression test for https://crbug.com/696208.
TEST(VerifiedContents, VerifiedContentsFileContainsDotSuffixedFilename) {
  std::unique_ptr<VerifiedContents> contents =
      CreateTestVerifiedContents("dot_space_suffix", "verified_contents.json");
  ASSERT_TRUE(contents);

  // Make sure we get expected values.
  EXPECT_EQ(contents->block_size(), 4096);
  EXPECT_EQ(contents->extension_id(), "abcdabcdabcdabcdabcdabcdabcdabcd");
  EXPECT_EQ("1.2.3.4", contents->version().GetString());

  auto has_tree_hash_root = [&contents](const std::string& file_path_str) {
    return contents->HasTreeHashRoot(
        base::FilePath::FromUTF8Unsafe(file_path_str));
  };
  auto tree_hash_root_equals = [&contents](const std::string& file_path_str,
                                           const char* expected_hash) {
    return contents->TreeHashRootEquals(
        base::FilePath::FromUTF8Unsafe(file_path_str),
        DecodeBase64Url(expected_hash));
  };

  // The original key "doT.html.", and its case variants should succeed.
  EXPECT_TRUE(has_tree_hash_root("doT.html."));
  EXPECT_TRUE(tree_hash_root_equals(
      "doT.html.", "jEsJEk-0azFYx7G91rSUPuzPBXp95863lG4MDwZcSog"));
  EXPECT_TRUE(has_tree_hash_root("dot.html."));
  EXPECT_TRUE(tree_hash_root_equals(
      "dot.html.", "jEsJEk-0azFYx7G91rSUPuzPBXp95863lG4MDwZcSog"));

  // Keys with dot stripped succeeds if kDotSpaceSuffixIgnored is true.
  {
    const char* kKey = "dot.html";
    EXPECT_EQ(kDotSpaceSuffixIgnored, has_tree_hash_root(kKey));
    EXPECT_EQ(kDotSpaceSuffixIgnored,
              tree_hash_root_equals(
                  kKey, "jEsJEk-0azFYx7G91rSUPuzPBXp95863lG4MDwZcSog"));
  }

  // Also, adding (.| )+ suffix would succeed if kDotSpaceSuffixIgnored is
  // true. This is already part of VerifiedContents.DotSpaceSuffixedFiles test.
}

}  // namespace extensions
