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

#include "sqlite_persistent_store_backend_base.h"

#include <utility>

#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros_local.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "sql/database.h"
#include "sql/error_delegate_util.h"

#if BUILDFLAG(IS_WIN)
#include <windows.h>
#endif  // BUILDFLAG(IS_WIN)

namespace net {

SQLitePersistentStoreBackendBase::SQLitePersistentStoreBackendBase(
    const base::FilePath& path,
    std::string histogram_tag,
    const int current_version_number,
    const int compatible_version_number,
    scoped_refptr<base::SequencedTaskRunner> background_task_runner,
    scoped_refptr<base::SequencedTaskRunner> client_task_runner,
    bool enable_exclusive_access)
    : path_(path),
      histogram_tag_(std::move(histogram_tag)),
      current_version_number_(current_version_number),
      compatible_version_number_(compatible_version_number),
      background_task_runner_(std::move(background_task_runner)),
      client_task_runner_(std::move(client_task_runner)),
      enable_exclusive_access_(enable_exclusive_access) {}

SQLitePersistentStoreBackendBase::~SQLitePersistentStoreBackendBase() {
  // If `db_` hasn't been reset by the time this destructor is called,
  // a use-after-free could occur if the `db_` error callback is ever
  // invoked. To guard against this, crash if `db_` hasn't been reset
  // so that this use-after-free doesn't happen and so that we'll be
  // alerted to the fact that a closer look at this code is needed.
  CHECK(!db_.get()) << "Close should already have been called.";
}

void SQLitePersistentStoreBackendBase::Flush(base::OnceClosure callback) {
  DCHECK(!background_task_runner_->RunsTasksInCurrentSequence());
  PostBackgroundTask(
      FROM_HERE,
      base::BindOnce(
          &SQLitePersistentStoreBackendBase::FlushAndNotifyInBackground, this,
          std::move(callback)));
}

void SQLitePersistentStoreBackendBase::Close() {
  if (background_task_runner_->RunsTasksInCurrentSequence()) {
    DoCloseInBackground();
  } else {
    // Must close the backend on the background runner.
    PostBackgroundTask(
        FROM_HERE,
        base::BindOnce(&SQLitePersistentStoreBackendBase::DoCloseInBackground,
                       this));
  }
}

void SQLitePersistentStoreBackendBase::SetBeforeCommitCallback(
    base::RepeatingClosure callback) {
  base::AutoLock locked(before_commit_callback_lock_);
  before_commit_callback_ = std::move(callback);
}

bool SQLitePersistentStoreBackendBase::InitializeDatabase() {
  DCHECK(background_task_runner_->RunsTasksInCurrentSequence());

  if (initialized_ || corruption_detected_) {
    // Return false if we were previously initialized but the DB has since been
    // closed, or if corruption caused a database reset during initialization.
    return db_ != nullptr;
  }

  base::ElapsedTimer timer;

  const base::FilePath dir = path_.DirName();
  if (!base::PathExists(dir) && !base::CreateDirectory(dir)) {
    return false;
  }

  // TODO(crbug.com/1430231): Remove explicit_locking = false. This currently
  // needs to be set to false because of several failing MigrationTests.
  db_ = std::make_unique<sql::Database>(sql::DatabaseOptions{
      .exclusive_locking = false,
      .exclusive_database_file_lock = enable_exclusive_access_});

  db_->set_histogram_tag(histogram_tag_);

  // base::Unretained is safe because |this| owns (and therefore outlives) the
  // sql::Database held by |db_|.
  db_->set_error_callback(base::BindRepeating(
      &SQLitePersistentStoreBackendBase::DatabaseErrorCallback,
      base::Unretained(this)));

  bool has_been_preloaded = false;
  // It is not possible to preload a database opened with exclusive access,
  // because the file cannot be opened again to preload it. In this case,
  // preload before opening the database.
  if (enable_exclusive_access_) {
    // See coments in Database::Preload for explanation of these values.
    constexpr int kPreReadSize = 128 * 1024 * 1024;  // 128 MB
    // TODO(crbug.com/1434166): Consider moving preload behind a database
    // option.
    base::PreReadFile(path_, /*is_executable=*/false, kPreReadSize);
    has_been_preloaded = true;
  }

  if (!db_->Open(path_)) {
    DLOG(ERROR) << "Unable to open " << histogram_tag_ << " DB.";
    RecordOpenDBProblem();
    Reset();
    return false;
  }

  // Only attempt a preload if the database hasn't already been preloaded above.
  if (!has_been_preloaded) {
    db_->Preload();
  }

  if (!MigrateDatabaseSchema() || !CreateDatabaseSchema()) {
    DLOG(ERROR) << "Unable to update or initialize " << histogram_tag_
                << " DB tables.";
    RecordDBMigrationProblem();
    Reset();
    return false;
  }

  base::UmaHistogramCustomTimes(histogram_tag_ + ".TimeInitializeDB",
                                timer.Elapsed(), base::Milliseconds(1),
                                base::Minutes(1), 50);

  initialized_ = DoInitializeDatabase();

  if (!initialized_) {
    DLOG(ERROR) << "Unable to initialize " << histogram_tag_ << " DB.";
    RecordOpenDBProblem();
    Reset();
    return false;
  }

  return true;
}

bool SQLitePersistentStoreBackendBase::DoInitializeDatabase() {
  return true;
}

void SQLitePersistentStoreBackendBase::Reset() {
  if (db_ && db_->is_open())
    db_->Raze();
  meta_table_.Reset();
  db_.reset();
}

void SQLitePersistentStoreBackendBase::Commit() {
  DCHECK(background_task_runner_->RunsTasksInCurrentSequence());

  {
    base::AutoLock locked(before_commit_callback_lock_);
    if (!before_commit_callback_.is_null())
      before_commit_callback_.Run();
  }

  DoCommit();
}

void SQLitePersistentStoreBackendBase::PostBackgroundTask(
    const base::Location& origin,
    base::OnceClosure task) {
  if (!background_task_runner_->PostTask(origin, std::move(task))) {
    LOG(WARNING) << "Failed to post task from " << origin.ToString()
                 << " to background_task_runner_.";
  }
}

void SQLitePersistentStoreBackendBase::PostClientTask(
    const base::Location& origin,
    base::OnceClosure task) {
  if (!client_task_runner_->PostTask(origin, std::move(task))) {
    LOG(WARNING) << "Failed to post task from " << origin.ToString()
                 << " to client_task_runner_.";
  }
}

bool SQLitePersistentStoreBackendBase::MigrateDatabaseSchema() {
  // Version check.
  if (!meta_table_.Init(db_.get(), current_version_number_,
                        compatible_version_number_)) {
    return false;
  }

  if (meta_table_.GetCompatibleVersionNumber() > current_version_number_) {
    LOG(WARNING) << histogram_tag_ << " database is too new.";
    return false;
  }

  // |cur_version| is the version that the database ends up at, after all the
  // database upgrade statements.
  absl::optional<int> cur_version = DoMigrateDatabaseSchema();
  if (!cur_version.has_value())
    return false;

  // Metatable is corrupted. Try to recover.
  if (cur_version.value() < current_version_number_) {
    meta_table_.Reset();
    db_ = std::make_unique<sql::Database>();
    bool recovered = sql::Database::Delete(path_) && db()->Open(path_) &&
                     meta_table_.Init(db(), current_version_number_,
                                      compatible_version_number_);
    LOCAL_HISTOGRAM_BOOLEAN("Net.SQLite.CorruptMetaTableRecovered", recovered);
    if (!recovered) {
      NOTREACHED() << "Unable to reset the " << histogram_tag_ << " DB.";
      meta_table_.Reset();
      db_.reset();
      return false;
    }
  }

  return true;
}

void SQLitePersistentStoreBackendBase::FlushAndNotifyInBackground(
    base::OnceClosure callback) {
  DCHECK(background_task_runner_->RunsTasksInCurrentSequence());

  Commit();
  if (callback)
    PostClientTask(FROM_HERE, std::move(callback));
}

void SQLitePersistentStoreBackendBase::DoCloseInBackground() {
  DCHECK(background_task_runner_->RunsTasksInCurrentSequence());
  // Commit any pending operations
  Commit();

  meta_table_.Reset();
  db_.reset();
}

void SQLitePersistentStoreBackendBase::DatabaseErrorCallback(
    int error,
    sql::Statement* stmt) {
  DCHECK(background_task_runner_->RunsTasksInCurrentSequence());

  if (!sql::IsErrorCatastrophic(error))
    return;

  // TODO(shess): Running KillDatabase() multiple times should be
  // safe.
  if (corruption_detected_)
    return;

  corruption_detected_ = true;

  if (!initialized_) {
    sql::UmaHistogramSqliteResult(histogram_tag_ + ".ErrorInitializeDB", error);

#if BUILDFLAG(IS_WIN)
    base::UmaHistogramSparse(histogram_tag_ + ".WinGetLastErrorInitializeDB",
                             ::GetLastError());
#endif  // BUILDFLAG(IS_WIN)
  }

  // Don't just do the close/delete here, as we are being called by |db| and
  // that seems dangerous.
  // TODO(shess): Consider just calling RazeAndPoison() immediately.
  // db_ may not be safe to reset at this point, but RazeAndPoison()
  // would cause the stack to unwind safely with errors.
  PostBackgroundTask(
      FROM_HERE,
      base::BindOnce(&SQLitePersistentStoreBackendBase::KillDatabase, this));
}

void SQLitePersistentStoreBackendBase::KillDatabase() {
  DCHECK(background_task_runner_->RunsTasksInCurrentSequence());

  if (db_) {
    // This Backend will now be in-memory only. In a future run we will recreate
    // the database. Hopefully things go better then!
    db_->RazeAndPoison();
    meta_table_.Reset();
    db_.reset();
  }
}

}  // namespace net
