// Copyright 2015 The Crashpad Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "client/prune_crash_reports.h"

#include <sys/stat.h>
#include <stdint.h>

#include <algorithm>
#include <vector>

#include "base/logging.h"
#include "base/notreached.h"
#include "build/build_config.h"

namespace crashpad {

size_t PruneCrashReportDatabase(CrashReportDatabase* database,
                              PruneCondition* condition) {
  std::vector<CrashReportDatabase::Report> all_reports;
  CrashReportDatabase::OperationStatus status;

  status = database->GetPendingReports(&all_reports);
  if (status != CrashReportDatabase::kNoError) {
    LOG(ERROR) << "PruneCrashReportDatabase: Failed to get pending reports";
    return 0;
  }

  std::vector<CrashReportDatabase::Report> completed_reports;
  status = database->GetCompletedReports(&completed_reports);
  if (status != CrashReportDatabase::kNoError) {
    LOG(ERROR) << "PruneCrashReportDatabase: Failed to get completed reports";
    return 0;
  }
  all_reports.insert(all_reports.end(), completed_reports.begin(),
                     completed_reports.end());

  std::sort(all_reports.begin(), all_reports.end(),
      [](const CrashReportDatabase::Report& lhs,
         const CrashReportDatabase::Report& rhs) {
        return lhs.creation_time > rhs.creation_time;
      });

  size_t num_pruned = 0;
  for (const auto& report : all_reports) {
    if (condition->ShouldPruneReport(report)) {
      status = database->DeleteReport(report.uuid);
      if (status != CrashReportDatabase::kNoError) {
        LOG(ERROR) << "Database Pruning: Failed to remove report "
                   << report.uuid.ToString();
      } else {
        num_pruned++;
      }
    }
  }

  return num_pruned;

  // TODO(rsesek): For databases that do not use a directory structure, it is
  // possible for the metadata sidecar to become corrupted and thus leave
  // orphaned crash report files on-disk. https://crashpad.chromium.org/bug/66
}

// static
std::unique_ptr<PruneCondition> PruneCondition::GetDefault() {
  // DatabaseSizePruneCondition must be the LHS so that it is always evaluated,
  // due to the short-circuting behavior of BinaryPruneCondition.
  return std::make_unique<BinaryPruneCondition>(
      BinaryPruneCondition::OR,
      new DatabaseSizePruneCondition(1024 * 128),
      new AgePruneCondition(365));
}

static const time_t kSecondsInDay = 60 * 60 * 24;

AgePruneCondition::AgePruneCondition(int max_age_in_days)
    : oldest_report_time_(
        ((time(nullptr) - (max_age_in_days * kSecondsInDay))
             / kSecondsInDay) * kSecondsInDay) {}

AgePruneCondition::~AgePruneCondition() {}

bool AgePruneCondition::ShouldPruneReport(
    const CrashReportDatabase::Report& report) {
  return report.creation_time < oldest_report_time_;
}

DatabaseSizePruneCondition::DatabaseSizePruneCondition(size_t max_size_in_kb)
    : max_size_in_kb_(max_size_in_kb), measured_size_in_kb_(0) {}

DatabaseSizePruneCondition::~DatabaseSizePruneCondition() {}

bool DatabaseSizePruneCondition::ShouldPruneReport(
    const CrashReportDatabase::Report& report) {
  // Round up fractional KB to the next 1-KB boundary.
  measured_size_in_kb_ +=
      static_cast<size_t>((report.total_size + 1023) / 1024);
  return measured_size_in_kb_ > max_size_in_kb_;
}

BinaryPruneCondition::BinaryPruneCondition(
    Operator op, PruneCondition* lhs, PruneCondition* rhs)
    : op_(op), lhs_(lhs), rhs_(rhs) {}

BinaryPruneCondition::~BinaryPruneCondition() {}

bool BinaryPruneCondition::ShouldPruneReport(
    const CrashReportDatabase::Report& report) {
  switch (op_) {
    case AND:
      return lhs_->ShouldPruneReport(report) && rhs_->ShouldPruneReport(report);
    case OR:
      return lhs_->ShouldPruneReport(report) || rhs_->ShouldPruneReport(report);
  }
  NOTREACHED();
}

}  // namespace crashpad
