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

#include "components/bookmarks/browser/bookmark_node_data.h"

#include <algorithm>
#include <string>

#include "base/containers/span.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/numerics/safe_conversions.h"
#include "base/pickle.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "components/bookmarks/browser/bookmark_utils.h"
#include "components/bookmarks/common/bookmark_features.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"

namespace bookmarks {

namespace {

#if !BUILDFLAG(IS_APPLE)
std::unique_ptr<BookmarkNodeData> FromClipboardUrlReadResult(
    std::unique_ptr<BookmarkNodeData> data,
    std::u16string title,
    GURL url) {
  if (url.is_valid()) {
    BookmarkNodeData::Element element;
    element.is_url = true;
    element.url = url;
    element.title = title;

    data->elements.clear();
    data->elements.push_back(element);
    return data;
  }

  return nullptr;
}

void OnReadUrlFromClipboardComplete(
    base::OnceCallback<void(std::unique_ptr<BookmarkNodeData>)> callback,
    std::unique_ptr<BookmarkNodeData> data,
    ui::ClipboardUrlInfo url_info) {
  std::move(callback).Run(FromClipboardUrlReadResult(
      std::move(data), url_info.title, url_info.url));
}

void OnReadDataFromClipboardComplete(
    base::OnceCallback<void(std::unique_ptr<BookmarkNodeData>)> callback,
    std::string result) {
  auto data = std::make_unique<BookmarkNodeData>();
  if (!result.empty() &&
      data->ReadFromPickle(
          base::PickleIterator::WithData(base::as_byte_span(result)))) {
    CHECK(data->is_valid());
    std::move(callback).Run(std::move(data));
    return;
  }

  // If `ReadFromPickle` failed, `data` might have `profile_path_` set (see
  // `BookmarkNodeData::ReadFromPickle`). We want to preserve it when falling
  // back to `ReadURL`.
  ui::Clipboard::GetForCurrentThread()->ReadURL(
      /*data_dst=*/std::nullopt,
      base::BindOnce(&OnReadUrlFromClipboardComplete, std::move(callback),
                     std::move(data)));
}
#endif  // !BUILDFLAG(IS_APPLE)

}  // namespace

#if !BUILDFLAG(IS_APPLE)
namespace {
constexpr size_t kMaxBookmarkNestingDepth = 500;
constexpr size_t kMaxVectorPreallocateSize = 10000;
}  // namespace
#endif

BookmarkNodeData::Element::Element() : is_url(false), id_(0) {}

BookmarkNodeData::Element::Element(const BookmarkNode* node)
    : is_url(node->is_url()),
      url(node->url()),
      title(node->GetTitle()),
      date_added(node->date_added()),
      date_folder_modified(node->date_folder_modified()),
      id_(node->id()) {
  if (node->GetMetaInfoMap()) {
    meta_info_map = *node->GetMetaInfoMap();
  }
  for (const auto& child : node->children()) {
    children.emplace_back(child.get());
  }
}

BookmarkNodeData::Element::Element(const Element& other) = default;

BookmarkNodeData::Element::~Element() {}

#if !BUILDFLAG(IS_APPLE)
void BookmarkNodeData::Element::WriteToLegacyPickle(
    base::Pickle* pickle) const {
  pickle->WriteBool(is_url);
  pickle->WriteString(url.spec());
  pickle->WriteString16(title);
  pickle->WriteInt64(id_);
  pickle->WriteUInt32(static_cast<uint32_t>(meta_info_map.size()));
  for (const auto& [key, value] : meta_info_map) {
    pickle->WriteString(key);
    pickle->WriteString(value);
  }
  if (!is_url) {
    pickle->WriteUInt32(static_cast<uint32_t>(children.size()));
    for (const auto& child : children) {
      child.WriteToLegacyPickle(pickle);
    }
  }
}

bool BookmarkNodeData::Element::ReadFromLegacyPickle(
    base::PickleIterator* iterator,
    size_t depth) {
  std::string url_spec;
  if (!iterator->ReadBool(&is_url) || !iterator->ReadString(&url_spec) ||
      !iterator->ReadString16(&title) || !iterator->ReadInt64(&id_)) {
    return false;
  }
  url = GURL(url_spec);
  date_added = base::Time();
  date_folder_modified = base::Time();
  meta_info_map.clear();
  uint32_t meta_field_count;
  if (!iterator->ReadUInt32(&meta_field_count)) {
    return false;
  }
  for (size_t i = 0; i < meta_field_count; ++i) {
    std::string key;
    std::string value;
    if (!iterator->ReadString(&key) || !iterator->ReadString(&value)) {
      return false;
    }
    meta_info_map[key] = value;
  }
  children.clear();
  if (!is_url) {
    if (depth >= kMaxBookmarkNestingDepth) {
      return false;
    }

    uint32_t children_count_tmp;
    if (!iterator->ReadUInt32(&children_count_tmp)) {
      return false;
    }
    if (!base::IsValueInRangeForNumericType<size_t>(children_count_tmp)) {
      LOG(WARNING) << "children_count failed bounds check";
      return false;
    }
    // Note: do not preallocate the children vector. A pickle could be
    // constructed to contain N nested Elements. By continually recursing on
    // this ReadFromPickle function, the fast-fail logic is subverted. Each
    // child would claim it contains M more children. The first (and only) child
    // provided would claim M more children. We would allocate N * M Elements
    // along the way, while only receiving N Elements.
    const size_t children_count =
        base::checked_cast<size_t>(children_count_tmp);
    for (size_t i = 0; i < children_count; ++i) {
      children.emplace_back();
      if (!children.back().ReadFromLegacyPickle(iterator, depth + 1)) {
        return false;
      }
    }
  }
  return true;
}

base::Pickle BookmarkNodeData::Element::ToPickle() const {
  base::Pickle pickle;
  pickle.WriteBool(is_url);
  pickle.WriteString(url.spec());
  pickle.WriteString16(title);
  pickle.WriteInt64(id_);
  pickle.WriteUInt32(static_cast<uint32_t>(meta_info_map.size()));

  for (const auto& [key, value] : meta_info_map) {
    pickle.WriteString(key);
    pickle.WriteString(value);
  }

  if (!is_url) {
    pickle.WriteUInt32(static_cast<uint32_t>(children.size()));
    for (const auto& child : children) {
      pickle.WriteData(child.ToPickle());
    }
  }
  return pickle;
}

bool BookmarkNodeData::Element::FromPickle(base::PickleIterator iterator,
                                           size_t depth) {
  std::string url_spec;
  if (!iterator.ReadBool(&is_url) || !iterator.ReadString(&url_spec) ||
      !iterator.ReadString16(&title) || !iterator.ReadInt64(&id_)) {
    return false;
  }

  url = GURL(url_spec);
  date_added = base::Time();
  date_folder_modified = base::Time();
  meta_info_map.clear();

  uint32_t meta_field_count = 0;
  if (!iterator.ReadUInt32(&meta_field_count)) {
    return false;
  }

  for (size_t i = 0; i < meta_field_count; ++i) {
    std::string key;
    std::string value;
    if (!iterator.ReadString(&key) || !iterator.ReadString(&value)) {
      return false;
    }
    meta_info_map[key] = value;
  }

  children.clear();
  if (!is_url) {
    if (depth >= kMaxBookmarkNestingDepth) {
      return false;
    }

    uint32_t children_count_tmp = 0;
    if (!iterator.ReadUInt32(&children_count_tmp)) {
      return false;
    }

    if (!base::IsValueInRangeForNumericType<size_t>(children_count_tmp)) {
      LOG(WARNING) << "children_count failed bounds check";
      return false;
    }
    // Note: do not preallocate the children vector. A pickle could be
    // constructed to contain N nested Elements. By continually recursing on
    // this ReadFromPickle function, the fast-fail logic is subverted. Each
    // child would claim it contains M more children. The first (and only) child
    // provided would claim M more children. We would allocate N * M Elements
    // along the way, while only receiving N Elements.
    const size_t children_count =
        base::checked_cast<size_t>(children_count_tmp);
    for (size_t i = 0; i < children_count; ++i) {
      children.emplace_back();
      std::optional<base::span<const uint8_t>> span = iterator.ReadData();
      if (!span.has_value()) {
        return false;
      }
      if (!children.back().FromPickle(
              base::PickleIterator::WithData(span.value()), depth + 1)) {
        return false;
      }
    }
  }
  return true;
}
#endif

// BookmarkNodeData -----------------------------------------------------------

BookmarkNodeData::BookmarkNodeData() = default;

BookmarkNodeData::BookmarkNodeData(const BookmarkNodeData& other) = default;

BookmarkNodeData::BookmarkNodeData(const BookmarkNode* node) {
  elements.emplace_back(node);
}

BookmarkNodeData::BookmarkNodeData(
    const std::vector<raw_ptr<const BookmarkNode, VectorExperimental>>& nodes) {
  ReadFromVector(nodes);
}

BookmarkNodeData::~BookmarkNodeData() {}

#if !BUILDFLAG(IS_APPLE)
// static
void BookmarkNodeData::ClipboardContainsBookmarks(
    base::OnceCallback<void(bool)> callback) {
  ui::DataTransferEndpoint data_dst = ui::DataTransferEndpoint(
      ui::EndpointType::kDefault, {.notify_if_restricted = false});
  ui::Clipboard::GetForCurrentThread()->GetAllAvailableFormats(
      ui::ClipboardBuffer::kCopyPaste, data_dst,
      base::BindOnce(
          [](base::OnceCallback<void(bool)> callback,
             base::flat_set<ui::ClipboardFormatType> formats) {
            std::move(callback).Run(formats.contains(
                ui::ClipboardFormatType::BookmarkEntriesType()));
          },
          std::move(callback)));
}
#endif

bool BookmarkNodeData::ReadFromVector(
    const std::vector<raw_ptr<const BookmarkNode, VectorExperimental>>& nodes) {
  Clear();

  if (nodes.empty()) {
    return false;
  }

  for (const auto& node : nodes) {
    elements.emplace_back(node);
  }

  CHECK(is_valid());
  return true;
}

bool BookmarkNodeData::ReadFromTuple(const GURL& url,
                                     const std::u16string& title) {
  Clear();

  if (!url.is_valid()) {
    return false;
  }

  Element element;
  element.title = title;
  element.url = url;
  element.is_url = true;

  elements.push_back(element);

  CHECK(is_valid());
  return true;
}

#if !BUILDFLAG(IS_APPLE)
void BookmarkNodeData::WriteToClipboard(bool is_off_the_record) {
  ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);

  if (is_off_the_record) {
    // Data is copied from an incognito window, so mark it as off the record.
    scw.MarkAsOffTheRecord();
  }

#if BUILDFLAG(IS_WIN)
  const std::u16string kEOL(u"\r\n");
#else
  const std::u16string kEOL = u"\n";
#endif

  // If there is only one element and it is a URL, write the URL to the
  // clipboard.
  if (has_single_url()) {
    const std::u16string& title = elements[0].title;
    const std::string url = elements[0].url.spec();

    scw.WriteURL(ui::ClipboardUrlInfo{.url = GURL(url), .title = title});
    scw.WriteHyperlink(title, url);
    scw.WriteText(base::UTF8ToUTF16(url));
  } else {
    // We have either more than one URL, a folder, or a combination of URLs
    // and folders.
    std::u16string text;
    for (size_t i = 0; i < size(); i++) {
      text += i == 0 ? u"" : kEOL;
      if (!elements[i].is_url) {
        // Then it's a folder. Only copy the name of the folder.
        const std::u16string title = elements[i].title;
        text += title;
      } else {
        const std::u16string url = base::UTF8ToUTF16(elements[i].url.spec());
        text += url;
      }
    }
    scw.WriteText(text);
  }

  base::Pickle pickle;
  WriteToPickle(base::FilePath(), &pickle);
  scw.WritePickledData(pickle, ui::ClipboardFormatType::BookmarkEntriesType());
}

// static
void BookmarkNodeData::ReadFromClipboard(
    ui::ClipboardBuffer buffer,
    base::OnceCallback<void(std::unique_ptr<BookmarkNodeData>)> callback) {
  DCHECK_EQ(buffer, ui::ClipboardBuffer::kCopyPaste);
  ui::Clipboard* clipboard = ui::Clipboard::GetForCurrentThread();
  clipboard->ReadData(
      ui::ClipboardFormatType::BookmarkEntriesType(),
      /*data_dst=*/std::nullopt,
      base::BindOnce(&OnReadDataFromClipboardComplete, std::move(callback)));
}

void BookmarkNodeData::WriteToPickle(const base::FilePath& profile_path,
                                     base::Pickle* pickle) const {
  profile_path.WriteToPickle(pickle);
  if (base::FeatureList::IsEnabled(kEnableBookmarkNodeDataNewPickleFormat)) {
    // Backward-compatibility safeguard (as old versions cannot read the new
    // format).
    pickle->WriteUInt32(0);
    pickle->WriteUInt32(static_cast<uint32_t>(size()));
    for (const auto& element : elements) {
      pickle->WriteData(element.ToPickle());
    }
  } else {
    pickle->WriteUInt32(static_cast<uint32_t>(size()));
    for (const auto& element : elements) {
      element.WriteToLegacyPickle(pickle);
    }
  }
}

bool BookmarkNodeData::ReadFromPickle(base::PickleIterator data_iterator) {
  uint32_t element_count_tmp = 0;
  if (!profile_path_.ReadFromPickle(&data_iterator) ||
      !data_iterator.ReadUInt32(&element_count_tmp)) {
    return false;
  }

  // legacy format: profile_path, element_count, elements
  // newer format: profile_path, 0, element_count, elements
  bool legacy_format = (element_count_tmp != 0);
  if (!legacy_format) {
    if (!data_iterator.ReadUInt32(&element_count_tmp)) {
      return false;
    }
  }

  if (!base::IsValueInRangeForNumericType<size_t>(element_count_tmp)) {
    LOG(WARNING) << "element_count failed bounds check";
    return false;
  }
  const size_t element_count = base::checked_cast<size_t>(element_count_tmp);
  // Restrict vector preallocation to prevent OOM crashes on invalid or
  // malicious pickles.
  if (element_count > kMaxVectorPreallocateSize) {
    LOG(WARNING) << "element_count exceeds kMaxVectorPreallocateSize";
  }
  std::vector<Element> tmp_elements;
  tmp_elements.reserve(std::min(element_count, kMaxVectorPreallocateSize));
  if (legacy_format) {
    for (size_t i = 0; i < element_count; ++i) {
      tmp_elements.emplace_back();
      if (!tmp_elements.back().ReadFromLegacyPickle(&data_iterator)) {
        return false;
      }
    }
  } else {
    for (size_t i = 0; i < element_count; ++i) {
      std::optional<base::span<const uint8_t>> span = data_iterator.ReadData();
      if (!span.has_value()) {
        return false;
      }
      tmp_elements.emplace_back();
      if (!tmp_elements.back().FromPickle(
              base::PickleIterator::WithData(span.value()))) {
        return false;
      }
    }
  }
  elements.swap(tmp_elements);

  // Treat the data loading from pickle as a failure if elements is empty, which
  // forces the `ReadFromClipboard` fallback to continue reading data via
  // `ReadBookmark`. Otherwise, returning true while elements is empty will
  // trigger a CHECK. See http://crbug.com/472245783 for details.
  return is_valid();
}
#endif  // BUILDFLAG(IS_APPLE)

std::vector<raw_ptr<const BookmarkNode, VectorExperimental>>
BookmarkNodeData::GetNodes(BookmarkModel* model,
                           const base::FilePath& profile_path) const {
  std::vector<raw_ptr<const BookmarkNode, VectorExperimental>> nodes;

  if (!IsFromProfilePath(profile_path)) {
    return nodes;
  }

  for (size_t i = 0; i < size(); ++i) {
    const BookmarkNode* node = GetBookmarkNodeByID(model, elements[i].id_);
    if (!node) {
      nodes.clear();
      return nodes;
    }
    nodes.push_back(node);
  }
  return nodes;
}

const BookmarkNode* BookmarkNodeData::GetFirstNode(
    BookmarkModel* model,
    const base::FilePath& profile_path) const {
  std::vector<raw_ptr<const BookmarkNode, VectorExperimental>> nodes =
      GetNodes(model, profile_path);
  return nodes.size() == 1 ? nodes[0] : nullptr;
}

void BookmarkNodeData::Clear() {
  profile_path_.clear();
  elements.clear();
}

void BookmarkNodeData::SetOriginatingProfilePath(
    const base::FilePath& profile_path) {
  DCHECK(profile_path_.empty());
  profile_path_ = profile_path;
}

bool BookmarkNodeData::IsFromProfilePath(
    const base::FilePath& profile_path) const {
  // An empty path means the data is not associated with any profile.
  return !profile_path_.empty() && profile_path_ == profile_path;
}

}  // namespace bookmarks
