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

#include "chrome/installer/util/unbuffered_file_writer.h"

#include <windows.h>

#include <algorithm>
#include <bit>
#include <limits>
#include <utility>

#include "base/check.h"
#include "base/check_op.h"
#include "base/compiler_specific.h"
#include "base/files/drive_info.h"
#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/numerics/checked_math.h"
#include "base/process/memory.h"
#include "base/strings/strcat.h"
#include "base/types/expected_macros.h"

namespace installer {

namespace {

// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class Operation {
  kCreate = 0,
  // kGetInfo = 1,
  kAllocate = 2,
  kWrite = 3,
  kSetInfo = 4,
  kClearDelete = 5,
  kMaxValue = kClearDelete,
};

std::string_view ToString(Operation operation) {
  switch (operation) {
    case Operation::kCreate:
      return "Create";
    case Operation::kAllocate:
      return "Allocate";
    case Operation::kWrite:
      return "Write";
    case Operation::kSetInfo:
      return "SetInfo";
    case Operation::kClearDelete:
      return "ClearDelete";
  }
}

void RecordFailure(Operation operation, DWORD error_code) {
  base::UmaHistogramSparse(base::StrCat({"Setup.Install.UnbufferedFileWriter.",
                                         ToString(operation), ".Error"}),
                           static_cast<int>(error_code));
}

// Returns `value` rounded up to be an integral number of `chunk_size` chunks.
size_t RoundUp(size_t value, size_t chunk_size) {
  if (auto remainder = base::CheckMod(value, chunk_size);
      remainder.ValueOrDie() == 0) {
    return value;
  } else {
    return base::CheckAdd(value,
                          base::CheckSub(chunk_size, std::move(remainder)))
        .ValueOrDie();
  }
}

// Returns `value` rounded down to be an integral number of `chunk_size` chunks.
size_t RoundDown(size_t value, size_t chunk_size) {
  return base::CheckSub(value, base::CheckMod(value, chunk_size)).ValueOrDie();
}

// A deleter function that frees memory allocated via VirtualAlloc.
void VirtualFreeDeleteFn(void* ptr) {
  PCHECK(::VirtualFree(ptr, /*dwSize=*/0, MEM_RELEASE));
}

// Returns an empty AlignedBuffer.
base::HeapArray<uint8_t, void (*)(void*)> MakeEmptyBuffer() {
  // SAFETY: No allocation; deleter will never be called.
  return UNSAFE_BUFFERS(
      base::HeapArray<uint8_t, void (*)(void*)>::FromOwningPointer(nullptr, 0,
                                                                   nullptr));
}

// Returns the physical sector size for the device on which `path` resides,
// falling back to 4096 bytes (the norm as of this writing) if the value cannot
// be determined.
DWORD DeterminePhysicalSectorSize(const base::FilePath& path) {
  // TODO(crbug.com/456155453): Remove UMA and logging from this function once
  // this has been verified on stable.

  // These values are persisted to logs. Entries should not be renumbered and
  // numeric values should never be reused.
  enum class Result {
    kSuccess = 0,
    kDriveInfoFailed = 1,
    kNoSizeValue = 2,
    kGreaterThanFourK = 3,
    kNotPowerOfTwo = 4,
    kMaxValue = kNotPowerOfTwo
  };

  Result result = Result::kSuccess;  // Be optimistic.
  std::optional<DWORD> reported_sector_size;
  DWORD sector_size = 4096;  // A safe default in case all else fails.

  if (auto drive_info = base::GetFileDriveInfo(path); !drive_info.has_value()) {
    result = Result::kDriveInfoFailed;
  } else if (!drive_info->bytes_per_sector.has_value()) {
    result = Result::kNoSizeValue;
  } else {
    reported_sector_size = *drive_info->bytes_per_sector;
    if (!std::has_single_bit(*reported_sector_size)) {
      result = Result::kNotPowerOfTwo;
    } else if (*reported_sector_size > 4096) {
      result = Result::kGreaterThanFourK;
    } else {
      sector_size = *drive_info->bytes_per_sector;
    }
  }

  // Emit histograms and log only once per process.
  [[maybe_unused]] static const bool have_logged_once =
      [](Result result, std::optional<DWORD> reported_sector_size) {
        base::UmaHistogramEnumeration(
            "Setup.Install.UnbufferedFileWriter."
            "DeterminePhysicalSectorSizeResult",
            result);
        if (reported_sector_size.has_value()) {
          base::UmaHistogramSparse(
              "Setup.Install.UnbufferedFileWriter.PhysicalSectorSize",
              *reported_sector_size);
        }
        switch (result) {
          case Result::kSuccess:
            break;
          case Result::kDriveInfoFailed:
            PLOG(ERROR)
                << "Failed to get drive info; assuming 4KiB sector size.";
            break;
          case Result::kNoSizeValue:
            LOG(ERROR) << "Failed to determine physical sector size; assuming"
                          " 4KiB.";
            break;
          case Result::kGreaterThanFourK:
            LOG(ERROR) << "Device reported " << *reported_sector_size
                       << " bytes per sector, which is highly unusual; trying"
                          " with 4KiB.";
            break;
          case Result::kNotPowerOfTwo:
            LOG(ERROR) << "Device reported " << *reported_sector_size
                       << " bytes per sector, which is not a power of two; "
                          "trying with 4KiB.";
            break;
        }
        return true;
      }(result, reported_sector_size);

  return sector_size;
}

}  // namespace

// static
base::expected<UnbufferedFileWriter, DWORD> UnbufferedFileWriter::Create(
    const base::FilePath& path,
    int64_t buffer_size) {
  CHECK(!path.empty());
  CHECK_GE(buffer_size, 0LL);

  base::File file(::CreateFileW(path.value().c_str(),
                                /*dwDesiredAccess=*/GENERIC_WRITE | DELETE,
                                /*dwShareMode=*/FILE_SHARE_DELETE,
                                /*lpSecurityAttributes=*/nullptr,
                                /*dwCreationDisposition=*/CREATE_NEW,
                                /*dwFlagsAndAttributes=*/FILE_ATTRIBUTE_NORMAL |
                                    FILE_FLAG_NO_BUFFERING |
                                    FILE_FLAG_WRITE_THROUGH,
                                /*hTemplateFile=*/nullptr));

  if (!file.IsValid()) {
    auto error = ::GetLastError();
    PLOG(ERROR) << "CreateFileW failed";
    RecordFailure(Operation::kCreate, error);
    return base::unexpected(error);
  }

  // The file is deleted unless committed.
  file.DeleteOnClose(true);

  const DWORD physical_sector_size = DeterminePhysicalSectorSize(path);

  // Make the buffer size an even multiple of the physical sector size.
  buffer_size = buffer_size ? RoundUp(buffer_size, physical_sector_size)
                            : physical_sector_size;

  // Allocate a properly-aligned write buffer.
  AlignedBuffer buffer = AllocateAligned(buffer_size, physical_sector_size);
  if (buffer_size && buffer.empty()) {
    auto error = ::GetLastError();
    PLOG(ERROR) << "Allocation failed";
    RecordFailure(Operation::kAllocate, error);
    return base::unexpected(error);
  }

  return UnbufferedFileWriter(std::move(file), physical_sector_size,
                              std::move(buffer));
}

UnbufferedFileWriter::UnbufferedFileWriter(UnbufferedFileWriter&&) = default;
UnbufferedFileWriter& UnbufferedFileWriter::operator=(UnbufferedFileWriter&&) =
    default;

UnbufferedFileWriter::~UnbufferedFileWriter() = default;

void UnbufferedFileWriter::Advance(size_t offset) {
  CHECK_LE(offset, buffer_.size() - data_size_);
  data_size_ += offset;
}

base::expected<void, DWORD> UnbufferedFileWriter::Checkpoint() {
  CHECK(file_.IsValid());  // Use-after-move or after Commit.

  RETURN_IF_ERROR(Write(/*include_final_incomplete_sector=*/false));
  return base::ok();
}

base::expected<void, DWORD> UnbufferedFileWriter::Commit(
    std::optional<base::Time> last_modified_time) {
  CHECK(file_.IsValid());  // Use-after-move or after Commit.

  ASSIGN_OR_RETURN(int64_t file_size,
                   Write(/*include_final_incomplete_sector=*/true));

  if ((file_size % physical_sector_size_) != 0) {
    // Truncate down to the actual size.
    FILE_END_OF_FILE_INFO information = {};
    information.EndOfFile.QuadPart = file_size;
    if (!::SetFileInformationByHandle(
            file_.GetPlatformFile(),
            /*FileInformationClass=*/FileEndOfFileInfo,
            /*lpFileInformation=*/&information,
            /*dwBufferSize=*/sizeof(information))) {
      auto error = ::GetLastError();
      PLOG(ERROR) << "SetFileInformationByHandle failed";
      RecordFailure(Operation::kSetInfo, error);
      return base::unexpected(error);
    }
  }

  // The file has been written and shrunk down to the correct size. Clear the
  // delete-on-close bit so that it is retained when closed below.
  if (!file_.DeleteOnClose(false)) {
    auto error = ::GetLastError();
    PLOG(ERROR) << "DeleteOnClose failed";
    RecordFailure(Operation::kClearDelete, error);
    return base::unexpected(error);
  }

  if (last_modified_time) {
    // Make a best-effort attempt to set the file time before closing the file.
    FILETIME filetime = last_modified_time->ToFileTime();
    ::SetFileTime(file_.GetPlatformFile(), /*lpCreationTime=*/nullptr,
                  /*lpLastAccessTime=*/nullptr, /*lpLastWriteTime=*/&filetime);
  }

  // Close the file now that everything has succeeded.
  file_.Close();

  return base::ok();
}

UnbufferedFileWriter::UnbufferedFileWriter(base::File file,
                                           DWORD physical_sector_size,
                                           AlignedBuffer buffer)
    : file_(std::move(file)),
      physical_sector_size_(physical_sector_size),
      buffer_(std::move(buffer)) {}

base::expected<int64_t, DWORD> UnbufferedFileWriter::Write(
    bool include_final_incomplete_sector) {
  if (!buffer_.size()) {
    // A previous call to Write(true) has succeeded. One may not call
    // Write(false) again in this case.
    CHECK(include_final_incomplete_sector);
    return base::ok(file_position_);
  }

  // The number of bytes available to write.
  const size_t unwritten_size = data_size_ - written_size_;

  // The number of bytes available to write including padding to bring it up to
  // an integral number of physical sectors.
  const size_t unwritten_size_padded =
      RoundUp(unwritten_size, physical_sector_size_);

  // The number of bytes for this write; including or not including the padding
  // to fill a complete sector at the end.
  const size_t to_write_size =
      include_final_incomplete_sector
          ? unwritten_size_padded
          : RoundDown(unwritten_size, physical_sector_size_);

  // The aligned data to write in this call and the unwritten data at the end of
  // the buffer that will not be written if `include_final_incomplete_sector` is
  // false.
  auto [to_write, after_write] =
      buffer_.subspan(written_size_, unwritten_size_padded)
          .split_at(include_final_incomplete_sector
                        ? unwritten_size_padded
                        : unwritten_size_padded -
                              (unwritten_size - to_write_size));

  // The data in the incomplete final sector that will not be written.
  if (include_final_incomplete_sector) {
    // Zero-pad the end of the buffer to the physical sector boundary.
    std::ranges::fill(to_write.last(unwritten_size_padded - unwritten_size), 0);
  }

  // Write the data to disk in chunks no larger than 2^31-1 bytes (the max
  // supported by base::File::Write).
  const size_t max_write_size =
      RoundDown(static_cast<size_t>(std::numeric_limits<int>::max()),
                physical_sector_size_);
  while (!to_write.empty()) {
    size_t this_write_size = std::min(max_write_size, to_write.size());
    base::span<uint8_t> this_write;
    std::tie(this_write, to_write) = to_write.split_at(this_write_size);
    if (file_.Write(file_position_, this_write) != this_write_size) {
      auto error = ::GetLastError();
      PLOG(ERROR) << "Write failed";
      RecordFailure(Operation::kWrite, error);
      return base::unexpected(error);
    }
    written_size_ += this_write_size;
    file_position_ += this_write_size;
  }

  // No written data remains in the buffer.
  written_size_ = 0;

  if (include_final_incomplete_sector) {
    // All data has been written. Release the buffer.
    buffer_ = MakeEmptyBuffer();
    data_size_ = 0;

    // Subtract the padding from the file position so that it now represents
    // the desired size of the file.
    file_position_ -= (unwritten_size_padded - unwritten_size);
  } else {
    // `after_write` holds data beyond the last complete sector that was
    // written. Shift this to the front of the buffer for the next round of
    // writes.
    buffer_.copy_prefix_from(after_write);
    data_size_ = after_write.size();
  }

  return base::ok(file_position_);
}

// static
UnbufferedFileWriter::AlignedBuffer UnbufferedFileWriter::AllocateAligned(
    size_t size,
    DWORD alignment) {
  // Attempt using the normal allocator. In non-debug builds, PartitionAlloc
  // will make sensible choices to do a mapped-allocation for large sizes.
  uint8_t* mem = nullptr;
  if (base::UncheckedAlignedAlloc(size, alignment,
                                  reinterpret_cast<void**>(&mem))) {
    // SAFETY: UncheckedAlignedAlloc allocates at least `size` bytes.
    return UNSAFE_BUFFERS(AlignedBuffer::FromOwningPointer(
        mem, size, &base::UncheckedAlignedFree));
  }

  // If the normal allocator failed, try making a pagefile-backed allocation.
  // This is necessary for very large allocations, as PartitionAlloc rejects
  // them outright.
  static const DWORD kGranularity = [] {
    SYSTEM_INFO system_info = {};
    ::GetSystemInfo(&system_info);
    return system_info.dwAllocationGranularity;
  }();

  // Add support for devices that have physical sector sizes greater than the
  // allocation granularity if needed (doubtful -- famous last words!).
  CHECK_EQ(kGranularity % alignment, 0U);
  mem = static_cast<uint8_t*>(::VirtualAlloc(
      /*lpAddress=*/0, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
  if (mem) {
    // SAFETY: VirtualAlloc allocates at least `size` bytes.
    return UNSAFE_BUFFERS(
        AlignedBuffer::FromOwningPointer(mem, size, &VirtualFreeDeleteFn));
  }
  return MakeEmptyBuffer();
}

}  // namespace installer
