// Copyright 2017 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. See the AUTHORS file for names of contributors.

#include "third_party/leveldatabase/leveldb_chrome.h"

#include <memory>
#include <set>
#include <vector>

#include "base/bind.h"
#include "base/containers/flat_set.h"
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/format_macros.h"
#include "base/memory/memory_pressure_listener.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/stringprintf.h"
#include "base/sys_info.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "base/trace_event/memory_dump_manager.h"
#include "base/trace_event/memory_dump_provider.h"
#include "base/trace_event/process_memory_dump.h"
#include "third_party/leveldatabase/env_chromium.h"
#include "third_party/leveldatabase/src/helpers/memenv/memenv.h"
#include "util/mutexlock.h"

using MemoryPressureLevel = base::MemoryPressureListener::MemoryPressureLevel;
using base::trace_event::MemoryAllocatorDump;
using base::trace_event::MemoryDumpArgs;
using base::trace_event::MemoryDumpProvider;
using base::trace_event::ProcessMemoryDump;
using leveldb::Cache;
using leveldb::NewLRUCache;

namespace leveldb_chrome {

namespace {

size_t DefaultBlockCacheSize() {
  if (base::SysInfo::IsLowEndDevice())
    return 1 << 20;  // 1MB
  else
    return 8 << 20;  // 8MB
}

std::string GetDumpNameForMemEnv(const leveldb::Env* memenv) {
  return base::StringPrintf("leveldatabase/memenv_0x%" PRIXPTR,
                            reinterpret_cast<uintptr_t>(memenv));
}

// Singleton owning resources shared by Chrome's leveldb databases.
class Globals {
 public:
  static Globals* GetInstance() {
    static Globals* globals = new Globals();
    return globals;
  }

  Globals() : browser_block_cache_(NewLRUCache(DefaultBlockCacheSize())) {
    if (!base::SysInfo::IsLowEndDevice())
      web_block_cache_.reset(NewLRUCache(DefaultBlockCacheSize()));

    memory_pressure_listener_.reset(new base::MemoryPressureListener(
        base::Bind(&Globals::OnMemoryPressure, base::Unretained(this))));
  }

  Cache* web_block_cache() const {
    if (web_block_cache_)
      return web_block_cache_.get();
    return browser_block_cache();
  }

  Cache* browser_block_cache() const { return browser_block_cache_.get(); }

  // Called when the system is under memory pressure.
  void OnMemoryPressure(MemoryPressureLevel memory_pressure_level) {
    if (memory_pressure_level ==
        MemoryPressureLevel::MEMORY_PRESSURE_LEVEL_NONE)
      return;
    browser_block_cache()->Prune();
    if (browser_block_cache() == web_block_cache())
      return;
    web_block_cache()->Prune();
  }

  void DidCreateChromeMemEnv(leveldb::Env* env) {
    leveldb::MutexLock l(&env_mutex_);
    DCHECK(in_memory_envs_.find(env) == in_memory_envs_.end());
    in_memory_envs_.insert(env);
  }

  void WillDestroyChromeMemEnv(leveldb::Env* env) {
    leveldb::MutexLock l(&env_mutex_);
    DCHECK(in_memory_envs_.find(env) != in_memory_envs_.end());
    in_memory_envs_.erase(env);
  }

  bool IsInMemoryEnv(const leveldb::Env* env) const {
    leveldb::MutexLock l(&env_mutex_);
    return in_memory_envs_.find(env) != in_memory_envs_.end();
  }

  void DumpAllTrackedEnvs(const MemoryDumpArgs& dump_args,
                          base::trace_event::ProcessMemoryDump* pmd);

  void UpdateHistograms() {
    leveldb_env::DBTracker::GetInstance()->UpdateHistograms();

    // In-memory caches are hard-coded to be zero bytes so don't log
    // LevelDB.SharedCache.BytesUsed.InMemory.

    // leveldb limits the read cache size to 1GB, but its default value is 8MB,
    // and Chrome uses either 1MB or 8MB.
    if (GetSharedWebBlockCache() == GetSharedBrowserBlockCache()) {
      UMA_HISTOGRAM_COUNTS_100000("LevelDB.SharedCache.KBUsed.Unified",
                                  browser_block_cache_->TotalCharge() / 1024);
      return;
    }
    UMA_HISTOGRAM_COUNTS_100000("LevelDB.SharedCache.KBUsed.Web",
                                web_block_cache_->TotalCharge() / 1024);
    UMA_HISTOGRAM_COUNTS_100000("LevelDB.SharedCache.KBUsed.Browser",
                                browser_block_cache_->TotalCharge() / 1024);
  }

 private:
  ~Globals() {}

  std::unique_ptr<Cache> web_block_cache_;      // null on low end devices.
  std::unique_ptr<Cache> browser_block_cache_;  // Never null.
  // Listens for the system being under memory pressure.
  std::unique_ptr<base::MemoryPressureListener> memory_pressure_listener_;
  mutable leveldb::port::Mutex env_mutex_;
  base::flat_set<leveldb::Env*> in_memory_envs_;

  DISALLOW_COPY_AND_ASSIGN(Globals);
};

class ChromeMemEnv : public leveldb::EnvWrapper {
 public:
  ChromeMemEnv(leveldb::Env* base_env, const std::string& name)
      : EnvWrapper(leveldb::NewMemEnv(base_env)),
        base_env_(target()),
        name_(name) {
    Globals::GetInstance()->DidCreateChromeMemEnv(this);
  }

  ~ChromeMemEnv() override {
    Globals::GetInstance()->WillDestroyChromeMemEnv(this);
  }

  leveldb::Status NewWritableFile(const std::string& f,
                                  leveldb::WritableFile** r) override {
    leveldb::Status s = leveldb::EnvWrapper::NewWritableFile(f, r);
    if (s.ok()) {
      base::AutoLock lock(files_lock_);
      file_names_.insert(f);
    }
    return s;
  }

  leveldb::Status NewAppendableFile(const std::string& f,
                                    leveldb::WritableFile** r) override {
    leveldb::Status s = leveldb::EnvWrapper::NewAppendableFile(f, r);
    if (s.ok()) {
      base::AutoLock lock(files_lock_);
      file_names_.insert(f);
    }
    return s;
  }

  leveldb::Status DeleteFile(const std::string& fname) override {
    leveldb::Status s = leveldb::EnvWrapper::DeleteFile(fname);
    if (s.ok()) {
      base::AutoLock lock(files_lock_);
      DCHECK(base::ContainsKey(file_names_, fname));
      file_names_.erase(fname);
    }
    return s;
  }

  leveldb::Status RenameFile(const std::string& src,
                             const std::string& target) override {
    leveldb::Status s = leveldb::EnvWrapper::RenameFile(src, target);
    if (s.ok()) {
      base::AutoLock lock(files_lock_);
      file_names_.erase(src);
      file_names_.insert(target);
    }
    return s;
  }

  // Calculate the memory used by this in-memory database. leveldb's in-memory
  // databases store their data in a MemEnv which can be shared between
  // databases. Chrome rarely (if ever) shares MemEnv's. This calculation is
  // also an estimate as leveldb does not expose a way to retrieve or properly
  // calculate the amount of RAM used by a MemEnv.
  uint64_t size() {
    // This is copied from
    // //third_party/leveldatabase/src/helpers/memenv/memenv.cc which cannot be
    // included here. Duplicated for size calculations only.
    struct FileState {
      leveldb::port::Mutex refs_mutex_;
      int refs_;
      std::vector<char*> blocks_;
      uint64_t size_;
      enum { kBlockSize = 8 * 1024 };
    };

    uint64_t total_size = 0;
    base::AutoLock lock(files_lock_);
    for (const std::string& fname : file_names_) {
      uint64_t file_size;
      leveldb::Status s = GetFileSize(fname, &file_size);
      DCHECK(s.ok());
      if (s.ok())
        total_size += file_size + fname.size() + 1 + sizeof(FileState);
    }
    return total_size;
  }

  const std::string& name() const { return name_; }

  bool OnMemoryDump(const MemoryDumpArgs& dump_args, ProcessMemoryDump* pmd) {
    auto* env_dump = pmd->CreateAllocatorDump(GetDumpNameForMemEnv(this));

    env_dump->AddScalar(base::trace_event::MemoryAllocatorDump::kNameSize,
                        base::trace_event::MemoryAllocatorDump::kUnitsBytes,
                        size());

    if (dump_args.level_of_detail !=
        base::trace_event::MemoryDumpLevelOfDetail::BACKGROUND) {
      env_dump->AddString("name", "", name());
    }

    const char* system_allocator_name =
        base::trace_event::MemoryDumpManager::GetInstance()
            ->system_allocator_pool_name();
    if (system_allocator_name) {
      pmd->AddSuballocation(env_dump->guid(), system_allocator_name);
    }

    return true;
  }

 private:
  std::unique_ptr<leveldb::Env> base_env_;
  const std::string name_;
  base::Lock files_lock_;
  std::set<std::string> file_names_;
  DISALLOW_COPY_AND_ASSIGN(ChromeMemEnv);
};

void Globals::DumpAllTrackedEnvs(const MemoryDumpArgs& dump_args,
                                 ProcessMemoryDump* pmd) {
  leveldb::MutexLock l(&env_mutex_);
  for (auto env : in_memory_envs_)
    static_cast<ChromeMemEnv*>(env)->OnMemoryDump(dump_args, pmd);
}

// Delete all files in a |directory| using using the provided |env|.
// Note, this is not recursive as it is only called to delete files in an
// in-memory Env's filesystem.
leveldb::Status DeleteEnvDirectory(const std::string& directory,
                                   leveldb::Env* env) {
  std::vector<std::string> filenames;
  leveldb::Status result = env->GetChildren(directory, &filenames);
  if (!result.ok()) {
    // Ignore error in case directory does not exist
    return leveldb::Status::OK();
  }

  leveldb::FileLock* lock;
  const std::string lockname = leveldb::LockFileName(directory);
  result = env->LockFile(lockname, &lock);
  if (!result.ok())
    return result;

  for (const std::string& filename : filenames) {
    leveldb::Status del = env->DeleteFile(directory + "/" + filename);
    if (result.ok() && !del.ok())
      result = del;
  }
  env->UnlockFile(lock);  // Ignore error since state is already gone
  env->DeleteFile(lockname);
  if (result.ok())
    result = env->DeleteDir(directory);

  return result;
}

}  // namespace

// Returns a separate (from the default) block cache for use by web APIs.
// This must be used when opening the databases accessible to Web-exposed APIs,
// so rogue pages can't mount a denial of service attack by hammering the block
// cache. Without separate caches, such an attack might slow down Chrome's UI to
// the point where the user can't close the offending page's tabs.
Cache* GetSharedWebBlockCache() {
  return Globals::GetInstance()->web_block_cache();
}

Cache* GetSharedBrowserBlockCache() {
  return Globals::GetInstance()->browser_block_cache();
}

Cache* GetSharedInMemoryBlockCache() {
  // Zero size cache to prevent cache hits.
  static leveldb::Cache* s_empty_cache = leveldb::NewLRUCache(0);
  return s_empty_cache;
}

bool IsMemEnv(const leveldb::Env* env) {
  DCHECK(env);
  return Globals::GetInstance()->IsInMemoryEnv(env);
}

std::unique_ptr<leveldb::Env> NewMemEnv(const std::string& name,
                                        leveldb::Env* base_env) {
  if (!base_env)
    base_env = leveldb::Env::Default();
  return std::make_unique<ChromeMemEnv>(base_env, name);
}

void UpdateHistograms() {
  return Globals::GetInstance()->UpdateHistograms();
}

bool ParseFileName(const std::string& filename,
                   uint64_t* number,
                   leveldb::FileType* type) {
  return leveldb::ParseFileName(filename, number, type);
}

bool CorruptClosedDBForTesting(const base::FilePath& db_path) {
  base::File current(db_path.Append(FILE_PATH_LITERAL("CURRENT")),
                     base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
  if (!current.IsValid()) {
    return false;
  }
  const char kString[] = "StringWithoutEOL";
  if (current.Write(0, kString, sizeof(kString)) != sizeof(kString))
    return false;
  current.Close();
  return true;
}

bool PossiblyValidDB(const base::FilePath& db_path, leveldb::Env* env) {
  const base::FilePath current = db_path.Append(FILE_PATH_LITERAL("CURRENT"));
  return env->FileExists(current.AsUTF8Unsafe());
}

leveldb::Status DeleteDB(const base::FilePath& db_path,
                         const leveldb::Options& options) {
  leveldb::Status status = leveldb::DestroyDB(db_path.AsUTF8Unsafe(), options);
  if (!status.ok())
    return status;

  if (options.env && leveldb_chrome::IsMemEnv(options.env)) {
    // DeleteEnvDirectory isn't recursive, but this function assumes that (for
    // in-memory env's only) leveldb is the only one writing to the Env so this
    // is OK.
    return DeleteEnvDirectory(db_path.AsUTF8Unsafe(), options.env);
  }

  // TODO(cmumford): To be fully safe this implementation should acquire a lock
  // as there is some daylight in between DestroyDB and DeleteFile.
  if (!base::DeleteFile(db_path, true)) {
    // Only delete the directory when when DestroyDB is successful. This is
    // because DestroyDB checks for database locks, and will fail if in use.
    return leveldb::Status::IOError(db_path.AsUTF8Unsafe(), "Error deleting");
  }

  return leveldb::Status::OK();
}

MemoryAllocatorDump* GetEnvAllocatorDump(ProcessMemoryDump* pmd,
                                         leveldb::Env* tracked_memenv) {
  DCHECK(Globals::GetInstance()->IsInMemoryEnv(tracked_memenv))
      << std::hex << tracked_memenv << " is not tracked";
  return pmd->GetAllocatorDump(GetDumpNameForMemEnv(tracked_memenv));
}

void DumpAllTrackedEnvs(ProcessMemoryDump* pmd) {
  Globals::GetInstance()->DumpAllTrackedEnvs(pmd->dump_args(), pmd);
}

}  // namespace leveldb_chrome
