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

#include "third_party/zlib/google/zip_reader.h"

#include <algorithm>
#include <cstdint>
#include <string_view>
#include <utility>

#include "base/check.h"
#include "base/containers/span.h"
#include "base/containers/heap_array.h"
#include "base/strings/string_view_util.h"
#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/i18n/i18n_constants.h"
#include "base/i18n/icu_string_conversions.h"
#include "base/logging.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "build/build_config.h"
#include "third_party/zlib/google/redact.h"
#include "third_party/zlib/google/zip_internal.h"

#if defined(USE_SYSTEM_MINIZIP)
#include <minizip/unzip.h>
#else
#include "third_party/zlib/contrib/minizip/unzip.h"
#if defined(OS_WIN)
#include "third_party/zlib/contrib/minizip/iowin32.h"
#endif  // defined(OS_WIN)
#endif  // defined(USE_SYSTEM_MINIZIP)

#if defined(OS_POSIX)
#include <sys/stat.h>
#endif

namespace zip {
namespace {

enum UnzipError : int;

std::ostream& operator<<(std::ostream& out, UnzipError error) {
#define SWITCH_ERR(X) \
  case X:             \
    return out << #X;
  switch (error) {
    SWITCH_ERR(UNZ_OK);
    SWITCH_ERR(UNZ_END_OF_LIST_OF_FILE);
    SWITCH_ERR(UNZ_ERRNO);
    SWITCH_ERR(UNZ_PARAMERROR);
    SWITCH_ERR(UNZ_BADZIPFILE);
    SWITCH_ERR(UNZ_INTERNALERROR);
    SWITCH_ERR(UNZ_CRCERROR);
    default:
      return out << "UNZ" << static_cast<int>(error);
  }
#undef SWITCH_ERR
}

bool IsValidFileNameCharacterOnWindows(char16_t c) {
  if (c < 32)
    return false;

  switch (c) {
    case '<':   // Less than
    case '>':   // Greater than
    case ':':   // Colon
    case '"':   // Double quote
    case '|':   // Vertical bar or pipe
    case '?':   // Question mark
    case '*':   // Asterisk
    case '/':   // Forward slash
    case '\\':  // Backslash
      return false;
  }

  return true;
}

// A writer delegate that writes to a given string.
class StringWriterDelegate : public WriterDelegate {
 public:
  explicit StringWriterDelegate(std::string* output) : output_(output) {}

  // WriterDelegate methods:
  bool WriteBytes(base::span<const uint8_t> data) override {
    *output_ += base::as_string_view(data);
    return true;
  }

 private:
  std::string* const output_;
};

#if defined(OS_POSIX)
void SetPosixFilePermissions(int fd, int mode) {
  base::stat_wrapper_t sb;
  if (base::File::Fstat(fd, &sb)) {
    return;
  }
  mode_t new_mode = sb.st_mode;
  // Transfer the executable bit only if the file is readable.
  if ((sb.st_mode & S_IRUSR) == S_IRUSR && (mode & S_IXUSR) == S_IXUSR) {
    new_mode |= S_IXUSR;
  }
  if ((sb.st_mode & S_IRGRP) == S_IRGRP && (mode & S_IXGRP) == S_IXGRP) {
    new_mode |= S_IXGRP;
  }
  if ((sb.st_mode & S_IROTH) == S_IROTH && (mode & S_IXOTH) == S_IXOTH) {
    new_mode |= S_IXOTH;
  }
  if (new_mode != sb.st_mode) {
    fchmod(fd, new_mode);
  }
}
#endif

// Callback function for zlib that opens a file stream from a ReaderDelegate.
void* OpenReaderDelegate(void* opaque, const void* /*filename*/, int mode) {
  if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER) != ZLIB_FILEFUNC_MODE_READ) {
    return nullptr;
  }
  // The opaque parameter is the ReaderDelegate pointer.
  return opaque;
}

uLong ReadReaderDelegate(void* opaque, void* stream, void* buf, uLong size) {
  ReaderDelegate* delegate = static_cast<ReaderDelegate*>(opaque);
  // SAFETY: raw Read() method from third_party library.
  auto span = UNSAFE_BUFFERS(
      base::span(static_cast<uint8_t*>(buf), static_cast<size_t>(size)));
  int64_t bytes_read = delegate->ReadBytes(span);
  return (bytes_read < 0) ? 0 : static_cast<uLong>(bytes_read);
}

uLong WriteReaderDelegate(void* /*opaque*/,
                          void* /*stream*/,
                          const void* /*buf*/,
                          uLong /*size*/) {
  NOTREACHED();
}

ZPOS64_T TellReaderDelegate(void* opaque, void* stream) {
  ReaderDelegate* delegate = static_cast<ReaderDelegate*>(opaque);
  return delegate->Tell();
}

long SeekReaderDelegate(void* opaque,
                        void* stream,
                        ZPOS64_T offset,
                        int origin) {
  ReaderDelegate* delegate = static_cast<ReaderDelegate*>(opaque);
  int64_t new_offset = 0;

  if (origin == ZLIB_FILEFUNC_SEEK_SET) {
    new_offset = offset;
  } else if (origin == ZLIB_FILEFUNC_SEEK_CUR) {
    new_offset = delegate->Tell() + offset;
  } else if (origin == ZLIB_FILEFUNC_SEEK_END) {
    // For SEEK_END, offset is the distance from the end of the file.
    new_offset = delegate->GetLength() - offset;
  } else {
    return -1;
  }

  return delegate->Seek(new_offset) ? 0 : -1;
}

int CloseReaderDelegate(void* opaque, void* stream) {
  // We don't own the delegate, so we don't delete it.
  return 0;
}

int ErrorReaderDelegate(void* opaque, void* stream) {
  return 0;
}

}  // namespace

ZipReader::ZipReader() {
  Reset();
}

ZipReader::~ZipReader() {
  Close();
}

bool ZipReader::Open(const base::FilePath& zip_path) {
  DCHECK(!zip_file_);

  // Use of "Unsafe" function does not look good, but there is no way to do
  // this safely on Linux. See file_util.h for details.
  zip_file_ = internal::OpenForUnzipping(zip_path.AsUTF8Unsafe());
  if (!zip_file_) {
    LOG(ERROR) << "Cannot open ZIP archive " << Redact(zip_path);
    return false;
  }

  return OpenInternal();
}

bool ZipReader::OpenFromPlatformFile(base::PlatformFile zip_fd) {
  DCHECK(!zip_file_);

#if defined(OS_POSIX) || defined(OS_FUCHSIA)
  zip_file_ = internal::OpenFdForUnzipping(zip_fd);
#elif defined(OS_WIN)
  zip_file_ = internal::OpenHandleForUnzipping(zip_fd);
#endif
  if (!zip_file_) {
    LOG(ERROR) << "Cannot open ZIP from file handle " << zip_fd;
    return false;
  }

  return OpenInternal();
}

bool ZipReader::OpenFromString(const std::string& data) {
  zip_file_ = internal::PrepareMemoryForUnzipping(data);
  if (!zip_file_)
    return false;
  return OpenInternal();
}

bool ZipReader::OpenFromReaderDelegate(ReaderDelegate* delegate) {
  DCHECK(!zip_file_);
  DCHECK(delegate);

  zlib_filefunc64_def zip_funcs;
  zip_funcs.zopen64_file = OpenReaderDelegate;
  zip_funcs.zread_file = ReadReaderDelegate;
  zip_funcs.zwrite_file = WriteReaderDelegate;
  zip_funcs.ztell64_file = TellReaderDelegate;
  zip_funcs.zseek64_file = SeekReaderDelegate;
  zip_funcs.zclose_file = CloseReaderDelegate;
  zip_funcs.zerror_file = ErrorReaderDelegate;
  zip_funcs.opaque = delegate;

  zip_file_ = unzOpen2_64(nullptr, &zip_funcs);
  if (!zip_file_) {
    return false;
  }

  return OpenInternal();
}

void ZipReader::Close() {
  if (zip_file_) {
    if (const UnzipError err{unzClose(zip_file_)}; err != UNZ_OK) {
      LOG(ERROR) << "Error while closing ZIP archive: " << err;
    }
  }
  Reset();
}

const ZipReader::Entry* ZipReader::Next() {
  DCHECK(zip_file_);

  if (reached_end_)
    return nullptr;

  DCHECK(ok_);

  // Move to the next entry if we're not trying to open the first entry.
  if (next_index_ > 0) {
    if (const UnzipError err{unzGoToNextFile(zip_file_)}; err != UNZ_OK) {
      reached_end_ = true;
      if (err != UNZ_END_OF_LIST_OF_FILE) {
        LOG(ERROR) << "Cannot go to next entry in ZIP: " << err;
        ok_ = false;
      }
      return nullptr;
    }
  }

  next_index_++;

  if (!OpenEntry()) {
    reached_end_ = true;
    ok_ = false;
    return nullptr;
  }

  return &entry_;
}

bool ZipReader::OpenEntry() {
  DCHECK(zip_file_);

  // Get entry info.
  unz_file_info64 info = {};
  auto path_in_zip = base::HeapArray<char>::WithSize(internal::kZipMaxPath);
  if (const UnzipError err{unzGetCurrentFileInfo64(
          zip_file_, &info, path_in_zip.data(), path_in_zip.size() - 1,
          nullptr, 0, nullptr, 0)};
      err != UNZ_OK) {
    LOG(ERROR) << "Cannot get entry from ZIP: " << err;
    return false;
  }

  DCHECK(path_in_zip[info.size_filename] == '\0');
  entry_.path_in_original_encoding = path_in_zip.data();

    const char* const configured_encoding =
      encoding_.empty() ? base::kCodepageUTF8 : encoding_.c_str();
  const char* entry_path_encoding = configured_encoding;
  bool physical_path_is_directory = false;
  bool physical_path_is_unsafe = false;

  // If an Info-ZIP Unicode Path Extra Field is present, the physical Central
  // Directory path is about to be overridden. Decode and normalize it now into
  // `entry_.physical_path` so consumers (e.g. Safe Browsing) can still see the
  // name that other tools (e.g. Windows Explorer) would use for extraction.
  if (info.size_utf8_filename > 0) {
    std::u16string physical_path_in_utf16;
    if (!base::CodepageToUTF16(entry_.path_in_original_encoding,
                               configured_encoding,
                               base::OnStringConversionError::SUBSTITUTE,
                               &physical_path_in_utf16)) {
      LOG(ERROR) << "Cannot convert path from encoding " << configured_encoding;
      return false;
    }
    // Normalize() stores the normalized result in entry_.path; copy it before
    // applying the Unicode Path Extra Field below.
    Normalize(physical_path_in_utf16);
    entry_.physical_path = entry_.path;
    physical_path_is_directory = entry_.is_directory;
    physical_path_is_unsafe = entry_.is_unsafe;

    // Use the Info-ZIP Unicode Path Extra Field if present.
    DCHECK(info.utf8_filename[info.size_utf8_filename] == '\0');
    entry_.path_in_original_encoding = info.utf8_filename;
    entry_path_encoding = base::kCodepageUTF8;
  }

  // Convert path from original encoding to Unicode.
  std::u16string path_in_utf16;
  if (!base::CodepageToUTF16(entry_.path_in_original_encoding,
                             entry_path_encoding,
                             base::OnStringConversionError::SUBSTITUTE,
                             &path_in_utf16)) {
    LOG(ERROR) << "Cannot convert path from encoding " << entry_path_encoding;
    return false;
  }

  // Normalize path.
  Normalize(path_in_utf16);

  if (info.size_utf8_filename > 0) {
    // Treat an entry as a directory only if both names are directories;
    // otherwise callers that analyze file entries should inspect it as a file.
    entry_.is_directory = entry_.is_directory && physical_path_is_directory;
    // Treat an entry as unsafe if either name is unsafe.
    entry_.is_unsafe = entry_.is_unsafe || physical_path_is_unsafe;
  } else {
    // In the common case (no Unicode Path Extra Field) the physical path
    // matches the effective path.
    entry_.physical_path = entry_.path;
  }

  entry_.original_size = info.uncompressed_size;

  // The file content of this entry is encrypted if flag bit 0 is set.
  entry_.is_encrypted = info.flag & 1;
  if (entry_.is_encrypted) {
    // Is the entry AES encrypted.
    entry_.uses_aes_encryption = info.compression_method == 99;
  } else {
    entry_.uses_aes_encryption = false;
  }

  // Construct the last modified time. The timezone info is not present in ZIP
  // archives, so we construct the time as UTC.
  const base::Time::Exploded exploded_time = {
      .year = static_cast<int>(info.tmu_date.tm_year),
      .month =
          static_cast<int>(info.tmu_date.tm_mon + 1),  // 0-based vs 1-based
      .day_of_month = static_cast<int>(info.tmu_date.tm_mday),
      .hour = static_cast<int>(info.tmu_date.tm_hour),
      .minute = static_cast<int>(info.tmu_date.tm_min),
      .second = static_cast<int>(info.tmu_date.tm_sec)};

  if (!base::Time::FromUTCExploded(exploded_time, &entry_.last_modified))
    entry_.last_modified = base::Time::UnixEpoch();

#if defined(OS_POSIX)
  entry_.posix_mode = (info.external_fa >> 16L) & (S_IRWXU | S_IRWXG | S_IRWXO);
  entry_.is_symbolic_link = S_ISLNK(info.external_fa >> 16L);
#else
  entry_.posix_mode = 0;
  entry_.is_symbolic_link = false;
#endif

  return true;
}

void ZipReader::Normalize(std::u16string_view in) {
  entry_.is_unsafe = true;

  // Directory entries in ZIP have a path ending with "/".
  entry_.is_directory = base::EndsWith(in, u"/");

  std::u16string normalized_path;
  if (base::StartsWith(in, u"/")) {
    normalized_path = u"ROOT";
    entry_.is_unsafe = false;
  }

  for (;;) {
    // Consume initial path separators.
    const std::u16string_view::size_type i = in.find_first_not_of(u'/');
    if (i == std::u16string_view::npos) {
      break;
    }

    in.remove_prefix(i);
    DCHECK(!in.empty());

    // Isolate next path component.
    const std::u16string_view part = in.substr(0, in.find_first_of(u'/'));
    DCHECK(!part.empty());

    in.remove_prefix(part.size());

    if (!normalized_path.empty())
      normalized_path += u'/';

    if (part == u".") {
      normalized_path += u"DOT";
      entry_.is_unsafe = true;
      continue;
    }

    if (part == u"..") {
      normalized_path += u"UP";
      entry_.is_unsafe = true;
      continue;
    }

    // Windows has more restrictions than other systems when it comes to valid
    // file paths. Replace Windows-invalid characters on all systems for
    // consistency. In particular, this prevents a path component containing
    // colon and backslash from being misinterpreted as an absolute path on
    // Windows.
    for (const char16_t c : part) {
      normalized_path += IsValidFileNameCharacterOnWindows(c) ? c : 0xFFFD;
    }

    entry_.is_unsafe = false;
  }

  // If the entry is a directory, add the final path separator to the entry
  // path.
  if (entry_.is_directory && !normalized_path.empty()) {
    normalized_path += u'/';
    entry_.is_unsafe = false;
  }

  entry_.path = base::FilePath::FromUTF16Unsafe(normalized_path);

  // By construction, we should always get a relative path.
  DCHECK(!entry_.path.IsAbsolute()) << entry_.path;
}

void ZipReader::ReportProgress(ListenerCallback listener_callback,
                               uint64_t bytes) const {
  delta_bytes_read_ += bytes;

  const base::TimeTicks now = base::TimeTicks::Now();
  if (next_progress_report_time_ > now)
    return;

  next_progress_report_time_ = now + progress_period_;
  listener_callback.Run(delta_bytes_read_);
  delta_bytes_read_ = 0;
}

bool ZipReader::ExtractCurrentEntry(WriterDelegate* delegate,
                                    ListenerCallback listener_callback,
                                    uint64_t num_bytes_to_extract) const {
  DCHECK(zip_file_);
  DCHECK_LT(0, next_index_);
  DCHECK(ok_);
  DCHECK(!reached_end_);

  // Use password only for encrypted files. For non-encrypted files, no password
  // is needed, and must be nullptr.
  const char* const password =
      entry_.is_encrypted ? password_.c_str() : nullptr;
  if (const UnzipError err{unzOpenCurrentFilePassword(zip_file_, password)};
      err != UNZ_OK) {
    LOG(ERROR) << "Cannot open file " << Redact(entry_.path)
               << " from ZIP: " << err;
    return false;
  }

  DCHECK(delegate);
  if (!delegate->PrepareOutput())
    return false;

  uint64_t remaining_capacity = num_bytes_to_extract;
  bool entire_file_extracted = false;

  while (remaining_capacity > 0) {
    char buf[internal::kZipBufSize];
    const int num_bytes_read =
        unzReadCurrentFile(zip_file_, buf, internal::kZipBufSize);

    if (num_bytes_read == 0) {
      entire_file_extracted = true;
      break;
    }

    if (num_bytes_read < 0) {
      LOG(ERROR) << "Cannot read file " << Redact(entry_.path)
                 << " from ZIP: " << UnzipError(num_bytes_read);
      break;
    }

    if (listener_callback) {
      ReportProgress(listener_callback, num_bytes_read);
    }

    DCHECK_LT(0, num_bytes_read);
    CHECK_LE(num_bytes_read, internal::kZipBufSize);

    uint64_t num_bytes_to_write = std::min<uint64_t>(
        remaining_capacity, base::checked_cast<uint64_t>(num_bytes_read));
    if (!delegate->WriteBytes(base::as_byte_span(buf).first(
            base::checked_cast<size_t>(num_bytes_to_write)))) {
      break;
    }

    if (remaining_capacity == base::checked_cast<uint64_t>(num_bytes_read)) {
      // Ensures function returns true if the entire file has been read.
      const int n = unzReadCurrentFile(zip_file_, buf, 1);
      entire_file_extracted = (n == 0);
      LOG_IF(ERROR, n < 0) << "Cannot read file " << Redact(entry_.path)
                           << " from ZIP: " << UnzipError(n);
    }

    CHECK_GE(remaining_capacity, num_bytes_to_write);
    remaining_capacity -= num_bytes_to_write;
  }

  if (const UnzipError err{unzCloseCurrentFile(zip_file_)}; err != UNZ_OK) {
    LOG(ERROR) << "Cannot extract file " << Redact(entry_.path)
               << " from ZIP: " << err;
    entire_file_extracted = false;
  }

  if (entire_file_extracted) {
    delegate->SetPosixFilePermissions(entry_.posix_mode);
    if (entry_.last_modified != base::Time::UnixEpoch()) {
      delegate->SetTimeModified(entry_.last_modified);
    }
  } else {
    delegate->OnError();
  }

  if (listener_callback) {
    listener_callback.Run(delta_bytes_read_);
    delta_bytes_read_ = 0;
  }

  return entire_file_extracted;
}

bool ZipReader::ExtractCurrentEntry(WriterDelegate* delegate,
                                    uint64_t num_bytes_to_extract) const {
  return ExtractCurrentEntry(delegate, ListenerCallback(),
                             num_bytes_to_extract);
}

bool ZipReader::ExtractCurrentEntryWithListener(
    WriterDelegate* delegate,
    ListenerCallback listener_callback) const {
  return ExtractCurrentEntry(delegate, listener_callback);
}

void ZipReader::ExtractCurrentEntryToFilePathAsync(
    const base::FilePath& output_file_path,
    SuccessCallback success_callback,
    FailureCallback failure_callback,
    ProgressCallback progress_callback) {
  DCHECK(zip_file_);
  DCHECK_LT(0, next_index_);
  DCHECK(ok_);
  DCHECK(!reached_end_);

  // If this is a directory, just create it and return.
  if (entry_.is_directory) {
    if (base::CreateDirectory(output_file_path)) {
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, std::move(success_callback));
    } else {
      LOG(ERROR) << "Cannot create directory " << Redact(output_file_path);
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, std::move(failure_callback));
    }
    return;
  }

  // Use password only for encrypted files. For non-encrypted files, no password
  // is needed, and must be nullptr.
  const char* const password =
      entry_.is_encrypted ? password_.c_str() : nullptr;
  if (const UnzipError err{unzOpenCurrentFilePassword(zip_file_, password)};
      err != UNZ_OK) {
    LOG(ERROR) << "Cannot open file " << Redact(entry_.path)
               << " from ZIP: " << err;
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, std::move(failure_callback));
    return;
  }

  base::FilePath output_dir_path = output_file_path.DirName();
  if (!base::CreateDirectory(output_dir_path)) {
    LOG(ERROR) << "Cannot create directory " << Redact(output_dir_path);
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, std::move(failure_callback));
    return;
  }

  const int flags = base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE;
  base::File output_file(output_file_path, flags);

  if (!output_file.IsValid()) {
    LOG(ERROR) << "Cannot create file " << Redact(output_file_path);
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, std::move(failure_callback));
    return;
  }

  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(&ZipReader::ExtractChunk, weak_ptr_factory_.GetWeakPtr(),
                     std::move(output_file), std::move(success_callback),
                     std::move(failure_callback), std::move(progress_callback),
                     0 /* initial offset */));
}

bool ZipReader::ExtractCurrentEntryToString(uint64_t max_read_bytes,
                                            std::string* output) const {
  DCHECK(output);
  DCHECK(zip_file_);
  DCHECK_LT(0, next_index_);
  DCHECK(ok_);
  DCHECK(!reached_end_);

  output->clear();

  if (max_read_bytes == 0)
    return true;

  if (entry_.is_directory)
    return true;

  // The original_size is the best hint for the real size, so it saves doing
  // reallocations for the common case when the uncompressed size is correct.
  // However, we need to assume that the uncompressed size could be incorrect
  // therefore this function needs to read as much data as possible.
  output->reserve(base::checked_cast<size_t>(std::min<uint64_t>(
      max_read_bytes, base::checked_cast<uint64_t>(entry_.original_size))));

  StringWriterDelegate writer(output);
  return ExtractCurrentEntry(&writer, max_read_bytes);
}

bool ZipReader::OpenInternal() {
  DCHECK(zip_file_);

  unz_global_info zip_info = {};  // Zero-clear.
  if (const UnzipError err{unzGetGlobalInfo(zip_file_, &zip_info)};
      err != UNZ_OK) {
    LOG(ERROR) << "Cannot get ZIP info: " << err;
    return false;
  }

  num_entries_ = zip_info.number_entry;
  reached_end_ = (num_entries_ <= 0);
  ok_ = true;
  return true;
}

void ZipReader::Reset() {
  zip_file_ = nullptr;
  num_entries_ = 0;
  next_index_ = 0;
  reached_end_ = true;
  ok_ = false;
  delta_bytes_read_ = 0;
  entry_ = {};
}

void ZipReader::ExtractChunk(base::File output_file,
                             SuccessCallback success_callback,
                             FailureCallback failure_callback,
                             ProgressCallback progress_callback,
                             int64_t offset) {
  char buffer[internal::kZipBufSize];

  const int num_bytes_read =
      unzReadCurrentFile(zip_file_, buffer, internal::kZipBufSize);

  if (num_bytes_read == 0) {
    if (const UnzipError err{unzCloseCurrentFile(zip_file_)}; err != UNZ_OK) {
      LOG(ERROR) << "Cannot extract file " << Redact(entry_.path)
                 << " from ZIP: " << err;
      std::move(failure_callback).Run();
      return;
    }

    std::move(success_callback).Run();
    return;
  }

  if (num_bytes_read < 0) {
    LOG(ERROR) << "Cannot read file " << Redact(entry_.path)
               << " from ZIP: " << UnzipError(num_bytes_read);
    std::move(failure_callback).Run();
    return;
  }

  if (!output_file.WriteAndCheck(
          offset, base::as_byte_span(buffer).first(
                      static_cast<size_t>(num_bytes_read)))) {
    LOG(ERROR) << "Cannot write " << num_bytes_read
               << " bytes to file at offset " << offset;
    std::move(failure_callback).Run();
    return;
  }

  offset += num_bytes_read;
  progress_callback.Run(offset);

  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(&ZipReader::ExtractChunk, weak_ptr_factory_.GetWeakPtr(),
                     std::move(output_file), std::move(success_callback),
                     std::move(failure_callback), std::move(progress_callback),
                     offset));
}

// FileWriterDelegate ----------------------------------------------------------

FileWriterDelegate::FileWriterDelegate(base::File* file) : file_(file) {
  DCHECK(file_);
}

FileWriterDelegate::FileWriterDelegate(base::File owned_file)
    : owned_file_(std::move(owned_file)) {
  DCHECK_EQ(file_, &owned_file_);
}

FileWriterDelegate::~FileWriterDelegate() {}

bool FileWriterDelegate::PrepareOutput() {
  DCHECK(file_);

  if (!file_->IsValid()) {
    LOG(ERROR) << "File is not valid";
    return false;
  }

  const int64_t length = file_->GetLength();
  if (length < 0) {
    PLOG(ERROR) << "Cannot get length of file handle "
                << file_->GetPlatformFile();
    return false;
  }

  // Just log a warning if the file is not empty.
  // See crbug.com/1309879 and crbug.com/774762.
  LOG_IF(WARNING, length > 0)
      << "File handle " << file_->GetPlatformFile()
      << " is not empty: Its length is " << length << " bytes";

  return true;
}

bool FileWriterDelegate::WriteBytes(base::span<const uint8_t> data) {
  const std::optional<size_t> bytes_written = file_->WriteAtCurrentPos(data);
  if (bytes_written > 0) {
    file_length_ += *bytes_written;
  }
  return bytes_written == data.size();
}

void FileWriterDelegate::SetTimeModified(const base::Time& time) {
  file_->SetTimes(base::Time::Now(), time);
}

void FileWriterDelegate::SetPosixFilePermissions(int mode) {
#if defined(OS_POSIX)
  zip::SetPosixFilePermissions(file_->GetPlatformFile(), mode);
#endif
}

void FileWriterDelegate::OnError() {
  file_length_ = 0;
  file_->SetLength(0);
}

// FilePathWriterDelegate ------------------------------------------------------

FilePathWriterDelegate::FilePathWriterDelegate(base::FilePath output_file_path)
    : FileWriterDelegate(base::File()),
      output_file_path_(std::move(output_file_path)) {}

FilePathWriterDelegate::~FilePathWriterDelegate() {}

bool FilePathWriterDelegate::PrepareOutput() {
  // We can't rely on parent directory entries being specified in the
  // zip, so we make sure they are created.
  if (const base::FilePath dir = output_file_path_.DirName();
      !base::CreateDirectory(dir)) {
    PLOG(ERROR) << "Cannot create directory " << Redact(dir);
    return false;
  }

  owned_file_.Initialize(output_file_path_,
                         base::File::FLAG_CREATE | base::File::FLAG_WRITE);
  if (!owned_file_.IsValid()) {
    PLOG(ERROR) << "Cannot create file " << Redact(output_file_path_) << ": "
                << base::File::ErrorToString(owned_file_.error_details());
    return false;
  }

  const int64_t length = owned_file_.GetLength();
  if (length < 0) {
    PLOG(ERROR) << "Cannot get length of file " << Redact(output_file_path_);
    return false;
  }

  if (length > 0) {
    LOG(ERROR) << "File " << Redact(output_file_path_)
               << " is not empty: Its length is " << length << " bytes";
    return false;
  }

  return true;
}

void FilePathWriterDelegate::OnError() {
  FileWriterDelegate::OnError();
  owned_file_.Close();

  if (!base::DeleteFile(output_file_path_)) {
    LOG(ERROR) << "Cannot delete partially extracted file "
               << Redact(output_file_path_);
  }
}

}  // namespace zip
