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

#include "storage/browser/blob/write_blob_to_file.h"

#include <stdint.h>

#include <algorithm>
#include <limits>
#include <memory>
#include <utility>
#include <vector>

#include "base/bind.h"
#include "base/files/file_util.h"
#include "base/numerics/checked_math.h"
#include "base/numerics/safe_conversions.h"
#include "base/task/post_task.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "storage/browser/blob/blob_data_handle.h"
#include "storage/browser/blob/blob_data_item.h"
#include "storage/browser/blob/blob_data_snapshot.h"
#include "storage/browser/blob/shareable_blob_data_item.h"
#include "storage/browser/file_system/file_stream_reader.h"
#include "storage/browser/file_system/file_stream_writer.h"
#include "storage/browser/file_system/file_writer_delegate.h"
#include "storage/common/file_system/file_system_mount_option.h"
#include "third_party/blink/public/common/blob/blob_utils.h"

namespace storage {
namespace {
using DelegateNoProgressWriteCallback = base::OnceCallback<void(
    base::File::Error result,
    int64_t bytes,
    FileWriterDelegate::WriteProgressStatus write_status)>;

struct WriteState {
  std::unique_ptr<FileWriterDelegate> delegate;
  DelegateNoProgressWriteCallback callback;
  base::CheckedNumeric<int64_t> total_bytes;
};

// Utility function to ignore all progress events returned when running
// FileWriterDelegate::Start. This means that there is either a single call for
// a success or failure. This wrapper also handles owning the lifetime of the
// FileWriterDelegate, which it destructs after receiving a success or error.
FileWriterDelegate::DelegateWriteCallback IgnoreProgressWrapper(
    std::unique_ptr<FileWriterDelegate> delegate,
    DelegateNoProgressWriteCallback callback) {
  return base::BindRepeating(
      [](WriteState* write_state, base::File::Error result, int64_t bytes,
         FileWriterDelegate::WriteProgressStatus write_status) {
        DCHECK_GE(bytes, 0);
        DCHECK(!write_state->callback.is_null());
        DCHECK(write_state->delegate);
        if (result == base::File::FILE_OK) {
          DCHECK(write_status == FileWriterDelegate::SUCCESS_COMPLETED ||
                 write_status == FileWriterDelegate::SUCCESS_IO_PENDING);
        } else {
          DCHECK(write_status == FileWriterDelegate::ERROR_WRITE_STARTED ||
                 write_status == FileWriterDelegate::ERROR_WRITE_NOT_STARTED);
        }

        write_state->total_bytes += bytes;
        if (write_status == FileWriterDelegate::SUCCESS_IO_PENDING)
          return;
        std::move(write_state->callback)
            .Run(result, write_state->total_bytes.ValueOrDie(), write_status);
        // This is necessary because FileWriterDelegate owns this repeating
        // callback, so there is a cyclic dependency.
        write_state->delegate.reset();
      },
      base::Owned(new WriteState{std::move(delegate), std::move(callback), 0}));
}

// Copied from file_util_posix.cc, with the addition of |offset| and |max_size|
// parameters. The parameters apply to the |infile|. The |bytes_copied|
// parameter keeps track of the number of bytes written to |outfile|.
// Note - this function can still succeed if the size |infile| is less than
// |max_size|. The caller should use |bytes_copied| to know exactly how many
// bytes were copied.
bool CopyFileContentsWithOffsetAndSize(base::File* infile,
                                       base::File* outfile,
                                       int64_t* bytes_copied,
                                       int64_t offset,
                                       int64_t max_size) {
  static constexpr size_t kBufferSize = 32768;
  DCHECK_GE(max_size, 0);
  *bytes_copied = 0;
  base::CheckedNumeric<int64_t> checked_max_size = max_size;

  std::vector<char> buffer(
      std::min(kBufferSize, base::checked_cast<size_t>(max_size)));
  infile->Seek(base::File::FROM_BEGIN, offset);

  for (;;) {
    size_t bytes_to_read =
        std::min(buffer.size(),
                 base::checked_cast<size_t>(checked_max_size.ValueOrDie()));
    int bytes_read = infile->ReadAtCurrentPos(buffer.data(), bytes_to_read);
    if (bytes_read < 0)
      return false;
    if (bytes_read == 0)
      return true;
    checked_max_size -= bytes_read;
    if (!checked_max_size.IsValid())
      return false;

    // Allow for partial writes
    int bytes_written_per_read = 0;
    do {
      int bytes_written_partial = outfile->WriteAtCurrentPos(
          &buffer[bytes_written_per_read], bytes_read - bytes_written_per_read);
      if (bytes_written_partial < 0)
        return false;

      bytes_written_per_read += bytes_written_partial;
      *bytes_copied += bytes_written_partial;
    } while (bytes_written_per_read < bytes_read);
    if (checked_max_size.ValueOrDie() == 0)
      return true;
  }

  NOTREACHED();
  return false;
}

// Copies the contents of |copy_from| to |copy_to|, with the given |offset| and
// optional |size| applied to the |copy_from| file. The
// |expected_last_modified_copy_from| must match, within a second, the last
// modified time of the |copy_from| file. Afterwards, the |last_modified| date
// is optionally saved as the last modified & last accessed time of |copy_to|.
// If |flush_on_close| is true, then Flush is called on the |copy_to| file
// before it is closed.
mojom::WriteBlobToFileResult CopyFileAndMaybeWriteTimeModified(
    const base::FilePath& copy_from,
    base::Time expected_last_modified_copy_from,
    const base::FilePath& copy_to,
    int64_t offset,
    base::Optional<int64_t> size,
    base::Optional<base::Time> last_modified,
    bool flush_on_close) {
  // Do a full file copy if the sizes match and there is no offset.
  if (offset == 0) {
    base::File::Info info;
    base::GetFileInfo(copy_from, &info);
    if (!FileStreamReader::VerifySnapshotTime(expected_last_modified_copy_from,
                                              info)) {
      return mojom::WriteBlobToFileResult::kInvalidBlob;
    }
    if (!size || info.size == size.value()) {
      bool success = base::CopyFile(copy_from, copy_to);
      if (!success)
        return mojom::WriteBlobToFileResult::kIOError;
      if (last_modified && !base::TouchFile(copy_to, last_modified.value(),
                                            last_modified.value())) {
        return mojom::WriteBlobToFileResult::kTimestampError;
      }
      return mojom::WriteBlobToFileResult::kSuccess;
    }
  }

  // Do a manual file-to-file copy. This will overwrite the file if there
  // already is one.
  base::File infile =
      base::File(copy_from, base::File::FLAG_OPEN | base::File::FLAG_READ);
  base::File outfile(copy_to,
                     base::File::FLAG_WRITE | base::File::FLAG_CREATE_ALWAYS);
  if (!outfile.IsValid())
    return mojom::WriteBlobToFileResult::kIOError;

  base::File::Info info;
  infile.GetInfo(&info);
  if (!FileStreamReader::VerifySnapshotTime(expected_last_modified_copy_from,
                                            info)) {
    return mojom::WriteBlobToFileResult::kInvalidBlob;
  }

  int64_t bytes_copied = 0;
  if (!CopyFileContentsWithOffsetAndSize(
          &infile, &outfile, &bytes_copied, offset,
          size.value_or(std::numeric_limits<int64_t>::max()))) {
    return mojom::WriteBlobToFileResult::kIOError;
  }
  if (size && bytes_copied != size.value())
    return mojom::WriteBlobToFileResult::kInvalidBlob;

  if (last_modified &&
      !outfile.SetTimes(last_modified.value(), last_modified.value())) {
    // If the file modification time isn't set correctly, then reading will
    // fail.
    return mojom::WriteBlobToFileResult::kTimestampError;
  }
  if (flush_on_close)
    outfile.Flush();
  outfile.Close();
  return mojom::WriteBlobToFileResult::kSuccess;
}

mojom::WriteBlobToFileResult CreateEmptyFileAndMaybeSetModifiedTime(
    base::FilePath file_path,
    base::Optional<base::Time> last_modified,
    bool flush_on_write) {
  base::File file(file_path,
                  base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
  bool file_success = file.created();
  if (!file_success) {
    return mojom::WriteBlobToFileResult::kIOError;
  }
  if (flush_on_write)
    file.Flush();
  file.Close();
  if (last_modified && !base::TouchFile(file_path, last_modified.value(),
                                        last_modified.value())) {
    // If the file modification time isn't set correctly, then reading
    // the blob later will fail. Thus, failing to save it is an error.
    file.Close();
    return mojom::WriteBlobToFileResult::kTimestampError;
  }
  return mojom::WriteBlobToFileResult::kSuccess;
}

void HandleModifiedTimeOnBlobFileWriteComplete(
    base::FilePath file_path,
    base::Optional<base::Time> last_modified,
    bool flush_on_write,
    mojom::BlobStorageContext::WriteBlobToFileCallback callback,
    base::File::Error rv,
    int64_t bytes_written,
    FileWriterDelegate::WriteProgressStatus write_status) {
  bool success = write_status == FileWriterDelegate::SUCCESS_COMPLETED;
  if (!success) {
    std::move(callback).Run(mojom::WriteBlobToFileResult::kIOError);
    return;
  }
  if (success && !bytes_written) {
    // Special Case 1: Success but no bytes were written, so just create
    // an empty file (LocalFileStreamWriter only creates a file
    // if data is actually written).
    base::ThreadPool::PostTaskAndReplyWithResult(
        FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
        base::BindOnce(CreateEmptyFileAndMaybeSetModifiedTime,
                       std::move(file_path), last_modified, flush_on_write),
        std::move(callback));
    return;
  } else if (success && last_modified) {
    // Special Case 2: Success and |last_modified| needs to be set. Set
    // that before reporting write completion.
    base::ThreadPool::PostTaskAndReplyWithResult(
        FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
        base::BindOnce(
            [](int64_t bytes_written, base::FilePath file_path,
               base::Optional<base::Time> last_modified) {
              if (!base::TouchFile(file_path, last_modified.value(),
                                   last_modified.value())) {
                // If the file modification time isn't set correctly, then
                // reading the blob later will fail. Thus, failing to save it is
                // an error.
                return mojom::WriteBlobToFileResult::kTimestampError;
              }
              return mojom::WriteBlobToFileResult::kSuccess;
            },
            bytes_written, std::move(file_path), last_modified),
        std::move(callback));
    return;
  }
  std::move(callback).Run(mojom::WriteBlobToFileResult::kSuccess);
}

void WriteConstructedBlobToFile(
    std::unique_ptr<BlobDataHandle> blob_handle,
    const base::FilePath& file_path,
    bool flush_on_write,
    base::Optional<base::Time> last_modified,
    mojom::BlobStorageContext::WriteBlobToFileCallback callback,
    BlobStatus status) {
  DCHECK(!last_modified || !last_modified.value().is_null());
  if (status != BlobStatus::DONE) {
    DCHECK(BlobStatusIsError(status));
    std::move(callback).Run(mojom::WriteBlobToFileResult::kInvalidBlob);
    return;
  }
  // Check if we can do a copy optimization.
  // TODO(dmurph): Optimize the case of IDB blobs, which have a type
  // kReadableDataHandle.
  std::unique_ptr<BlobDataSnapshot> snapshot = blob_handle->CreateSnapshot();
  const auto& items = snapshot->items();
  if (items.size() == 1) {
    const BlobDataItem& item = *items[0];
    if (item.type() == BlobDataItem::Type::kFile) {
      // The File API cannot handle uint64_t.
      base::Optional<int64_t> optional_size = item.length();
      if (item.length() == blink::BlobUtils::kUnknownSize) {
        // The blob system uses a special value (max uint64_t) to denote an
        // unknown file size. This means the whole file should be copied.
        optional_size = base::nullopt;
      } else if (item.length() > std::numeric_limits<int64_t>::max()) {
        std::move(callback).Run(mojom::WriteBlobToFileResult::kError);
        return;
      }
      if (item.offset() > std::numeric_limits<int64_t>::max()) {
        std::move(callback).Run(mojom::WriteBlobToFileResult::kError);
        return;
      }

      base::ThreadPool::PostTaskAndReplyWithResult(
          FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
          base::BindOnce(CopyFileAndMaybeWriteTimeModified, item.path(),
                         item.expected_modification_time(), file_path,
                         item.offset(), optional_size, last_modified,
                         flush_on_write),
          std::move(callback));
      return;
    }
  }

  // If not, copy the BlobReader and FileStreamWriter.
  std::unique_ptr<FileStreamWriter> writer =
      FileStreamWriter::CreateForLocalFile(
          base::ThreadPool::CreateTaskRunner(
              {base::MayBlock(), base::TaskPriority::USER_VISIBLE})
              .get(),
          file_path, /*initial_offset=*/0,
          FileStreamWriter::CREATE_NEW_FILE_ALWAYS);

  FlushPolicy policy = flush_on_write || last_modified
                           ? FlushPolicy::FLUSH_ON_COMPLETION
                           : FlushPolicy::NO_FLUSH_ON_COMPLETION;
  auto delegate =
      std::make_unique<FileWriterDelegate>(std::move(writer), policy);

  auto* raw_delegate = delegate.get();
  raw_delegate->Start(
      blob_handle->CreateReader(),
      IgnoreProgressWrapper(
          std::move(delegate),
          base::BindOnce(HandleModifiedTimeOnBlobFileWriteComplete, file_path,
                         std::move(last_modified), flush_on_write,
                         std::move(callback))));
}

}  // namespace

void WriteBlobToFile(
    std::unique_ptr<BlobDataHandle> blob_handle,
    const base::FilePath& file_path,
    bool flush_on_write,
    base::Optional<base::Time> last_modified,
    mojom::BlobStorageContext::WriteBlobToFileCallback callback) {
  auto* blob_handle_ptr = blob_handle.get();
  blob_handle_ptr->RunOnConstructionComplete(base::BindOnce(
      &WriteConstructedBlobToFile, std::move(blob_handle), file_path,
      flush_on_write, last_modified, std::move(callback)));
}

}  // namespace storage
