/*
 * Copyright (C) 2010 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "third_party/blink/renderer/platform/blob/blob_data.h"

#include <memory>
#include <utility>

#include "base/check_is_test.h"
#include "base/containers/span.h"
#include "base/memory/ptr_util.h"
#include "base/memory/scoped_refptr.h"
#include "base/task/single_thread_task_runner.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "third_party/blink/public/common/thread_safe_browser_interface_broker_proxy.h"
#include "third_party/blink/public/mojom/blob/blob.mojom-blink.h"
#include "third_party/blink/public/mojom/blob/blob_registry.mojom-blink.h"
#include "third_party/blink/public/mojom/blob/data_element.mojom-blink.h"
#include "third_party/blink/public/mojom/blob/file_backed_blob_factory.mojom-blink.h"
#include "third_party/blink/public/platform/file_path_conversion.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/platform/blob/blob_bytes_provider.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
#include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"
#include "third_party/blink/renderer/platform/wtf/text/line_ending.h"
#include "third_party/blink/renderer/platform/wtf/text/text_encoding.h"
#include "third_party/blink/renderer/platform/wtf/thread_specific.h"
#include "third_party/blink/renderer/platform/wtf/uuid.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"

namespace blink {

using mojom::blink::BytesProvider;
using mojom::blink::DataElement;
using mojom::blink::DataElementBlob;
using mojom::blink::DataElementBytes;
using mojom::blink::DataElementBytesPtr;
using mojom::blink::DataElementFile;
using mojom::blink::DataElementPtr;

namespace {

// http://dev.w3.org/2006/webapi/FileAPI/#constructorBlob
bool IsValidBlobType(const String& type) {
  for (unsigned i = 0; i < type.length(); ++i) {
    UChar c = type[i];
    if (c < 0x20 || c > 0x7E)
      return false;
  }
  return true;
}

mojom::blink::BlobRegistry* g_blob_registry_for_testing = nullptr;

mojom::blink::BlobRegistry* GetThreadSpecificRegistry() {
  if (g_blob_registry_for_testing) [[unlikely]] {
    return g_blob_registry_for_testing;
  }

  DEFINE_THREAD_SAFE_STATIC_LOCAL(
      ThreadSpecific<mojo::Remote<mojom::blink::BlobRegistry>>, registry, ());
  if (!registry.IsSet()) [[unlikely]] {
    // TODO(mek): Going through BrowserInterfaceBroker to get a
    // mojom::blink::BlobRegistry ends up going through the main thread. Ideally
    // workers wouldn't need to do that.
    Platform::Current()->GetBrowserInterfaceBroker()->GetInterface(
        (*registry).BindNewPipeAndPassReceiver());
  }
  return registry->get();
}

}  // namespace

constexpr int64_t BlobData::kToEndOfFile;

RawData::RawData() = default;

BlobData::BlobData(FileCompositionStatus composition)
    : file_composition_(composition) {}

BlobData::~BlobData() = default;

Vector<mojom::blink::DataElementPtr> BlobData::ReleaseElements() {
  if (last_bytes_provider_) {
    DCHECK(last_bytes_provider_receiver_);
    BlobBytesProvider::Bind(std::move(last_bytes_provider_),
                            std::move(last_bytes_provider_receiver_));
  }

  return std::move(elements_);
}

void BlobData::SetContentType(const String& content_type) {
  if (IsValidBlobType(content_type))
    content_type_ = content_type;
  else
    content_type_ = "";
}

void BlobData::AppendData(scoped_refptr<RawData> data) {
  AppendDataInternal(base::span(*data), data);
}

void BlobData::AppendBlob(scoped_refptr<BlobDataHandle> data_handle,
                          int64_t offset,
                          int64_t length) {
  DCHECK_EQ(file_composition_, FileCompositionStatus::kNoUnknownSizeFiles)
      << "Blobs with a unknown-size file cannot have other items.";
  DCHECK(!data_handle->IsSingleUnknownSizeFile() ||
         length != BlobData::kToEndOfFile)
      << "It is illegal to append an unknown size file blob without specifying "
         "a size.";
  // Skip zero-byte items, as they don't matter for the contents of the blob.
  if (length == 0)
    return;
  elements_.push_back(DataElement::NewBlob(
      DataElementBlob::New(data_handle->CloneBlobRemote(), offset, length)));
}

void BlobData::AppendText(const String& text,
                          bool do_normalize_line_endings_to_native) {
  DCHECK_EQ(file_composition_, FileCompositionStatus::kNoUnknownSizeFiles)
      << "Blobs with a unknown-size file cannot have other items.";
  std::string utf8_text =
      Utf8Encoding().Encode(text, UnencodableHandling::kNone);

  if (do_normalize_line_endings_to_native) {
    if (utf8_text.length() >
        BlobBytesProvider::kMaxConsolidatedItemSizeInBytes) {
      auto raw_data = RawData::Create();
      NormalizeLineEndingsToNative(utf8_text, raw_data->MutableData());
      AppendDataInternal(base::span(*raw_data), raw_data);
    } else {
      Vector<uint8_t> buffer;
      NormalizeLineEndingsToNative(utf8_text, buffer);
      AppendDataInternal(base::span(buffer));
    }
  } else {
    AppendDataInternal(base::as_byte_span(utf8_text));
  }
}

void BlobData::AppendBytes(base::span<const uint8_t> bytes) {
  AppendDataInternal(bytes);
}

uint64_t BlobData::length() const {
  uint64_t length = 0;

  for (const auto& element : elements_) {
    switch (element->which()) {
      case DataElement::Tag::kBytes:
        length += element->get_bytes()->length;
        break;
      case DataElement::Tag::kFile:
        length += element->get_file()->length;
        break;
      case DataElement::Tag::kBlob:
        length += element->get_blob()->length;
        break;
    }
  }
  return length;
}

void BlobData::AppendDataInternal(base::span<const uint8_t> data,
                                  scoped_refptr<RawData> raw_data) {
  DCHECK_EQ(file_composition_, FileCompositionStatus::kNoUnknownSizeFiles)
      << "Blobs with a unknown-size file cannot have other items.";
  // Skip zero-byte items, as they don't matter for the contents of the blob.
  if (data.empty())
    return;
  bool should_embed_bytes = current_memory_population_ + data.size() <=
                            DataElementBytes::kMaximumEmbeddedDataSize;
  if (!elements_.empty() && elements_.back()->is_bytes()) {
    // Append bytes to previous element.
    DCHECK(last_bytes_provider_);
    DCHECK(last_bytes_provider_receiver_);
    const auto& bytes_element = elements_.back()->get_bytes();
    bytes_element->length += data.size();
    if (should_embed_bytes && bytes_element->embedded_data) {
      bytes_element->embedded_data->append_range(data);
      current_memory_population_ += data.size();
    } else if (bytes_element->embedded_data) {
      current_memory_population_ -= bytes_element->embedded_data->size();
      bytes_element->embedded_data = std::nullopt;
    }
  } else {
    if (last_bytes_provider_) {
      // If `last_bytes_provider_` is set, but the previous element is not a
      // bytes element, a new BytesProvider will be created and we need to
      // make sure to bind the previous one first.
      DCHECK(last_bytes_provider_receiver_);
      BlobBytesProvider::Bind(std::move(last_bytes_provider_),
                              std::move(last_bytes_provider_receiver_));
    }
    mojo::PendingRemote<BytesProvider> bytes_provider_remote;
    last_bytes_provider_ = std::make_unique<BlobBytesProvider>();
    last_bytes_provider_receiver_ =
        bytes_provider_remote.InitWithNewPipeAndPassReceiver();

    auto bytes_element = DataElementBytes::New(
        data.size(), std::nullopt, std::move(bytes_provider_remote));
    if (should_embed_bytes) {
      bytes_element->embedded_data = Vector<uint8_t>();
      bytes_element->embedded_data->append_range(data);
      current_memory_population_ += data.size();
    }
    elements_.push_back(DataElement::NewBytes(std::move(bytes_element)));
  }
  if (raw_data)
    last_bytes_provider_->AppendData(std::move(raw_data));
  else
    last_bytes_provider_->AppendData(std::move(data));
}

// static
scoped_refptr<BlobDataHandle> BlobDataHandle::CreateForFile(
    mojom::blink::FileBackedBlobFactory* file_backed_blob_factory,
    const String& path,
    int64_t offset,
    int64_t length,
    const std::optional<base::Time>& expected_modification_time,
    const String& content_type) {
  mojom::blink::DataElementFilePtr element = mojom::blink::DataElementFile::New(
      StringToFilePath(path), offset, length, expected_modification_time);
  uint64_t size = length == BlobData::kToEndOfFile
                      ? std::numeric_limits<uint64_t>::max()
                      : length;
  return base::AdoptRef(new BlobDataHandle(
      file_backed_blob_factory, std::move(element), content_type, size));
}

// static
scoped_refptr<BlobDataHandle> BlobDataHandle::CreateForFileSync(
    mojom::blink::FileBackedBlobFactory* file_backed_blob_factory,
    const String& path,
    int64_t offset,
    int64_t length,
    const std::optional<base::Time>& expected_modification_time,
    const String& content_type) {
  mojom::blink::DataElementFilePtr element = mojom::blink::DataElementFile::New(
      StringToFilePath(path), offset, length, expected_modification_time);
  uint64_t size = length == BlobData::kToEndOfFile
                      ? std::numeric_limits<uint64_t>::max()
                      : length;
  return base::AdoptRef(new BlobDataHandle(
      file_backed_blob_factory, std::move(element), content_type, size, true));
}

// static
scoped_refptr<BlobDataHandle> BlobDataHandle::Create(
    const String& uuid,
    const String& type,
    uint64_t size,
    mojo::PendingRemote<mojom::blink::Blob> blob_remote) {
  CHECK(blob_remote.is_valid());
  return base::AdoptRef(
      new BlobDataHandle(uuid, type, size, std::move(blob_remote)));
}

BlobDataHandle::BlobDataHandle()
    : uuid_(CreateCanonicalUuidString()),
      size_(0),
      is_single_unknown_size_file_(false) {
  GetThreadSpecificRegistry()->Register(
      blob_remote_.InitWithNewPipeAndPassReceiver(), uuid_, "", "", {});
}

BlobDataHandle::BlobDataHandle(std::unique_ptr<BlobData> data, uint64_t size)
    : uuid_(CreateCanonicalUuidString()),
      type_(data->ContentType()),
      size_(size),
      is_single_unknown_size_file_(data->IsSingleUnknownSizeFile()) {
  auto elements = data->ReleaseElements();
  TRACE_EVENT0("Blob", "Registry::RegisterBlob");
  GetThreadSpecificRegistry()->Register(
      blob_remote_.InitWithNewPipeAndPassReceiver(), uuid_,
      type_.IsNull() ? "" : type_, "", std::move(elements));
}

BlobDataHandle::BlobDataHandle(
    mojom::blink::FileBackedBlobFactory* file_backed_blob_factory,
    mojom::blink::DataElementFilePtr file_element,
    const String& content_type,
    uint64_t size,
    bool synchronous_register)
    : uuid_(CreateCanonicalUuidString()),
      type_(content_type),
      size_(size),
      is_single_unknown_size_file_(size ==
                                   std::numeric_limits<uint64_t>::max()) {
  if (file_backed_blob_factory) {
    if (synchronous_register) {
      file_backed_blob_factory->RegisterBlobSync(
          blob_remote_.InitWithNewPipeAndPassReceiver(), uuid_,
          type_.IsNull() ? "" : type_, std::move(file_element));
    } else {
      file_backed_blob_factory->RegisterBlob(
          blob_remote_.InitWithNewPipeAndPassReceiver(), uuid_,
          type_.IsNull() ? "" : type_, std::move(file_element));
    }
  } else {
    // TODO(b/287417238): Temporarily fallback to the previous BlobRegistry
    // registration when new interface is disabled by its feature flag or the
    // interface is not bound to a frame.
    Vector<mojom::blink::DataElementPtr> elements;
    elements.push_back(DataElement::NewFile(std::move(file_element)));
    TRACE_EVENT0("Blob", "Registry::RegisterBlob");
    GetThreadSpecificRegistry()->Register(
        blob_remote_.InitWithNewPipeAndPassReceiver(), uuid_,
        type_.IsNull() ? "" : type_, "", std::move(elements));
  }
}

BlobDataHandle::BlobDataHandle(const String& uuid,
                               const String& type,
                               uint64_t size)
    : uuid_(uuid),
      type_(IsValidBlobType(type) ? type : ""),
      size_(size),
      is_single_unknown_size_file_(false) {
  // This is only used by unit tests that won't access `blob_remote_`.
  CHECK_IS_TEST();
}

BlobDataHandle::BlobDataHandle(
    const String& uuid,
    const String& type,
    uint64_t size,
    mojo::PendingRemote<mojom::blink::Blob> blob_remote)
    : uuid_(uuid),
      type_(IsValidBlobType(type) ? type : ""),
      size_(size),
      is_single_unknown_size_file_(false),
      blob_remote_(std::move(blob_remote)) {
  DCHECK(blob_remote_.is_valid());
}

BlobDataHandle::~BlobDataHandle() = default;

mojo::PendingRemote<mojom::blink::Blob> BlobDataHandle::CloneBlobRemote() {
  base::AutoLock locker(blob_remote_lock_);
  if (!blob_remote_.is_valid())
    return mojo::NullRemote();
  mojo::Remote<mojom::blink::Blob> blob(std::move(blob_remote_));
  mojo::PendingRemote<mojom::blink::Blob> blob_clone;
  blob->Clone(blob_clone.InitWithNewPipeAndPassReceiver());
  blob_remote_ = blob.Unbind();
  return blob_clone;
}

void BlobDataHandle::CloneBlobRemote(
    mojo::PendingReceiver<mojom::blink::Blob> receiver) {
  base::AutoLock locker(blob_remote_lock_);
  if (!blob_remote_.is_valid())
    return;
  mojo::Remote<mojom::blink::Blob> blob(std::move(blob_remote_));
  blob->Clone(std::move(receiver));
  blob_remote_ = blob.Unbind();
}

void BlobDataHandle::ReadAll(
    mojo::ScopedDataPipeProducerHandle pipe,
    mojo::PendingRemote<mojom::blink::BlobReaderClient> client) {
  base::AutoLock locker(blob_remote_lock_);
  mojo::Remote<mojom::blink::Blob> blob(std::move(blob_remote_));
  blob->ReadAll(std::move(pipe), std::move(client));
  blob_remote_ = blob.Unbind();
}

bool BlobDataHandle::CaptureSnapshot(
    uint64_t* snapshot_size,
    std::optional<base::Time>* snapshot_modification_time) {
  // This method operates on a cloned blob remote; this lets us avoid holding
  // the |blob_remote_lock_| locked during the duration of the (synchronous)
  // CaptureSnapshot call.
  mojo::Remote<mojom::blink::Blob> remote(CloneBlobRemote());
  return remote->CaptureSnapshot(snapshot_size, snapshot_modification_time);
}

// static
mojom::blink::BlobRegistry* BlobDataHandle::GetBlobRegistry() {
  return GetThreadSpecificRegistry();
}

// static
void BlobDataHandle::SetBlobRegistryForTesting(
    mojom::blink::BlobRegistry* registry) {
  g_blob_registry_for_testing = registry;
}

}  // namespace blink
