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

#ifndef COMPONENTS_SESSIONS_CORE_COMMAND_STORAGE_BACKEND_H_
#define COMPONENTS_SESSIONS_CORE_COMMAND_STORAGE_BACKEND_H_

#include <stddef.h>

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

#include "base/files/file_path.h"
#include "base/functional/callback_forward.h"
#include "base/memory/ref_counted.h"
#include "base/memory/ref_counted_delete_on_sequence.h"
#include "base/memory/scoped_refptr.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "components/os_crypt/async/common/encryptor.h"
#include "components/sessions/core/command_storage_manager.h"
#include "components/sessions/core/session_command.h"
#include "components/sessions/core/sessions_export.h"

namespace base {
class Clock;
class File;
}  // namespace base

namespace sessions {

// CommandStorageBackend is the backend used by CommandStorageManager. It writes
// SessionCommands to disk with the ability to read back at a later date.
// CommandStorageBackend (mostly) does not interpret the commands in any way, it
// simply reads/writes them.
//
// CommandStorageBackend writes to a file with a suffix that indicates the
// time the file was opened. The time stamp allows this code to determine the
// most recently written file. When AppendCommands() is supplied a value of true
// for `truncate`, the current file is closed and a new file is created (with
// a newly generated timestamp). When AppendCommands() successfully writes the
// commands to the file an internal command (whose id is
// `kInitialStateMarkerCommandId`) is written. During startup, the most recent
// file that has the internal command written is used. This ensures restore does
// not attempt to use a file that did not have the complete state written
// (this would happen if chrome crashed while writing the commands, or there
// was a file system error part way through writing).
//
// AppendCommands() takes a callback that is called if there is an error in
// writing to the file. The expectation is if there is an error, the consuming
// code must call AppendCommands() again with `truncate` set to true. If there
// was an error in writing to the file, calls to AppendCommands() with a value
// of false for `truncate` are ignored. This is done to ensure the consuming
// code correctly supplies the initial state.
class SESSIONS_EXPORT CommandStorageBackend
    : public base::RefCountedDeleteOnSequence<CommandStorageBackend> {
 public:
  struct SESSIONS_EXPORT ReadCommandsResult {
    ReadCommandsResult();
    ReadCommandsResult(ReadCommandsResult&& other);
    ReadCommandsResult& operator=(ReadCommandsResult&& other);
    ReadCommandsResult(const ReadCommandsResult&) = delete;
    ReadCommandsResult& operator=(const ReadCommandsResult&) = delete;
    ~ReadCommandsResult();

    std::vector<std::unique_ptr<sessions::SessionCommand>> commands;
    bool error_reading = false;
  };

  using id_type = SessionCommand::id_type;
  using size_type = SessionCommand::size_type;

  // Initial size of the buffer used in reading the file. This is exposed
  // for testing.
  static const int kFileReadBufferSize;

  // Represents data for a session. Public for tests.
  // Creates a CommandStorageBackend. This method is invoked on the MAIN thread,
  // and does no IO. The real work is done from InitIfNecessary(), which is
  // invoked on a background task runer.
  // |encryptor| may be null, in which case the file is not encrypted.
  //
  // See `CommandStorageManager` for details on `type` and `path`.
  CommandStorageBackend(
      scoped_refptr<base::SequencedTaskRunner> owning_task_runner,
      const base::FilePath& path,
      CommandStorageManager::SessionType type,
      scoped_refptr<os_crypt_async::Encryptor> encryptor,
      base::Clock* clock = nullptr);
  CommandStorageBackend(const CommandStorageBackend&) = delete;
  CommandStorageBackend& operator=(const CommandStorageBackend&) = delete;

  // Returns true if the file at |path| was generated by this class.
  static bool IsValidFileForTest(const base::FilePath& path);

  // Returns the path the files are being written to.
  const base::FilePath current_path_for_testing() const {
    return open_file_ ? open_file_->path : base::FilePath();
  }

  bool IsFileOpenForTesting() const { return open_file_.get() != nullptr; }

  base::SequencedTaskRunner* owning_task_runner() {
    return base::RefCountedDeleteOnSequence<
        CommandStorageBackend>::owning_task_runner();
  }

  // Appends the specified commands to the current file. If |truncate| is true
  // the file is truncated. If there is an error writing the commands,
  // `error_callback` is run.
  void AppendCommands(
      std::vector<std::unique_ptr<sessions::SessionCommand>> commands,
      bool truncate,
      base::OnceClosure error_callback);

  bool inited_for_testing() const { return inited_; }

  // Parses out the timestamp from a path pointing to a session file.
  static bool TimestampFromPath(const base::FilePath& path, base::Time& result);

  // Returns the commands from the last session file.
  ReadCommandsResult ReadLastSessionCommands();

  // Deletes the file containing the commands for the last session.
  void DeleteLastSession();

  // Moves the current session file to the last session file. This is typically
  // called during startup or if the user launches the app and no tabbed
  // browsers are running. After calling this, set_pending_reset() must be
  // called.
  void MoveCurrentSessionToLastSession();

  // Used in testing to emulate an error in writing to the file. The value is
  // automatically reset after the failure.
  void ForceAppendCommandsToFailForTesting();

 private:
  friend class base::RefCountedDeleteOnSequence<CommandStorageBackend>;
  friend class base::DeleteHelper<CommandStorageBackend>;
  friend class CommandStorageBackendTest;
  friend class SessionFileReader;

  struct SessionInfo {
    base::FilePath path;
    base::Time timestamp;
  };

  struct OpenFile {
    OpenFile();
    ~OpenFile();

    base::FilePath path;
    std::unique_ptr<base::File> file;
    // Set to true once `kInitialStateMarkerCommandId` is written.
    bool did_write_marker = false;
  };

  // Statuses that can occur when writing a file using AppendCommands().
  // These values are persisted to logs. Entries should not be renumbered and
  // numeric values should never be reused.
  // LINT.IfChange(WriteStatus)
  enum class WriteStatus {
    kUnknown = 0,
    kSuccess = 1,
    kFileNotOpened = 2,
    kFileWriteError = 3,
    kSerializationError = 4,
    kEncryptionUnavailable = 5,  // OSCrypt lacked permission to encrypt.
    kMaxValue = kEncryptionUnavailable,
  };
  // LINT.ThenChange(//tools/metrics/histograms/metadata/session/enums.xml:CommandStorageWriteStatus)

  static bool IsError(WriteStatus status) {
    return status != WriteStatus::kSuccess;
  }

  // Statuses that can occur when reading a file using ReadLastSessionCommands()
  // These values are persisted to logs. Entries should not be renumbered and
  // numeric values should never be reused.
  // LINT.IfChange(ReadStatus)
  enum class ReadStatus {
    kUnknown = 0,
    kSuccess = 1,  // The file was read successfully.
    kNoFile = 2,   // No file exists for the last session (not an error).
    kFileInvalid = 3,
    kFileEmpty = 4,
    kInvalidHeader = 5,
    kInvalidCommand = 6,
    kUnsupportedVersion = 7,
    kDecryptionUnavailable = 8,  // OSCrypt lacked permission to decrypt.
    kMaxValue = kDecryptionUnavailable,
  };
  // LINT.ThenChange(//tools/metrics/histograms/metadata/session/enums.xml:CommandStorageReadStatus)

  static bool IsError(ReadStatus status) {
    return status != ReadStatus::kSuccess && status != ReadStatus::kNoFile;
  }

  ~CommandStorageBackend();

  // Performs initialization on the background task run, if necessary.
  void InitIfNecessary();

  // Generates the path to a session file based on the provided parameters.
  static base::FilePath GetFilePath(CommandStorageManager::SessionType type,
                                    const base::FilePath& path,
                                    base::Time time,
                                    bool encrypted);

  // Closes the file. The next time AppendCommands() is called the file will
  // implicitly be reopened.
  void CloseFile();

  // If current_session_file_ is open, it is truncated so that it is essentially
  // empty (only contains the header). If current_session_file_ isn't open, it
  // is is opened and the header is written to it. After this
  // current_session_file_ contains no commands.
  // NOTE: current_session_file_ may be null if the file couldn't be opened or
  // the header couldn't be written.
  void TruncateOrOpenFile();

  // Opens the current file and writes the header. On success a handle to
  // the file is returned.
  std::unique_ptr<base::File> OpenAndWriteHeader(
      const base::FilePath& path) const;

  // Appends the specified commands to the specified file.
  WriteStatus AppendCommandsToFile(
      base::File* file,
      const std::vector<std::unique_ptr<sessions::SessionCommand>>& commands);

  // Writes `command` to `file`.
  WriteStatus AppendCommandToFile(base::File* file,
                                  const sessions::SessionCommand& command);

  // Gets data for the last session file.
  std::optional<SessionInfo> FindLastSessionFile() const;

  // Attempt to delete all sessions besides the current and last. This is a
  // best effort operation.
  void DeleteLastSessionFiles() const;

  // Gets all sessions files.
  std::vector<SessionInfo> GetSessionFilesSortedByReverseTimestamp() const {
    return GetSessionFilesSortedByReverseTimestamp(supplied_path_, type_,
                                                   is_encrypted());
  }
  static std::vector<SessionInfo> GetSessionFilesSortedByReverseTimestamp(
      const base::FilePath& path,
      CommandStorageManager::SessionType type,
      bool encrypted);

  static bool CompareSessionInfoTimestamps(const SessionInfo& a,
                                           const SessionInfo& b) {
    return b.timestamp < a.timestamp;
  }

  // Returns true if `path` can be used for the last session.
  bool CanUseFileForLastSession(const base::FilePath& path) const;

  // Used in testing to emulate an error in writing to the file. The value is
  // automatically reset after the failure.
  void ForceAppendCommandsToFailForTesting(WriteStatus status);

  static std::string GetHistogramNameForTesting(
      CommandStorageManager::SessionType session_type,
      bool encrypted,
      std::string_view operation,
      std::string_view slice = "",
      std::string_view metric = "");

  std::string GetHistogramName(std::string_view operation,
                               std::string_view slice,
                               std::string_view metric) const;

  bool is_encrypted() const { return encryptor_.get() != nullptr; }

  const CommandStorageManager::SessionType type_;

  // This is the path supplied to the constructor. See CommandStorageManager
  // constructor for details.
  const base::FilePath supplied_path_;

  // TaskRunner that the callback is added to.
  scoped_refptr<base::SequencedTaskRunner> callback_task_runner_;

  raw_ptr<base::Clock> clock_;

  // May be null, in which case the file is not encrypted.
  scoped_refptr<os_crypt_async::Encryptor> encryptor_;

  // File and path commands are being written.
  std::unique_ptr<OpenFile> open_file_;

  // Whether InitIfNecessary() was called. InitIfNecessary() is called on the
  // background task runner.
  bool inited_ = false;

  // Incremented every time a command is written.
  int commands_written_ = 0;

  // Timestamp when this session was started.
  base::Time timestamp_;

  // Data for the last session.
  std::optional<SessionInfo> last_session_info_;

  // Paths of the two most recently written files with a valid marker (the
  // first of which may be the currently open file). When a new file is
  // successfully opened and the initial set of commands is written,
  // `last_or_current_path_with_valid_marker_` is set to the path. At this
  // point the previous file (initial value of
  // `last_or_current_path_with_valid_marker_`) is no longer needed, and can be
  // deleted. As there is no guarantee the commands have actually been written
  // to disk, we keep one additional file around.
  // `second_to_last_path_with_valid_marker_` maintains the previous valid file
  // with a marker.
  std::optional<base::FilePath> last_or_current_path_with_valid_marker_;
  std::optional<base::FilePath> second_to_last_path_with_valid_marker_;

  WriteStatus force_write_status_for_testing_ = WriteStatus::kUnknown;
};

}  // namespace sessions

#endif  // COMPONENTS_SESSIONS_CORE_COMMAND_STORAGE_BACKEND_H_
