// 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 "chrome/browser/extensions/updater/local_extension_cache.h"

#include <stddef.h>
#include <stdint.h>

#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/macros.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/test/sequenced_worker_pool_owner.h"
#include "base/values.h"
#include "chrome/common/extensions/extension_constants.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "content/public/test/test_utils.h"
#include "crypto/secure_hash.h"
#include "crypto/sha2.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

const char kTestExtensionId1[] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const char kTestExtensionId2[] = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
const char kTestExtensionId3[] = "cccccccccccccccccccccccccccccccc";

}  // namespace

namespace extensions {

class LocalExtensionCacheTest : public testing::Test {
 public:
  LocalExtensionCacheTest() {}
  ~LocalExtensionCacheTest() override {}

  scoped_refptr<base::SequencedTaskRunner> background_task_runner() {
    return background_task_runner_;
  }

  // testing::Test overrides:
  void SetUp() override {
    pool_owner_.reset(
        new base::SequencedWorkerPoolOwner(3, "Background Pool"));
    background_task_runner_ = pool_owner_->pool()->GetSequencedTaskRunner(
        pool_owner_->pool()->GetNamedSequenceToken("background"));
  }

  base::FilePath CreateCacheDir(bool initialized) {
    EXPECT_TRUE(cache_dir_.CreateUniqueTempDir());
    if (initialized)
      CreateFlagFile(cache_dir_.path());
    return cache_dir_.path();
  }

  base::FilePath CreateTempDir() {
    EXPECT_TRUE(temp_dir_.CreateUniqueTempDir());
    return temp_dir_.path();
  }

  void CreateFlagFile(const base::FilePath& dir) {
    CreateFile(dir.Append(
        extensions::LocalExtensionCache::kCacheReadyFlagFileName),
        0, base::Time::Now());
  }

  void CreateExtensionFile(const base::FilePath& dir,
                           const std::string& id,
                           const std::string& version,
                           size_t size,
                           const base::Time& timestamp,
                           base::FilePath* filename) {
    const base::FilePath file = GetExtensionFileName(dir, id, version, "");
    if (filename)
      *filename = file;
    CreateFile(file, size, timestamp);
  }

  void CreateFile(const base::FilePath& file,
                  size_t size,
                  const base::Time& timestamp) {
    std::string data(size, 0);
    EXPECT_EQ(base::WriteFile(file, data.data(), data.size()), int(size));
    EXPECT_TRUE(base::TouchFile(file, timestamp, timestamp));
  }

  std::string CreateSignedExtensionFile(const base::FilePath& dir,
                                        const std::string& id,
                                        const std::string& version,
                                        size_t size,
                                        const base::Time& timestamp,
                                        base::FilePath* filename) {
    std::string data(size, 0);

    crypto::SecureHash* hash =
        crypto::SecureHash::Create(crypto::SecureHash::SHA256);
    hash->Update(data.c_str(), size);
    uint8_t output[crypto::kSHA256Length];
    hash->Finish(output, sizeof(output));
    const std::string hex_hash =
        base::ToLowerASCII(base::HexEncode(output, sizeof(output)));
    delete hash;

    const base::FilePath file =
        GetExtensionFileName(dir, id, version, hex_hash);
    if (filename)
      *filename = file;
    EXPECT_EQ(base::WriteFile(file, data.data(), data.size()), int(size));
    EXPECT_TRUE(base::TouchFile(file, timestamp, timestamp));

    return hex_hash;
  }

  base::FilePath GetExtensionFileName(const base::FilePath& dir,
                                      const std::string& id,
                                      const std::string& version,
                                      const std::string& hash) {
    return dir.Append(
        extensions::LocalExtensionCache::ExtensionFileName(id, version, hash));
  }

  void WaitForCompletion() {
    // In the worst case you need to repeat this up to 3 times to make sure that
    // all pending tasks we sent from UI thread to task runner and back to UI.
    for (int i = 0; i < 3; i++) {
      // Wait for background task completion that sends replay to UI thread.
      pool_owner_->pool()->FlushForTesting();
      // Wait for UI thread task completion.
      base::RunLoop().RunUntilIdle();
    }
  }

 private:
  content::TestBrowserThreadBundle thread_bundle_;

  scoped_ptr<base::SequencedWorkerPoolOwner> pool_owner_;
  scoped_refptr<base::SequencedTaskRunner> background_task_runner_;

  base::ScopedTempDir cache_dir_;
  base::ScopedTempDir temp_dir_;

  DISALLOW_COPY_AND_ASSIGN(LocalExtensionCacheTest);
};

static void SimpleCallback(bool* ptr) {
  *ptr = true;
}

TEST_F(LocalExtensionCacheTest, Basic) {
  base::FilePath cache_dir(CreateCacheDir(false));

  LocalExtensionCache cache(cache_dir,
                            1000,
                            base::TimeDelta::FromDays(30),
                            background_task_runner());
  cache.SetCacheStatusPollingDelayForTests(base::TimeDelta());

  bool initialized = false;
  cache.Init(true, base::Bind(&SimpleCallback, &initialized));

  WaitForCompletion();
  EXPECT_FALSE(initialized);

  base::FilePath file10, file01, file20, file30;
  CreateExtensionFile(cache_dir, kTestExtensionId1, "1.0", 100,
                      base::Time::Now() - base::TimeDelta::FromDays(1),
                      &file10);
  CreateExtensionFile(cache_dir, kTestExtensionId1, "0.1", 100,
                      base::Time::Now() - base::TimeDelta::FromDays(10),
                      &file01);
  CreateExtensionFile(cache_dir, kTestExtensionId2, "2.0", 100,
                      base::Time::Now() - base::TimeDelta::FromDays(40),
                      &file20);
  CreateExtensionFile(cache_dir, kTestExtensionId3, "3.0", 900,
                      base::Time::Now() - base::TimeDelta::FromDays(41),
                      &file30);

  CreateFlagFile(cache_dir);

  WaitForCompletion();
  ASSERT_TRUE(initialized);

  // Older version should be removed on cache initialization.
  EXPECT_FALSE(base::PathExists(file01));

  // All extensions should be there because cleanup happens on shutdown to
  // support use case when device was not used to more than 30 days and cache
  // shouldn't be cleaned before someone will have a chance to use it.
  EXPECT_TRUE(cache.GetExtension(kTestExtensionId1, "", NULL, NULL));
  EXPECT_TRUE(cache.GetExtension(kTestExtensionId2, "", NULL, NULL));
  EXPECT_TRUE(cache.GetExtension(kTestExtensionId3, "", NULL, NULL));

  bool did_shutdown = false;
  cache.Shutdown(base::Bind(&SimpleCallback, &did_shutdown));
  WaitForCompletion();
  ASSERT_TRUE(did_shutdown);

  EXPECT_TRUE(base::PathExists(file10));
  EXPECT_FALSE(base::PathExists(file20));
  EXPECT_FALSE(base::PathExists(file30));
}

TEST_F(LocalExtensionCacheTest, KeepHashed) {
  base::FilePath cache_dir(CreateCacheDir(false));

  LocalExtensionCache cache(cache_dir, 1000, base::TimeDelta::FromDays(30),
                            background_task_runner());
  cache.SetCacheStatusPollingDelayForTests(base::TimeDelta());

  bool initialized = false;
  cache.Init(true, base::Bind(&SimpleCallback, &initialized));

  WaitForCompletion();
  EXPECT_FALSE(initialized);

  // Add three identical extensions with different hash sums
  const base::Time time = base::Time::Now() - base::TimeDelta::FromDays(1);
  base::FilePath file, file1, file2;
  CreateExtensionFile(cache_dir, kTestExtensionId1, "1.0", 100, time, &file);
  const std::string hash1 = CreateSignedExtensionFile(
      cache_dir, kTestExtensionId1, "1.0", 100, time, &file1);
  const std::string hash2 = CreateSignedExtensionFile(
      cache_dir, kTestExtensionId1, "1.0", 123, time, &file2);

  CreateFlagFile(cache_dir);

  WaitForCompletion();
  ASSERT_TRUE(initialized);

  // Unhashed version should be removed on cache initialization.
  EXPECT_FALSE(base::PathExists(file));
  // Both hashed versions should stay
  EXPECT_TRUE(base::PathExists(file1));
  EXPECT_TRUE(base::PathExists(file2));

  // We should be able to lookup all three extension queries
  std::string version;
  EXPECT_TRUE(cache.GetExtension(kTestExtensionId1, "", NULL, &version));
  EXPECT_EQ(version, "1.0");
  EXPECT_TRUE(cache.GetExtension(kTestExtensionId1, hash1, NULL, NULL));
  EXPECT_TRUE(cache.GetExtension(kTestExtensionId1, hash2, NULL, NULL));
}

TEST_F(LocalExtensionCacheTest, KeepLatest) {
  base::FilePath cache_dir(CreateCacheDir(false));

  LocalExtensionCache cache(cache_dir, 1000, base::TimeDelta::FromDays(30),
                            background_task_runner());
  cache.SetCacheStatusPollingDelayForTests(base::TimeDelta());

  bool initialized = false;
  cache.Init(true, base::Bind(&SimpleCallback, &initialized));

  WaitForCompletion();
  EXPECT_FALSE(initialized);

  // All extension files are hashed, but have different versions
  const base::Time time = base::Time::Now() - base::TimeDelta::FromDays(1);
  base::FilePath file1, file21, file22;
  const std::string hash1 = CreateSignedExtensionFile(
      cache_dir, kTestExtensionId1, "1.0", 100, time, &file1);
  const std::string hash21 = CreateSignedExtensionFile(
      cache_dir, kTestExtensionId1, "2.0", 101, time, &file21);
  const std::string hash22 = CreateSignedExtensionFile(
      cache_dir, kTestExtensionId1, "2.0", 123, time, &file22);

  CreateFlagFile(cache_dir);

  WaitForCompletion();
  ASSERT_TRUE(initialized);

  // Older version should be removed
  EXPECT_FALSE(base::PathExists(file1));
  // Both newer hashed versions should stay
  EXPECT_TRUE(base::PathExists(file21));
  EXPECT_TRUE(base::PathExists(file22));

  // We should be able to lookup only the latest version queries
  EXPECT_FALSE(cache.GetExtension(kTestExtensionId1, hash1, NULL, NULL));
  EXPECT_TRUE(cache.GetExtension(kTestExtensionId1, hash21, NULL, NULL));
  EXPECT_TRUE(cache.GetExtension(kTestExtensionId1, hash22, NULL, NULL));
}

TEST_F(LocalExtensionCacheTest, Complex) {
  base::FilePath cache_dir(CreateCacheDir(false));

  LocalExtensionCache cache(cache_dir, 1000, base::TimeDelta::FromDays(30),
                            background_task_runner());
  cache.SetCacheStatusPollingDelayForTests(base::TimeDelta());

  bool initialized = false;
  cache.Init(true, base::Bind(&SimpleCallback, &initialized));

  WaitForCompletion();
  EXPECT_FALSE(initialized);

  // Like in KeepHashed test, but with two different versions
  const base::Time time = base::Time::Now() - base::TimeDelta::FromDays(1);
  base::FilePath file1, file11, file12, file2, file21, file22;
  CreateExtensionFile(cache_dir, kTestExtensionId1, "1.0", 100, time, &file1);
  const std::string hash11 = CreateSignedExtensionFile(
      cache_dir, kTestExtensionId1, "1.0", 101, time, &file11);
  const std::string hash12 = CreateSignedExtensionFile(
      cache_dir, kTestExtensionId1, "1.0", 102, time, &file12);
  CreateExtensionFile(cache_dir, kTestExtensionId1, "2.0", 103, time, &file2);
  const std::string hash21 = CreateSignedExtensionFile(
      cache_dir, kTestExtensionId1, "2.0", 104, time, &file21);
  const std::string hash22 = CreateSignedExtensionFile(
      cache_dir, kTestExtensionId1, "2.0", 105, time, &file22);

  CreateFlagFile(cache_dir);

  WaitForCompletion();
  ASSERT_TRUE(initialized);

  // Older and unhashed versions should be removed
  EXPECT_FALSE(base::PathExists(file1));
  EXPECT_FALSE(base::PathExists(file11));
  EXPECT_FALSE(base::PathExists(file12));
  EXPECT_FALSE(base::PathExists(file2));
  // Newest hashed versions should stay
  EXPECT_TRUE(base::PathExists(file21));
  EXPECT_TRUE(base::PathExists(file22));

  // We should be able to lookup only the latest version queries, both with and
  // without hash
  std::string version;
  EXPECT_TRUE(cache.GetExtension(kTestExtensionId1, "", NULL, &version));
  EXPECT_EQ(version, "2.0");
  EXPECT_FALSE(cache.GetExtension(kTestExtensionId1, hash11, NULL, NULL));
  EXPECT_FALSE(cache.GetExtension(kTestExtensionId1, hash12, NULL, NULL));
  EXPECT_TRUE(cache.GetExtension(kTestExtensionId1, hash21, NULL, NULL));
  EXPECT_TRUE(cache.GetExtension(kTestExtensionId1, hash22, NULL, NULL));
}

static void OnPutExtension(scoped_ptr<base::RunLoop>* run_loop,
                           const base::FilePath& file_path,
                           bool file_ownership_passed) {
  ASSERT_TRUE(*run_loop);
  (*run_loop)->Quit();
}

static void PutExtensionAndWait(LocalExtensionCache& cache,
                                const std::string& id,
                                const std::string& expected_hash,
                                const base::FilePath& path,
                                const std::string& version) {
  scoped_ptr<base::RunLoop> run_loop;
  run_loop.reset(new base::RunLoop);
  cache.PutExtension(id, expected_hash, path, version,
                     base::Bind(&OnPutExtension, &run_loop));
  run_loop->Run();
}

TEST_F(LocalExtensionCacheTest, PutExtensionCases) {
  base::FilePath cache_dir(CreateCacheDir(false));

  LocalExtensionCache cache(cache_dir, 1000, base::TimeDelta::FromDays(30),
                            background_task_runner());
  cache.SetCacheStatusPollingDelayForTests(base::TimeDelta());

  bool initialized = false;
  cache.Init(true, base::Bind(&SimpleCallback, &initialized));

  WaitForCompletion();
  EXPECT_FALSE(initialized);

  // Initialize cache with several different files
  const base::Time time = base::Time::Now() - base::TimeDelta::FromDays(1);
  base::FilePath file11, file12, file2, file3;
  const std::string hash11 = CreateSignedExtensionFile(
      cache_dir, kTestExtensionId1, "1.0", 101, time, &file11);
  const std::string hash12 = CreateSignedExtensionFile(
      cache_dir, kTestExtensionId1, "1.0", 102, time, &file12);
  CreateSignedExtensionFile(cache_dir, kTestExtensionId2, "0.2", 200, time,
                            &file2);
  CreateExtensionFile(cache_dir, kTestExtensionId3, "0.3", 300, time, &file3);

  CreateFlagFile(cache_dir);

  WaitForCompletion();
  ASSERT_TRUE(initialized);

  // Create and initialize installation source directory.
  base::ScopedTempDir temp_dir;
  EXPECT_TRUE(temp_dir.CreateUniqueTempDir());
  const base::FilePath temp_path = temp_dir.path();
  std::string version;

  // Right now we have two files for the first extension
  EXPECT_TRUE(base::PathExists(file11));
  EXPECT_TRUE(base::PathExists(file12));
  EXPECT_TRUE(base::PathExists(file2));
  EXPECT_TRUE(base::PathExists(file3));

  // 1. Cache contains an older version
  base::FilePath temp1;
  CreateExtensionFile(temp_path, kTestExtensionId1, "3.0", 110, time, &temp1);
  PutExtensionAndWait(cache, kTestExtensionId1, "", temp1, "3.0");
  // New file added
  const base::FilePath unhashed =
      GetExtensionFileName(cache_dir, kTestExtensionId1, "3.0", "");
  EXPECT_TRUE(base::PathExists(unhashed));
  // Old files removed from cache (kept in the directory though)
  EXPECT_TRUE(cache.GetExtension(kTestExtensionId1, hash11, NULL, &version));
  EXPECT_EQ(version, "3.0");
  EXPECT_TRUE(base::DeleteFile(temp1, false));

  // 2. Cache contains a newer version
  base::FilePath temp2;
  CreateExtensionFile(temp_path, kTestExtensionId1, "2.0", 120, time, &temp2);
  PutExtensionAndWait(cache, kTestExtensionId1, "", temp2, "2.0");
  // New file skipped
  EXPECT_FALSE(base::PathExists(
      GetExtensionFileName(cache_dir, kTestExtensionId1, "2.0", "")));
  // Old file kept
  EXPECT_TRUE(cache.GetExtension(kTestExtensionId1, "", NULL, &version));
  EXPECT_EQ(version, "3.0");
  EXPECT_TRUE(base::DeleteFile(temp2, false));

  // 3. Cache contains the same version without hash, our file is unhashed
  base::FilePath temp3;
  CreateExtensionFile(temp_path, kTestExtensionId1, "3.0", 130, time, &temp3);
  PutExtensionAndWait(cache, kTestExtensionId1, "", temp3, "3.0");
  // New file skipped, old file kept
  EXPECT_EQ(base::File(unhashed, base::File::FLAG_READ | base::File::FLAG_OPEN)
                .GetLength(),
            110);
  EXPECT_TRUE(base::DeleteFile(temp3, false));

  // 4. Cache contains the same version without hash, our file is hashed
  base::FilePath temp4;
  const std::string hash3 = CreateSignedExtensionFile(
      temp_path, kTestExtensionId1, "3.0", 140, time, &temp4);
  PutExtensionAndWait(cache, kTestExtensionId1, hash3, temp4, "3.0");
  // New file added
  const base::FilePath hashed =
      GetExtensionFileName(cache_dir, kTestExtensionId1, "3.0", hash3);
  EXPECT_TRUE(base::PathExists(hashed));
  EXPECT_TRUE(cache.GetExtension(kTestExtensionId1, hash3, NULL, NULL));
  // Old file removed (queries return hashed version)
  base::FilePath unhashed_path;
  EXPECT_TRUE(cache.GetExtension(kTestExtensionId1, "", &unhashed_path, NULL));
  EXPECT_EQ(unhashed_path, hashed);
  EXPECT_TRUE(base::DeleteFile(temp4, false));
  EXPECT_TRUE(base::DeleteFile(unhashed, false));

  // 5. Cache contains the same version with hash, our file is unhashed
  base::FilePath temp5;
  CreateExtensionFile(temp_path, kTestExtensionId1, "3.0", 150, time, &temp5);
  PutExtensionAndWait(cache, kTestExtensionId1, "", temp5, "3.0");
  // New file skipped
  EXPECT_FALSE(base::PathExists(unhashed));
  // Old file kept
  EXPECT_TRUE(cache.GetExtension(kTestExtensionId1, hash3, NULL, NULL));
  EXPECT_TRUE(base::DeleteFile(temp5, false));

  // 6. Cache contains the same version with hash, our file has the "same" hash
  base::FilePath temp6;
  CreateExtensionFile(temp_path, kTestExtensionId1, "3.0", 160, time, &temp6);
  PutExtensionAndWait(cache, kTestExtensionId1, hash3, temp6, "3.0");
  // New file skipped, old file kept
  EXPECT_EQ(base::File(hashed, base::File::FLAG_READ | base::File::FLAG_OPEN)
                .GetLength(),
            140);
  EXPECT_TRUE(base::DeleteFile(temp6, false));

  // 7. Cache contains the same version with hash, our file is different
  base::FilePath temp7;
  const std::string hash4 = CreateSignedExtensionFile(
      temp_path, kTestExtensionId1, "3.0", 170, time, &temp7);
  PutExtensionAndWait(cache, kTestExtensionId1, hash4, temp7, "3.0");
  // New file addded
  const base::FilePath hashed2 =
      GetExtensionFileName(cache_dir, kTestExtensionId1, "3.0", hash4);
  EXPECT_TRUE(base::PathExists(hashed2));
  EXPECT_TRUE(cache.GetExtension(kTestExtensionId1, hash4, NULL, NULL));
  // Old file kept
  EXPECT_TRUE(cache.GetExtension(kTestExtensionId1, hash3, NULL, NULL));
  EXPECT_TRUE(base::DeleteFile(temp7, false));
}

}  // namespace extensions