// 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/settings.h"

#include <stdint.h>

#include <limits>

#include "base/logging.h"
#include "base/notreached.h"
#include "base/posix/eintr_wrapper.h"
#include "build/build_config.h"
#include "util/file/filesystem.h"
#include "util/numeric/in_range_cast.h"

namespace crashpad {

#if !CRASHPAD_FLOCK_ALWAYS_SUPPORTED

Settings::ScopedLockedFileHandle::ScopedLockedFileHandle()
    : handle_(kInvalidFileHandle), lockfile_path_() {
    }

Settings::ScopedLockedFileHandle::ScopedLockedFileHandle(
    FileHandle handle,
    const base::FilePath& lockfile_path)
    : handle_(handle), lockfile_path_(lockfile_path) {
}

Settings::ScopedLockedFileHandle::ScopedLockedFileHandle(
    ScopedLockedFileHandle&& other)
    : handle_(other.handle_), lockfile_path_(other.lockfile_path_) {
  other.handle_ = kInvalidFileHandle;
  other.lockfile_path_ = base::FilePath();
}

Settings::ScopedLockedFileHandle& Settings::ScopedLockedFileHandle::operator=(
    ScopedLockedFileHandle&& other) {
  handle_ = other.handle_;
  lockfile_path_ = other.lockfile_path_;

  other.handle_ = kInvalidFileHandle;
  other.lockfile_path_ = base::FilePath();
  return *this;
}

Settings::ScopedLockedFileHandle::~ScopedLockedFileHandle() {
  Destroy();
}

void Settings::ScopedLockedFileHandle::Destroy() {
  if (handle_ != kInvalidFileHandle) {
    CheckedCloseFile(handle_);
  }
  if (!lockfile_path_.empty()) {
    const bool success = LoggingRemoveFile(lockfile_path_);
    DCHECK(success);
  }
}

#else  // !CRASHPAD_FLOCK_ALWAYS_SUPPORTED

#if BUILDFLAG(IS_IOS)

Settings::ScopedLockedFileHandle::ScopedLockedFileHandle(
    const FileHandle& value)
    : ScopedGeneric(value) {
  ios_background_task_ = std::make_unique<internal::ScopedBackgroundTask>(
      "ScopedLockedFileHandle");
}

Settings::ScopedLockedFileHandle::ScopedLockedFileHandle(
    Settings::ScopedLockedFileHandle&& rvalue) {
  ios_background_task_.reset(rvalue.ios_background_task_.release());
  reset(rvalue.release());
}

Settings::ScopedLockedFileHandle& Settings::ScopedLockedFileHandle::operator=(
    Settings::ScopedLockedFileHandle&& rvalue) {
  ios_background_task_.reset(rvalue.ios_background_task_.release());
  reset(rvalue.release());
  return *this;
}

Settings::ScopedLockedFileHandle::~ScopedLockedFileHandle() {
  // Call reset() to ensure the lock is released before the ios_background_task.
  reset();
}

#endif  // BUILDFLAG(IS_IOS)

namespace internal {

// static
void ScopedLockedFileHandleTraits::Free(FileHandle handle) {
  if (handle != kInvalidFileHandle) {
    LoggingUnlockFile(handle);
    CheckedCloseFile(handle);
  }
}

}  // namespace internal

#endif  // BUILDFLAG(IS_FUCHSIA)

struct Settings::Data {
  static constexpr uint32_t kSettingsMagic = 'CPds';
  static constexpr uint32_t kSettingsVersion = 1;

  enum Options : uint32_t {
    kUploadsEnabled = 1 << 0,
  };

  Data() : magic(kSettingsMagic),
           version(kSettingsVersion),
           options(0),
           padding_0(0),
           last_upload_attempt_time(0),
           client_id() {}

  uint32_t magic;
  uint32_t version;
  uint32_t options;
  uint32_t padding_0;
  int64_t last_upload_attempt_time;  // time_t
  UUID client_id;
};

Settings::Settings() = default;

Settings::~Settings() = default;

bool Settings::Initialize(const base::FilePath& file_path) {
  DCHECK(initialized_.is_uninitialized());
  initialized_.set_invalid();
  file_path_ = file_path;

  Data settings;
  if (!OpenForWritingAndReadSettings(&settings).is_valid())
    return false;

  initialized_.set_valid();
  return true;
}

bool Settings::GetClientID(UUID* client_id) {
  DCHECK(initialized_.is_valid());

  Data settings;
  if (!OpenAndReadSettings(&settings))
    return false;

  *client_id = settings.client_id;
  return true;
}

bool Settings::GetUploadsEnabled(bool* enabled) {
  DCHECK(initialized_.is_valid());

  Data settings;
  if (!OpenAndReadSettings(&settings))
    return false;

  *enabled = (settings.options & Data::Options::kUploadsEnabled) != 0;
  return true;
}

bool Settings::SetUploadsEnabled(bool enabled) {
  DCHECK(initialized_.is_valid());

  Data settings;
  ScopedLockedFileHandle handle = OpenForWritingAndReadSettings(&settings);
  if (!handle.is_valid())
    return false;

  if (enabled)
    settings.options |= Data::Options::kUploadsEnabled;
  else
    settings.options &= ~Data::Options::kUploadsEnabled;

  return WriteSettings(handle.get(), settings);
}

bool Settings::GetLastUploadAttemptTime(time_t* time) {
  DCHECK(initialized_.is_valid());

  Data settings;
  if (!OpenAndReadSettings(&settings))
    return false;

  *time = InRangeCast<time_t>(settings.last_upload_attempt_time,
                              std::numeric_limits<time_t>::max());
  return true;
}

bool Settings::SetLastUploadAttemptTime(time_t time) {
  DCHECK(initialized_.is_valid());

  Data settings;
  ScopedLockedFileHandle handle = OpenForWritingAndReadSettings(&settings);
  if (!handle.is_valid())
    return false;

  settings.last_upload_attempt_time = InRangeCast<int64_t>(time, 0);

  return WriteSettings(handle.get(), settings);
}

#if !CRASHPAD_FLOCK_ALWAYS_SUPPORTED
// static
bool Settings::IsLockExpired(const base::FilePath& file_path,
                             time_t lockfile_ttl) {
  time_t now = time(nullptr);
  base::FilePath lock_path(file_path.value() + Settings::kLockfileExtension);
  ScopedFileHandle lock_fd(LoggingOpenFileForRead(lock_path));
  time_t lock_timestamp;
  if (!LoggingReadFileExactly(
          lock_fd.get(), &lock_timestamp, sizeof(lock_timestamp))) {
    return false;
  }
  return now >= lock_timestamp + lockfile_ttl;
}
#endif  // !CRASHPAD_FLOCK_ALWAYS_SUPPORTED

// static
Settings::ScopedLockedFileHandle Settings::MakeScopedLockedFileHandle(
    const internal::MakeScopedLockedFileHandleOptions& options,
    FileLocking locking,
    const base::FilePath& file_path) {
#if !CRASHPAD_FLOCK_ALWAYS_SUPPORTED
  base::FilePath lockfile_path(file_path.value() +
                               Settings::kLockfileExtension);
  ScopedFileHandle lockfile_scoped(
      LoggingOpenFileForWrite(lockfile_path,
                              FileWriteMode::kCreateOrFail,
                              FilePermissions::kWorldReadable));
  if (!lockfile_scoped.is_valid()) {
    return ScopedLockedFileHandle();
  }
  time_t now = time(nullptr);
  if (!LoggingWriteFile(lockfile_scoped.get(), &now, sizeof(now))) {
    return ScopedLockedFileHandle();
  }
  ScopedFileHandle scoped(GetHandleFromOptions(file_path, options));
  if (scoped.is_valid()) {
    return ScopedLockedFileHandle(scoped.release(), lockfile_path);
  }
  bool success = LoggingRemoveFile(lockfile_path);
  DCHECK(success);
  return ScopedLockedFileHandle();
#else
  ScopedFileHandle scoped(GetHandleFromOptions(file_path, options));
  // It's important to create the ScopedLockedFileHandle before calling
  // LoggingLockFile on iOS, so a ScopedBackgroundTask is created *before*
  // the LoggingLockFile call below.
  ScopedLockedFileHandle handle(kInvalidFileHandle);
  if (scoped.is_valid()) {
    if (LoggingLockFile(
            scoped.get(), locking, FileLockingBlocking::kBlocking) !=
        FileLockingResult::kSuccess) {
      scoped.reset();
    }
  }
  handle.reset(scoped.release());
  return handle;
#endif  // !CRASHPAD_FLOCK_ALWAYS_SUPPORTED
}

// static
FileHandle Settings::GetHandleFromOptions(
    const base::FilePath& file_path,
    const internal::MakeScopedLockedFileHandleOptions& options) {
  switch (options.function_enum) {
    case internal::FileOpenFunction::kLoggingOpenFileForRead:
      return LoggingOpenFileForRead(file_path);
    case internal::FileOpenFunction::kLoggingOpenFileForReadAndWrite:
      return LoggingOpenFileForReadAndWrite(
          file_path, options.mode, options.permissions);
    case internal::FileOpenFunction::kOpenFileForReadAndWrite:
      return OpenFileForReadAndWrite(
          file_path, options.mode, options.permissions);
  }
  NOTREACHED();
  return kInvalidFileHandle;
}

Settings::ScopedLockedFileHandle Settings::OpenForReading() {
  internal::MakeScopedLockedFileHandleOptions options;
  options.function_enum = internal::FileOpenFunction::kLoggingOpenFileForRead;
  return MakeScopedLockedFileHandle(options, FileLocking::kShared, file_path());
}

Settings::ScopedLockedFileHandle Settings::OpenForReadingAndWriting(
    FileWriteMode mode, bool log_open_error) {
  DCHECK(mode != FileWriteMode::kTruncateOrCreate);

  internal::MakeScopedLockedFileHandleOptions options;
  options.mode = mode;
  options.permissions = FilePermissions::kOwnerOnly;
  if (log_open_error) {
    options.function_enum =
        internal::FileOpenFunction::kLoggingOpenFileForReadAndWrite;
  } else {
    options.function_enum =
        internal::FileOpenFunction::kOpenFileForReadAndWrite;
  }

  return MakeScopedLockedFileHandle(
      options, FileLocking::kExclusive, file_path());
}

bool Settings::OpenAndReadSettings(Data* out_data) {
  ScopedLockedFileHandle handle = OpenForReading();
  if (!handle.is_valid())
    return false;

  if (ReadSettings(handle.get(), out_data, true))
    return true;

  // The settings file is corrupt, so reinitialize it.
  handle.reset();

  // The settings failed to be read, so re-initialize them.
  return RecoverSettings(kInvalidFileHandle, out_data);
}

Settings::ScopedLockedFileHandle Settings::OpenForWritingAndReadSettings(
    Data* out_data) {
  ScopedLockedFileHandle handle;
  bool created = false;
  if (!initialized_.is_valid()) {
    // If this object is initializing, it hasn’t seen a settings file already,
    // so go easy on errors. Creating a new settings file for the first time
    // shouldn’t spew log messages.
    //
    // First, try to use an existing settings file.
    handle = OpenForReadingAndWriting(FileWriteMode::kReuseOrFail, false);

    if (!handle.is_valid()) {
      // Create a new settings file if it didn’t already exist.
      handle = OpenForReadingAndWriting(FileWriteMode::kCreateOrFail, false);

      if (handle.is_valid()) {
        created = true;
      }

      // There may have been a race to create the file, and something else may
      // have won. There will be one more attempt to try to open or create the
      // file below.
    }
  }

  if (!handle.is_valid()) {
    // Either the object is initialized, meaning it’s already seen a valid
    // settings file, or the object is initializing and none of the above
    // attempts to create the settings file succeeded. Either way, this is the
    // last chance for success, so if this fails, log a message.
    handle = OpenForReadingAndWriting(FileWriteMode::kReuseOrCreate, true);
  }

  if (!handle.is_valid())
    return ScopedLockedFileHandle();

  // Attempt reading the settings even if the file is known to have just been
  // created. The file-create and file-lock operations don’t occur atomically,
  // and something else may have written the settings before this invocation
  // took the lock. If the settings file was definitely just created, though,
  // don’t log any read errors. The expected non-race behavior in this case is a
  // zero-length read, with ReadSettings() failing.
  if (!ReadSettings(handle.get(), out_data, !created)) {
    if (!RecoverSettings(handle.get(), out_data))
      return ScopedLockedFileHandle();
  }

  return handle;
}

bool Settings::ReadSettings(FileHandle handle,
                            Data* out_data,
                            bool log_read_error) {
  if (LoggingSeekFile(handle, 0, SEEK_SET) != 0)
    return false;

  bool read_result =
      log_read_error
          ? LoggingReadFileExactly(handle, out_data, sizeof(*out_data))
          : ReadFileExactly(handle, out_data, sizeof(*out_data));

  if (!read_result)
    return false;

  if (out_data->magic != Data::kSettingsMagic) {
    LOG(ERROR) << "Settings magic is not " << Data::kSettingsMagic;
    return false;
  }

  if (out_data->version != Data::kSettingsVersion) {
    LOG(ERROR) << "Settings version is not " << Data::kSettingsVersion;
    return false;
  }

  return true;
}

bool Settings::WriteSettings(FileHandle handle, const Data& data) {
  if (LoggingSeekFile(handle, 0, SEEK_SET) != 0)
    return false;

  if (!LoggingTruncateFile(handle))
    return false;

  return LoggingWriteFile(handle, &data, sizeof(Data));
}

bool Settings::RecoverSettings(FileHandle handle, Data* out_data) {
  ScopedLockedFileHandle scoped_handle;
  if (handle == kInvalidFileHandle) {
    scoped_handle =
        OpenForReadingAndWriting(FileWriteMode::kReuseOrCreate, true);
    handle = scoped_handle.get();

    // Test if the file has already been recovered now that the exclusive lock
    // is held.
    if (ReadSettings(handle, out_data, true))
      return true;
  }

  if (handle == kInvalidFileHandle) {
    LOG(ERROR) << "Invalid file handle";
    return false;
  }

  if (!InitializeSettings(handle))
    return false;

  return ReadSettings(handle, out_data, true);
}

bool Settings::InitializeSettings(FileHandle handle) {
  Data settings;
  if (!settings.client_id.InitializeWithNew())
    return false;

  return WriteSettings(handle, settings);
}

}  // namespace crashpad
