// 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.

#ifndef CRASHPAD_CLIENT_CRASH_REPORT_DATABASE_H_
#define CRASHPAD_CLIENT_CRASH_REPORT_DATABASE_H_

#include <stdint.h>
#include <time.h>

#include <map>
#include <memory>
#include <string>
#include <vector>

#include "base/files/file_path.h"
#include "util/file/file_io.h"
#include "util/file/file_reader.h"
#include "util/file/file_writer.h"
#include "util/file/scoped_remove_file.h"
#include "util/misc/metrics.h"
#include "util/misc/uuid.h"

namespace crashpad {

class Settings;
class SettingsReader;

//! \brief An interface for managing a collection of crash report files and
//!     metadata associated with the crash reports.
//!
//! All Report objects that are returned by this class are logically const.
//! They are snapshots of the database at the time the query was run, and the
//! data returned is liable to change after the query is executed.
//!
//! The lifecycle of a crash report has three stages:
//!
//!   1. New: A crash report is created with PrepareNewCrashReport(), the
//!      the client then writes the report, and then calls
//!      FinishedWritingCrashReport() to make the report Pending.
//!   2. Pending: The report has been written but has not been locally
//!      processed, or it was has been brought back from 'Completed' state by
//!      user request.
//!   3. Completed: The report has been locally processed, either by uploading
//!      it to a collection server and calling RecordUploadComplete(), or by
//!      calling SkipReportUpload().
class CrashReportDatabase {
 public:
  //! \brief A crash report record.
  //!
  //! This represents the metadata for a crash report, as well as the location
  //! of the report itself. A CrashReportDatabase maintains at least this
  //! information.
  struct Report {
    Report();

    //! A unique identifier by which this report will always be known to the
    //! database.
    UUID uuid;

    //! The current location of the crash report on the client’s filesystem.
    //! The location of a crash report may change over time, so the UUID should
    //! be used as the canonical identifier.
    base::FilePath file_path;

    //! An identifier issued to this crash report by a collection server.
    std::string id;

    //! The time at which the report was generated.
    time_t creation_time;

    //! Whether this crash report was successfully uploaded to a collection
    //! server.
    bool uploaded;

    //! The last timestamp at which an attempt was made to submit this crash
    //! report to a collection server. If this is zero, then the report has
    //! never been uploaded. If #uploaded is true, then this timestamp is the
    //! time at which the report was uploaded, and no other attempts to upload
    //! this report will be made.
    time_t last_upload_attempt_time;

    //! The number of times an attempt was made to submit this report to
    //! a collection server. If this is more than zero, then
    //! #last_upload_attempt_time will be set to the timestamp of the most
    //! recent attempt.
    int upload_attempts;

    //! Whether this crash report was explicitly requested by user to be
    //! uploaded. This can be true only if report is in the 'pending' state.
    bool upload_explicitly_requested;

    //! The total size in bytes taken by the report, including any potential
    //! attachments.
    uint64_t total_size;
  };

  //! \brief A crash report that is in the process of being written.
  //!
  //! An instance of this class should be created via PrepareNewCrashReport().
  class NewReport {
   public:
    NewReport();

    NewReport(const NewReport&) = delete;
    NewReport& operator=(const NewReport&) = delete;

    ~NewReport();

    //! \brief An open FileWriter with which to write the report.
    FileWriter* Writer() const { return writer_.get(); }

    //! \brief Returns a FileReaderInterface to the report, or `nullptr` with a
    //!     message logged.
    FileReaderInterface* Reader();

    //! A unique identifier by which this report will always be known to the
    //! database.
    const UUID& ReportID() const { return uuid_; }

    //! \brief Adds an attachment to the report.
    //!
    //! \param[in] name The key and name for the attachment, which will be
    //!     included in the http upload. The attachment will not appear in the
    //!     minidump report. \a name should only use characters from the set
    //!     `[a-zA-Z0-9._-]`.
    //! \return A FileWriter that the caller should use to write the contents of
    //!     the attachment, or `nullptr` on failure with an error logged.
    FileWriter* AddAttachment(const std::string& name);

   private:
    friend class CrashReportDatabaseGeneric;
    friend class CrashReportDatabaseMac;
    friend class CrashReportDatabaseWin;

    bool Initialize(CrashReportDatabase* database,
                    const base::FilePath& directory,
                    const base::FilePath::StringType& extension,
                    const UUID* uuid = nullptr);

    std::unique_ptr<FileWriter> writer_;
    std::unique_ptr<FileReader> reader_;
    ScopedRemoveFile file_remover_;
    std::vector<std::unique_ptr<FileWriter>> attachment_writers_;
    std::vector<ScopedRemoveFile> attachment_removers_;
    UUID uuid_;
    CrashReportDatabase* database_;
  };

  //! \brief A crash report that is in the process of being uploaded.
  //!
  //! An instance of this class should be created via GetReportForUploading().
  class UploadReport : public Report {
   public:
    UploadReport();

    UploadReport(const UploadReport&) = delete;
    UploadReport& operator=(const UploadReport&) = delete;

    virtual ~UploadReport();

    //! \brief An open FileReader with which to read the report.
    FileReader* Reader() const { return reader_.get(); }

    //! \brief Obtains a mapping of names to file readers for any attachments
    //!     for the report.
    std::map<std::string, FileReader*> GetAttachments() const {
      return attachment_map_;
    }

   private:
    friend class CrashReportDatabase;
    friend class CrashReportDatabaseGeneric;
    friend class CrashReportDatabaseMac;
    friend class CrashReportDatabaseWin;

    bool Initialize(const base::FilePath& path, CrashReportDatabase* database);
    void InitializeAttachments();

    std::unique_ptr<FileReader> reader_;
    CrashReportDatabase* database_;
    std::vector<std::unique_ptr<FileReader>> attachment_readers_;
    std::map<std::string, FileReader*> attachment_map_;
    bool report_metrics_;
  };

  //! \brief A helper class for creating crash report envelopes that aggregate
  //! crash data.
  class Envelope {
   public:
    Envelope(const UUID& uuid);

    Envelope(const Envelope&) = delete;
    Envelope& operator=(const Envelope&) = delete;

    ~Envelope() = default;

    //! \brief Initializes the feedback report with the given file path.
    //! \param[in] path The path where the feedback report will be written.
    //! \return true on success, false on failure.
    bool Initialize(const base::FilePath& path);

    //! \brief Adds attachments to the feedback report.
    //! \param[in] attachments Vector of file paths to attach.
    void AddAttachments(const std::vector<base::FilePath>& attachments);

    //! \brief Adds minidump data to the feedback report.
    //! \param[in] reader File reader for the minidump data.
    void AddMinidump(FileReaderInterface* reader);

    //! \brief Finalizes the feedback report and closes file handles.
    void Finish();

   private:
    void AddEvent(const base::FilePath& event,
                  const std::vector<base::FilePath>& breadcrumbs);
    void AddAttachment(const base::FilePath& attachment);

    UUID uuid_;
    base::FilePath path_;
    std::unique_ptr<FileWriter> writer_;
  };

  //! \brief The result code for operations performed on a database.
  enum OperationStatus {
    //! \brief No error occurred.
    kNoError = 0,

    //! \brief The report that was requested could not be located.
    //!
    //! This may occur when the report is present in the database but not in a
    //! state appropriate for the requested operation, for example, if
    //! GetReportForUploading() is called to obtain report that’s already in the
    //! completed state.
    kReportNotFound,

    //! \brief An error occured while performing a file operation on a crash
    //!     report.
    //!
    //! A database is responsible for managing both the metadata about a report
    //! and the actual crash report itself. This error is returned when an
    //! error occurred when managing the report file. Additional information
    //! will be logged.
    kFileSystemError,

    //! \brief An error occured while recording metadata for a crash report or
    //!     database-wide settings.
    //!
    //! A database is responsible for managing both the metadata about a report
    //! and the actual crash report itself. This error is returned when an
    //! error occurred when managing the metadata about a crash report or
    //! database-wide settings. Additional information will be logged.
    kDatabaseError,

    //! \brief The operation could not be completed because a concurrent
    //!     operation affecting the report is occurring.
    kBusyError,

    //! \brief The report cannot be uploaded by user request as it has already
    //!     been uploaded.
    kCannotRequestUpload,
  };

  CrashReportDatabase(const CrashReportDatabase&) = delete;
  CrashReportDatabase& operator=(const CrashReportDatabase&) = delete;

  virtual ~CrashReportDatabase() {}

  //! \brief Opens a database of crash reports, possibly creating it.
  //!
  //! \param[in] path A path to the database to be created or opened. If the
  //!     database does not yet exist, it will be created if possible. Note that
  //!     for databases implemented as directory structures, existence refers
  //!     solely to the outermost directory.
  //!
  //! \return A database object on success, `nullptr` on failure with an error
  //!     logged.
  //!
  //! \sa InitializeWithoutCreating
  static std::unique_ptr<CrashReportDatabase> Initialize(
      const base::FilePath& path);

  //! \brief Opens an existing database of crash reports.
  //!
  //! \param[in] path A path to the database to be opened. If the database does
  //!     not yet exist, it will not be created. Note that for databases
  //!     implemented as directory structures, existence refers solely to the
  //!     outermost directory. On such databases, as long as the outermost
  //!     directory is present, this method will create the inner structure.
  //!
  //! \return A database object on success, `nullptr` on failure with an error
  //!     logged.
  //!
  //! \sa Initialize
  static std::unique_ptr<CrashReportDatabase> InitializeWithoutCreating(
      const base::FilePath& path);

  //! \brief Given a database path, return a read-only view of its settings.
  //!
  //! \param[in] path A path to the database. If the database does not exist, or
  //!     the settings file does not exist, the returned reader will fail its
  //!     read methods.
  //!
  //! \return A SettingsReader.
  static std::unique_ptr<SettingsReader> GetSettingsReaderForDatabasePath(
      const base::FilePath& path);

  //! \brief Returns the Settings object for this database.
  //!
  //! \return A weak pointer to the Settings object, which is owned by the
  //!     database.
  virtual Settings* GetSettings() = 0;

  //! \brief Creates a record of a new crash report.
  //!
  //! Callers should write the crash report using the FileWriter provided.
  //! Callers should then call FinishedWritingCrashReport() to complete report
  //! creation. If an error is encountered while writing the crash report, no
  //! special action needs to be taken. If FinishedWritingCrashReport() is not
  //! called, the report will be removed from the database when \a report is
  //! destroyed.
  //!
  //! \param[out] report A NewReport object containing a FileWriter with which
  //!     to write the report data. Only valid if this returns #kNoError.
  //! \param[in] uuid Optional caller-provided UUID to use for this report. If
  //!     null, the database generates one.
  //!
  //! \return The operation status code.
  virtual OperationStatus PrepareNewCrashReport(
      std::unique_ptr<NewReport>* report, const UUID* uuid = nullptr) = 0;

  //! \brief Informs the database that a crash report has been successfully
  //!     written.
  //!
  //! \param[in] report A NewReport obtained with PrepareNewCrashReport(). The
  //!     NewReport object will be invalidated as part of this call.
  //! \param[out] uuid The UUID of this crash report.
  //!
  //! \return The operation status code.
  virtual OperationStatus FinishedWritingCrashReport(
      std::unique_ptr<NewReport> report,
      UUID* uuid) = 0;

  //! \brief Returns the crash report record for the unique identifier.
  //!
  //! \param[in] uuid The crash report record unique identifier.
  //! \param[out] report A crash report record. Only valid if this returns
  //!     #kNoError.
  //!
  //! \return The operation status code.
  virtual OperationStatus LookUpCrashReport(const UUID& uuid,
                                            Report* report) = 0;

  //! \brief Returns a list of crash report records that have not been uploaded.
  //!
  //! \param[out] reports A list of crash report record objects. This must be
  //!     empty on entry. Only valid if this returns #kNoError.
  //!
  //! \return The operation status code.
  virtual OperationStatus GetPendingReports(std::vector<Report>* reports) = 0;

  //! \brief Returns a list of crash report records that have been completed,
  //!     either by being uploaded or by skipping upload.
  //!
  //! \param[out] reports A list of crash report record objects. This must be
  //!     empty on entry. Only valid if this returns #kNoError.
  //!
  //! \return The operation status code.
  virtual OperationStatus GetCompletedReports(std::vector<Report>* reports) = 0;

  //! \brief Obtains and locks a report object for uploading to a collection
  //!     server. On iOS the file lock is released and mutual-exclusion is kept
  //!     via a file attribute.
  //!
  //! Callers should upload the crash report using the FileReader provided.
  //! Callers should then call RecordUploadComplete() to record a successful
  //! upload. If RecordUploadComplete() is not called, the upload attempt will
  //! be recorded as unsuccessful and the report lock released when \a report is
  //! destroyed.
  //!
  //! On iOS, holding a lock during a slow upload can lead to watchdog kills if
  //! the app is suspended mid-upload. Instead, if the client can obtain the
  //! lock, the database sets a lock-time file attribute and releases the lock.
  //! The attribute is cleared when the upload is completed. The lock-time
  //! attribute can be used to prevent file access from other processes, or to
  //! discard reports that likely were terminated mid-upload.
  //!
  //! \param[in] uuid The unique identifier for the crash report record.
  //! \param[out] report A crash report record for the report to be uploaded.
  //!     Only valid if this returns #kNoError.
  //! \param[in] report_metrics If `false`, metrics will not be recorded for
  //!     this upload attempt when RecordUploadComplete() is called or \a report
  //!     is destroyed. Metadata for the upload attempt will still be recorded
  //!     in the database.
  //!
  //! \return The operation status code.
  virtual OperationStatus GetReportForUploading(
      const UUID& uuid,
      std::unique_ptr<const UploadReport>* report,
      bool report_metrics = true) = 0;

  //! \brief Records a successful upload for a report and updates the last
  //!     upload attempt time as returned by
  //!     Settings::GetLastUploadAttemptTime().
  //!
  //! \param[in] report A UploadReport object obtained from
  //!     GetReportForUploading(). The UploadReport object will be invalidated
  //!     and the report unlocked as part of this call.
  //! \param[in] id The possibly empty identifier assigned to this crash report
  //!     by the collection server.
  //!
  //! \return The operation status code.
  OperationStatus RecordUploadComplete(
      std::unique_ptr<const UploadReport> report,
      const std::string& id);

  //! \brief Moves a report from the pending state to the completed state, but
  //!     without the report being uploaded.
  //!
  //! This can be used if the user has disabled crash report collection, but
  //! crash generation is still enabled in the product.
  //!
  //! \param[in] uuid The unique identifier for the crash report record.
  //! \param[in] reason The reason the report upload is being skipped for
  //!     metrics tracking purposes.
  //!
  //! \return The operation status code.
  virtual OperationStatus SkipReportUpload(
      const UUID& uuid,
      Metrics::CrashSkippedReason reason) = 0;

  //! \brief Deletes a crash report file and its associated metadata.
  //!
  //! \param[in] uuid The UUID of the report to delete.
  //!
  //! \return The operation status code.
  virtual OperationStatus DeleteReport(const UUID& uuid) = 0;

  //! \brief Marks a crash report as explicitly requested to be uploaded by the
  //!     user and moves it to 'pending' state.
  //!
  //! \param[in] uuid The unique identifier for the crash report record.
  //!
  //! \return The operation status code.
  virtual OperationStatus RequestUpload(const UUID& uuid) = 0;

  //! \brief Cleans the database of expired lockfiles, metadata without report
  //!     files, report files without metadata, and attachments without report
  //!     files.
  //!
  //! As the macOS implementation does not use  lock or metadata files, the
  //! cleaning is limited to attachments without report files.
  //!
  //! \param[in] lockfile_ttl The number of seconds at which lockfiles or new
  //!     report files are considered expired.
  //! \return The number of reports cleaned.
  virtual int CleanDatabase(time_t lockfile_ttl) { return 0; }

  //! \brief Launches the external crash reporter.
  //!
  //! This method launches an external crash reporter process with the
  //! provided crash report file. The implementation is platform-specific and
  //! may have different behaviors on different operating systems.
  //!
  //! \param[in] crash_reporter The path to the crash reporter executable.
  //!     On macOS, this can be either an executable or an .app bundle.
  //! \param[in] crash_envelope The path to the crash report envelope file that
  //!     will be passed as an argument to the crash reporter.
  //!
  //! \note The launched process runs detached from the parent process.
  //! \note This method may not be implemented on all platforms.
  virtual void LaunchCrashReporter(const base::FilePath& crash_reporter,
                                   const base::FilePath& crash_envelope) {}

 protected:
  CrashReportDatabase() = default;

  //! \brief The path to the database passed to Initialize.
  //!
  //! \return The filepath of the database;
  virtual base::FilePath DatabasePath() = 0;

  //! \brief Build a filepath for the root attachments directory.
  //!
  //! \return The filepath to the attachments directory.
  base::FilePath AttachmentsRootPath();

  //! \brief  Build a filepath for the directory for the report to hold
  //!     attachments.
  //!
  //! \param[in] uuid The unique identifier for the crash report record.
  //!
  //! \return The filepath to the report attachments directory.
  base::FilePath AttachmentsPath(const UUID& uuid);

  //! \brief Attempts to remove any attachments associated with the given
  //!     report UUID. There may not be any, so failing is not an error.
  //!
  //! \param[in] uuid The unique identifier for the crash report record.
  void RemoveAttachmentsByUUID(const UUID& uuid);

 private:
  //! \brief Adjusts a crash report record’s metadata to account for an upload
  //!     attempt, and updates the last upload attempt time as returned by
  //!     Settings::GetLastUploadAttemptTime().
  //!
  //! \param[in] report The report object obtained from
  //!     GetReportForUploading().
  //! \param[in] successful Whether the upload attempt was successful.
  //! \param[in] id The identifier assigned to this crash report by the
  //!     collection server. Must be empty if \a successful is `false`; may be
  //!     empty if it is `true`.
  //!
  //! \return The operation status code.
  virtual OperationStatus RecordUploadAttempt(UploadReport* report,
                                              bool successful,
                                              const std::string& id) = 0;
};

}  // namespace crashpad

#endif  // CRASHPAD_CLIENT_CRASH_REPORT_DATABASE_H_
