// Copyright (c) 2012 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 "storage/browser/fileapi/sandbox_origin_database.h"

#include <stdint.h>

#include <set>
#include <utility>

#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/format_macros.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/metrics/histogram.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "storage/common/fileapi/file_system_util.h"
#include "third_party/leveldatabase/env_chromium.h"
#include "third_party/leveldatabase/src/include/leveldb/db.h"
#include "third_party/leveldatabase/src/include/leveldb/write_batch.h"

namespace {

const base::FilePath::CharType kOriginDatabaseName[] =
    FILE_PATH_LITERAL("Origins");
const char kOriginKeyPrefix[] = "ORIGIN:";
const char kLastPathKey[] = "LAST_PATH";
const int64_t kMinimumReportIntervalHours = 1;
const char kInitStatusHistogramLabel[] = "FileSystem.OriginDatabaseInit";
const char kDatabaseRepairHistogramLabel[] = "FileSystem.OriginDatabaseRepair";

enum InitStatus {
  INIT_STATUS_OK = 0,
  INIT_STATUS_CORRUPTION,
  INIT_STATUS_IO_ERROR,
  INIT_STATUS_UNKNOWN_ERROR,
  INIT_STATUS_MAX
};

enum RepairResult {
  DB_REPAIR_SUCCEEDED = 0,
  DB_REPAIR_FAILED,
  DB_REPAIR_MAX
};

std::string OriginToOriginKey(const std::string& origin) {
  std::string key(kOriginKeyPrefix);
  return key + origin;
}

const char* LastPathKey() {
  return kLastPathKey;
}

}  // namespace

namespace storage {

SandboxOriginDatabase::SandboxOriginDatabase(
    const base::FilePath& file_system_directory,
    leveldb::Env* env_override)
    : file_system_directory_(file_system_directory),
      env_override_(env_override) {
}

SandboxOriginDatabase::~SandboxOriginDatabase() {
}

bool SandboxOriginDatabase::Init(InitOption init_option,
                                 RecoveryOption recovery_option) {
  if (db_)
    return true;

  base::FilePath db_path = GetDatabasePath();
  if (init_option == FAIL_IF_NONEXISTENT && !base::PathExists(db_path))
    return false;

  std::string path = FilePathToString(db_path);
  leveldb::Options options;
  options.max_open_files = 0;  // Use minimum.
  options.create_if_missing = true;
  options.reuse_logs = leveldb_env::kDefaultLogReuseOptionValue;
  if (env_override_)
    options.env = env_override_;
  leveldb::DB* db;
  leveldb::Status status = leveldb::DB::Open(options, path, &db);
  ReportInitStatus(status);
  if (status.ok()) {
    db_.reset(db);
    return true;
  }
  HandleError(FROM_HERE, status);

  // Corruption due to missing necessary MANIFEST-* file causes IOError instead
  // of Corruption error.
  // Try to repair database even when IOError case.
  if (!status.IsCorruption() && !status.IsIOError())
    return false;

  switch (recovery_option) {
    case FAIL_ON_CORRUPTION:
      return false;
    case REPAIR_ON_CORRUPTION:
      LOG(WARNING) << "Attempting to repair SandboxOriginDatabase.";

      if (RepairDatabase(path)) {
        UMA_HISTOGRAM_ENUMERATION(kDatabaseRepairHistogramLabel,
                                  DB_REPAIR_SUCCEEDED, DB_REPAIR_MAX);
        LOG(WARNING) << "Repairing SandboxOriginDatabase completed.";
        return true;
      }
      UMA_HISTOGRAM_ENUMERATION(kDatabaseRepairHistogramLabel,
                                DB_REPAIR_FAILED, DB_REPAIR_MAX);
      // fall through
    case DELETE_ON_CORRUPTION:
      if (!base::DeleteFile(file_system_directory_, true))
        return false;
      if (!base::CreateDirectory(file_system_directory_))
        return false;
      return Init(init_option, FAIL_ON_CORRUPTION);
  }
  NOTREACHED();
  return false;
}

bool SandboxOriginDatabase::RepairDatabase(const std::string& db_path) {
  DCHECK(!db_.get());
  leveldb::Options options;
  options.max_open_files = 0;  // Use minimum.
  if (env_override_)
    options.env = env_override_;
  if (!leveldb::RepairDB(db_path, options).ok() ||
      !Init(FAIL_IF_NONEXISTENT, FAIL_ON_CORRUPTION)) {
    LOG(WARNING) << "Failed to repair SandboxOriginDatabase.";
    return false;
  }

  // See if the repaired entries match with what we have on disk.
  std::set<base::FilePath> directories;
  base::FileEnumerator file_enum(file_system_directory_,
                                 false /* recursive */,
                                 base::FileEnumerator::DIRECTORIES);
  base::FilePath path_each;
  while (!(path_each = file_enum.Next()).empty())
    directories.insert(path_each.BaseName());
  std::set<base::FilePath>::iterator db_dir_itr =
      directories.find(base::FilePath(kOriginDatabaseName));
  // Make sure we have the database file in its directory and therefore we are
  // working on the correct path.
  DCHECK(db_dir_itr != directories.end());
  directories.erase(db_dir_itr);

  std::vector<OriginRecord> origins;
  if (!ListAllOrigins(&origins)) {
    DropDatabase();
    return false;
  }

  // Delete any obsolete entries from the origins database.
  for (std::vector<OriginRecord>::iterator db_origin_itr = origins.begin();
       db_origin_itr != origins.end();
       ++db_origin_itr) {
    std::set<base::FilePath>::iterator dir_itr =
        directories.find(db_origin_itr->path);
    if (dir_itr == directories.end()) {
      if (!RemovePathForOrigin(db_origin_itr->origin)) {
        DropDatabase();
        return false;
      }
    } else {
      directories.erase(dir_itr);
    }
  }

  // Delete any directories not listed in the origins database.
  for (std::set<base::FilePath>::iterator dir_itr = directories.begin();
       dir_itr != directories.end();
       ++dir_itr) {
    if (!base::DeleteFile(file_system_directory_.Append(*dir_itr),
                           true /* recursive */)) {
      DropDatabase();
      return false;
    }
  }

  return true;
}

void SandboxOriginDatabase::HandleError(
    const tracked_objects::Location& from_here,
    const leveldb::Status& status) {
  db_.reset();
  LOG(ERROR) << "SandboxOriginDatabase failed at: "
             << from_here.ToString() << " with error: " << status.ToString();
}

void SandboxOriginDatabase::ReportInitStatus(const leveldb::Status& status) {
  base::Time now = base::Time::Now();
  base::TimeDelta minimum_interval =
      base::TimeDelta::FromHours(kMinimumReportIntervalHours);
  if (last_reported_time_ + minimum_interval >= now)
    return;
  last_reported_time_ = now;

  if (status.ok()) {
    UMA_HISTOGRAM_ENUMERATION(kInitStatusHistogramLabel,
                              INIT_STATUS_OK, INIT_STATUS_MAX);
  } else if (status.IsCorruption()) {
    UMA_HISTOGRAM_ENUMERATION(kInitStatusHistogramLabel,
                              INIT_STATUS_CORRUPTION, INIT_STATUS_MAX);
  } else if (status.IsIOError()) {
    UMA_HISTOGRAM_ENUMERATION(kInitStatusHistogramLabel,
                              INIT_STATUS_IO_ERROR, INIT_STATUS_MAX);
  } else {
    UMA_HISTOGRAM_ENUMERATION(kInitStatusHistogramLabel,
                              INIT_STATUS_UNKNOWN_ERROR, INIT_STATUS_MAX);
  }
}

bool SandboxOriginDatabase::HasOriginPath(const std::string& origin) {
  if (!Init(FAIL_IF_NONEXISTENT, REPAIR_ON_CORRUPTION))
    return false;
  if (origin.empty())
    return false;
  std::string path;
  leveldb::Status status =
      db_->Get(leveldb::ReadOptions(), OriginToOriginKey(origin), &path);
  if (status.ok())
    return true;
  if (status.IsNotFound())
    return false;
  HandleError(FROM_HERE, status);
  return false;
}

bool SandboxOriginDatabase::GetPathForOrigin(
    const std::string& origin, base::FilePath* directory) {
  if (!Init(CREATE_IF_NONEXISTENT, REPAIR_ON_CORRUPTION))
    return false;
  DCHECK(directory);
  if (origin.empty())
    return false;
  std::string path_string;
  std::string origin_key = OriginToOriginKey(origin);
  leveldb::Status status =
      db_->Get(leveldb::ReadOptions(), origin_key, &path_string);
  if (status.IsNotFound()) {
    int last_path_number;
    if (!GetLastPathNumber(&last_path_number))
      return false;
    path_string = base::StringPrintf("%03u", last_path_number + 1);
    // store both back as a single transaction
    leveldb::WriteBatch batch;
    batch.Put(LastPathKey(), path_string);
    batch.Put(origin_key, path_string);
    status = db_->Write(leveldb::WriteOptions(), &batch);
    if (!status.ok()) {
      HandleError(FROM_HERE, status);
      return false;
    }
  }
  if (status.ok()) {
    *directory = StringToFilePath(path_string);
    return true;
  }
  HandleError(FROM_HERE, status);
  return false;
}

bool SandboxOriginDatabase::RemovePathForOrigin(const std::string& origin) {
  if (!Init(CREATE_IF_NONEXISTENT, REPAIR_ON_CORRUPTION))
    return false;
  leveldb::Status status =
      db_->Delete(leveldb::WriteOptions(), OriginToOriginKey(origin));
  if (status.ok() || status.IsNotFound())
    return true;
  HandleError(FROM_HERE, status);
  return false;
}

bool SandboxOriginDatabase::ListAllOrigins(
    std::vector<OriginRecord>* origins) {
  DCHECK(origins);
  if (!Init(CREATE_IF_NONEXISTENT, REPAIR_ON_CORRUPTION)) {
    origins->clear();
    return false;
  }
  scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(leveldb::ReadOptions()));
  std::string origin_key_prefix = OriginToOriginKey(std::string());
  iter->Seek(origin_key_prefix);
  origins->clear();
  while (iter->Valid() && base::StartsWith(iter->key().ToString(),
                                           origin_key_prefix,
                                           base::CompareCase::SENSITIVE)) {
    std::string origin =
      iter->key().ToString().substr(origin_key_prefix.length());
    base::FilePath path = StringToFilePath(iter->value().ToString());
    origins->push_back(OriginRecord(origin, path));
    iter->Next();
  }
  return true;
}

void SandboxOriginDatabase::DropDatabase() {
  db_.reset();
}

base::FilePath SandboxOriginDatabase::GetDatabasePath() const {
  return file_system_directory_.Append(kOriginDatabaseName);
}

void SandboxOriginDatabase::RemoveDatabase() {
  DropDatabase();
  base::DeleteFile(GetDatabasePath(), true /* recursive */);
}

bool SandboxOriginDatabase::GetLastPathNumber(int* number) {
  DCHECK(db_);
  DCHECK(number);
  std::string number_string;
  leveldb::Status status =
      db_->Get(leveldb::ReadOptions(), LastPathKey(), &number_string);
  if (status.ok())
    return base::StringToInt(number_string, number);
  if (!status.IsNotFound()) {
    HandleError(FROM_HERE, status);
    return false;
  }
  // Verify that this is a totally new database, and initialize it.
  scoped_ptr<leveldb::Iterator> iter(db_->NewIterator(leveldb::ReadOptions()));
  iter->SeekToFirst();
  if (iter->Valid()) {  // DB was not empty, but had no last path number!
    LOG(ERROR) << "File system origin database is corrupt!";
    return false;
  }
  // This is always the first write into the database.  If we ever add a
  // version number, they should go in in a single transaction.
  status =
      db_->Put(leveldb::WriteOptions(), LastPathKey(), std::string("-1"));
  if (!status.ok()) {
    HandleError(FROM_HERE, status);
    return false;
  }
  *number = -1;
  return true;
}

}  // namespace storage
