// Copyright 2021 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/blink/public/common/storage_key/storage_key.h"

#include <algorithm>
#include <memory>
#include <ostream>
#include <string>
#include <string_view>

#include "base/check.h"
#include "base/feature_list.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/types/optional_util.h"
#include "net/base/features.h"
#include "net/base/isolation_info.h"
#include "net/base/network_isolation_partition.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "net/cookies/cookie_partition_key.h"
#include "third_party/abseil-cpp/absl/strings/ascii.h"
#include "url/gurl.h"

namespace {

// This enum represents the different type of encodable partitioning
// attributes. These values are persisted to disk. Entries should not be
// renumbered and numeric values should never be reused.
enum class EncodedAttribute : uint8_t {
  kTopLevelSite = 0,
  kNonceHigh = 1,
  kNonceLow = 2,
  kAncestorChainBit = 3,
  kTopLevelSiteOpaqueNonceHigh = 4,
  kTopLevelSiteOpaqueNonceLow = 5,
  kTopLevelSiteOpaquePrecursor = 6,
  kMaxValue = kTopLevelSiteOpaquePrecursor,
};

// Converts the attribute type into the separator + uint8_t byte
// serialization. E.x.: kTopLevelSite becomes "^0"
std::string SerializeAttributeSeparator(const EncodedAttribute type) {
  // Create a size 2 string, we'll overwrite the second char later.
  std::string ret(2, '^');
  char digit = static_cast<uint8_t>(type) + '0';
  ret[1] = digit;
  return ret;
}

// Converts the serialized separator into an EncodedAttribute enum.
// E.x.: "^0" becomes kTopLevelSite.
// Expects `in` to have a length of 2.
std::optional<EncodedAttribute> DeserializeAttributeSeparator(
    const std::string_view& in) {
  DCHECK_EQ(in.size(), 2U);
  uint8_t number = in[1] - '0';

  if (number > static_cast<uint8_t>(EncodedAttribute::kMaxValue)) {
    // Bad input, return std::nullopt to indicate an issue.
    return std::nullopt;
  }

  return static_cast<EncodedAttribute>(number);
}

// Returns true if there are at least 2 chars after the '^' in `in` and the
// second char is not '^'. Meaning that the substring is syntactically valid.
// This is to indicate that there is a valid separator with both a '^' and a
// uint8_t and some amount of encoded data. I.e.: "^09" has both a "^0" as the
// separator and '9' as the encoded data.
bool ValidSeparatorWithData(std::string_view in, size_t pos_of_caret) {
  if (in.length() > pos_of_caret + 2 && in[pos_of_caret + 2] != '^')
    return true;

  return false;
}

}  // namespace

namespace blink {

// static
std::optional<StorageKey> StorageKey::Deserialize(std::string_view in) {
  // As per the Serialize() call, we have to expect one of the following
  // structures:
  // <StorageKey `key`.origin> + "/" + "^1" + <StorageKey
  // `key`.nonce.High64Bits> + "^2" + <StorageKey `key`.nonce.Low64Bits>
  // - or -
  // <StorageKey `key`.origin> + "/"
  // - or -
  // <StorageKey `key`.origin> + "/" + "^3" + <StorageKey
  // `key`.ancestor_chain_bit>
  // - or -
  // <StorageKey `key`.origin> + "/" + "^0" + <StorageKey `key`.top_level_site>
  // - or -
  // <StorageKey `key`.origin> + "/" + ^4" + <StorageKey
  // `key`.top_level_site.nonce.High64Bits> + "^5" + <StorageKey
  // `key`.top_level_site.nonce.Low64Bits>  + "^6" + <StorageKey
  // `key`.top_level_site.precursor>
  //
  // See Serialize() for more information.

  // Let's check for a delimiting caret. The presence of a caret means this key
  // is partitioned.

  // More than three encoded attributes (delimited by carets) indicates a
  // malformed input.
  if (std::ranges::count(in, '^') > 3) {
    return std::nullopt;
  }

  const size_t pos_first_caret = in.find_first_of('^');
  const size_t pos_second_caret =
      pos_first_caret == std::string::npos
          ? std::string::npos
          : in.find_first_of('^', pos_first_caret + 1);
  const size_t pos_third_caret =
      pos_second_caret == std::string::npos
          ? std::string::npos
          : in.find_first_of('^', pos_second_caret + 1);

  url::Origin key_origin;
  net::SchemefulSite key_top_level_site;
  std::optional<base::UnguessableToken> nonce;
  blink::mojom::AncestorChainBit ancestor_chain_bit;

  if (pos_first_caret == std::string::npos) {
    // Only the origin is serialized.

    key_origin = url::Origin::Create(GURL(in));

    // In this case the top_level_site is implicitly the same site as the
    // origin.
    key_top_level_site = net::SchemefulSite(key_origin);

    // There is no nonce.

    // The origin should not be opaque and the serialization should be
    // reversible.
    if (key_origin.opaque() || key_origin.GetURL().spec() != in) {
      return std::nullopt;
    }

    return StorageKey(key_origin, key_top_level_site, nullptr,
                      blink::mojom::AncestorChainBit::kSameSite,
                      /*third_party_partitioning_allowed=*/false);
  }

  if (!ValidSeparatorWithData(in, pos_first_caret))
    return std::nullopt;

  // Otherwise the key is partitioned, let's see what it's partitioned by.
  std::optional<EncodedAttribute> first_attribute =
      DeserializeAttributeSeparator(in.substr(pos_first_caret, 2));
  if (!first_attribute.has_value())
    return std::nullopt;

  switch (first_attribute.value()) {
    case EncodedAttribute::kTopLevelSite: {
      // Cross-Origin keys cannot be read if partitioning is off.
      if (!IsThirdPartyStoragePartitioningEnabled()) {
        return std::nullopt;
      }

      // A top-level site is serialized and has only one encoded attribute.
      if (pos_second_caret != std::string::npos) {
        return std::nullopt;
      }

      // The origin is the portion up to, but not including, the caret
      // separator.
      const std::string_view origin_substr = in.substr(0, pos_first_caret);
      key_origin = url::Origin::Create(GURL(origin_substr));

      // The origin should not be opaque and the serialization should be
      // reversible.
      if (key_origin.opaque() || key_origin.GetURL().spec() != origin_substr) {
        return std::nullopt;
      }

      // The top_level_site is the portion beyond the first separator.
      size_t length_of_site = pos_second_caret - (pos_first_caret + 2);
      const std::string_view top_level_site_substr =
          in.substr(pos_first_caret + 2, length_of_site);
      key_top_level_site = net::SchemefulSite(GURL(top_level_site_substr));

      // The top level site should not be opaque and the serialization should be
      // reversible.
      if (key_top_level_site.opaque() ||
          key_top_level_site.Serialize() != top_level_site_substr) {
        return std::nullopt;
      }

      // There is no nonce or ancestor chain bit.

      // Neither should be opaque and they cannot match as that would mean
      // we should have simply encoded the origin and the input is malformed.
      if (key_origin.opaque() || key_top_level_site.opaque() ||
          key_top_level_site.IsSameSiteWith(key_origin)) {
        return std::nullopt;
      }

      // The ancestor chain bit must be CrossSite as that's an invariant
      // when the origin and top level site don't match.
      // TODO(crbug.com/1199077): Deserialize should always be able to make 3p
      // keys and shouldn't depend on the state of partitioning (because we
      // don't want to inadvertently turn two 3p keys into the same 1p key).
      // Unfortunately, some tests (and potentially code) depend on this. Here,
      // and below, should be changed to true and the dependencies on this
      // behavior should be removed.
      return StorageKey(key_origin, key_top_level_site, nullptr,
                        blink::mojom::AncestorChainBit::kCrossSite,
                        IsThirdPartyStoragePartitioningEnabled());
    }
    case EncodedAttribute::kAncestorChainBit: {
      // Same-Origin kCrossSite keys cannot be read if partitioning is off.
      if (!IsThirdPartyStoragePartitioningEnabled()) {
        return std::nullopt;
      }

      // An ancestor chain bit is serialized and has only one encoded attribute.
      if (pos_second_caret != std::string::npos) {
        return std::nullopt;
      }

      // The origin is the portion up to, but not including, the caret
      // separator.
      const std::string_view origin_substr = in.substr(0, pos_first_caret);
      key_origin = url::Origin::Create(GURL(origin_substr));

      // The origin should not be opaque and the serialization should be
      // reversible.
      if (key_origin.opaque() || key_origin.GetURL().spec() != origin_substr) {
        return std::nullopt;
      }

      // The ancestor_chain_bit is the portion beyond the first separator.
      int raw_bit;
      const std::string_view raw_bit_substr =
          in.substr(pos_first_caret + 2, std::string::npos);
      if (!base::StringToInt(raw_bit_substr, &raw_bit)) {
        return std::nullopt;
      }

      // If the integer conversion results in a value outside the enumerated
      // indices of [0,1] or trimmed leading 0s we must reject the key.
      if (raw_bit < 0 || raw_bit > 1 || raw_bit_substr.size() > 1) {
        return std::nullopt;
      }
      ancestor_chain_bit = static_cast<blink::mojom::AncestorChainBit>(raw_bit);

      // There is no nonce or top level site.

      // The origin shouldn't be opaque and ancestor chain bit must be CrossSite
      // as otherwise should have simply encoded the origin and the input is
      // malformed.
      if (ancestor_chain_bit != blink::mojom::AncestorChainBit::kCrossSite) {
        return std::nullopt;
      }

      // This format indicates the top level site matches the origin.
      return StorageKey(key_origin, net::SchemefulSite(key_origin), nullptr,
                        ancestor_chain_bit,
                        IsThirdPartyStoragePartitioningEnabled());
    }
    case EncodedAttribute::kNonceHigh: {
      // A nonce is serialized and has only two encoded attributes.
      if (pos_third_caret != std::string::npos) {
        return std::nullopt;
      }

      // Make sure we found the next separator, it's valid, that it's the
      // correct attribute.
      if (pos_second_caret == std::string::npos ||
          !ValidSeparatorWithData(in, pos_second_caret))
        return std::nullopt;

      std::optional<EncodedAttribute> second_attribute =
          DeserializeAttributeSeparator(in.substr(pos_second_caret, 2));
      if (!second_attribute.has_value() ||
          second_attribute.value() != EncodedAttribute::kNonceLow)
        return std::nullopt;

      // The origin is the portion up to, but not including, the first
      // separator.
      const std::string_view origin_substr = in.substr(0, pos_first_caret);
      key_origin = url::Origin::Create(GURL(origin_substr));

      // The origin should not be opaque and the serialization should be
      // reversible.
      if (key_origin.opaque() || key_origin.GetURL().spec() != origin_substr) {
        return std::nullopt;
      }

      // The first high 64 bits of the nonce are next, between the two
      // separators.
      size_t length_of_high = pos_second_caret - (pos_first_caret + 2);
      std::string_view high_digits =
          in.substr(pos_first_caret + 2, length_of_high);
      // The low 64 bits are last, after the second separator.
      std::string_view low_digits = in.substr(pos_second_caret + 2);

      uint64_t nonce_high = 0;
      uint64_t nonce_low = 0;

      if (!base::StringToUint64(high_digits, &nonce_high))
        return std::nullopt;

      if (!base::StringToUint64(low_digits, &nonce_low))
        return std::nullopt;

      // The key is corrupted if there are extra 0s in front of the nonce.
      if (base::NumberToString(nonce_high) != high_digits ||
          base::NumberToString(nonce_low) != low_digits) {
        return std::nullopt;
      }

      nonce = base::UnguessableToken::Deserialize(nonce_high, nonce_low);

      if (!nonce.has_value()) {
        return std::nullopt;
      }

      // This constructor makes a copy of the nonce, so getting the raw pointer
      // is safe.
      // Note: The partitioning allowed value is irrelevant with a nonce,
      // `false` was chosen arbitrarily.
      return StorageKey(key_origin, net::SchemefulSite(key_origin),
                        &nonce.value(),
                        blink::mojom::AncestorChainBit::kCrossSite,
                        /*third_party_partitioning_allowed=*/false);
    }
    case EncodedAttribute::kTopLevelSiteOpaqueNonceHigh: {
      // An opaque `top_level_site` is serialized.

      // Cross-Origin keys cannot be read if partitioning is off.
      if (!IsThirdPartyStoragePartitioningEnabled()) {
        return std::nullopt;
      }

      // Make sure we found the next separator, it's valid, that it's the
      // correct attribute.
      if (pos_second_caret == std::string::npos ||
          !ValidSeparatorWithData(in, pos_second_caret)) {
        return std::nullopt;
      }

      std::optional<EncodedAttribute> second_attribute =
          DeserializeAttributeSeparator(in.substr(pos_second_caret, 2));
      if (!second_attribute.has_value() ||
          second_attribute.value() !=
              EncodedAttribute::kTopLevelSiteOpaqueNonceLow) {
        return std::nullopt;
      }

      // The origin is the portion up to, but not including, the first
      // separator.
      const std::string_view origin_substr = in.substr(0, pos_first_caret);
      key_origin = url::Origin::Create(GURL(origin_substr));

      // The origin should not be opaque and the serialization should be
      // reversible.
      if (key_origin.opaque() || key_origin.GetURL().spec() != origin_substr) {
        return std::nullopt;
      }

      // The first high 64 bits of the sites's nonce are next, between the first
      // separators.
      size_t length_of_high = pos_second_caret - (pos_first_caret + 2);
      std::string_view high_digits =
          in.substr(pos_first_caret + 2, length_of_high);
      // The low 64 bits are next, after the second separator.
      size_t length_of_low = pos_third_caret - (pos_second_caret + 2);
      std::string_view low_digits =
          in.substr(pos_second_caret + 2, length_of_low);

      uint64_t nonce_high = 0;
      uint64_t nonce_low = 0;

      if (!base::StringToUint64(high_digits, &nonce_high)) {
        return std::nullopt;
      }

      if (!base::StringToUint64(low_digits, &nonce_low)) {
        return std::nullopt;
      }

      // The key is corrupted if there are extra 0s in front of the nonce.
      if (base::NumberToString(nonce_high) != high_digits ||
          base::NumberToString(nonce_low) != low_digits) {
        return std::nullopt;
      }

      const std::optional<base::UnguessableToken> site_nonce =
          base::UnguessableToken::Deserialize(nonce_high, nonce_low);

      // The nonce must have content.
      if (!site_nonce) {
        return std::nullopt;
      }

      // Make sure we found the final separator, it's valid, that it's the
      // correct attribute.
      if (pos_third_caret == std::string::npos ||
          (in.size() - pos_third_caret) < 2) {
        return std::nullopt;
      }

      std::optional<EncodedAttribute> third_attribute =
          DeserializeAttributeSeparator(in.substr(pos_third_caret, 2));
      if (!third_attribute.has_value() ||
          third_attribute.value() !=
              EncodedAttribute::kTopLevelSiteOpaquePrecursor) {
        return std::nullopt;
      }

      // The precursor is the rest of the input.
      const std::string_view url_precursor_substr =
          in.substr(pos_third_caret + 2);
      const GURL url_precursor(url_precursor_substr);
      const url::SchemeHostPort tuple_precursor(url_precursor);

      // The precursor must be empty or valid, and the serialization should be
      // reversible.
      if ((!url_precursor.is_empty() && !tuple_precursor.IsValid()) ||
          tuple_precursor.Serialize() != url_precursor_substr) {
        return std::nullopt;
      }

      // This constructor makes a copy of the site's nonce, so getting the raw
      // pointer is safe.
      return StorageKey(
          key_origin,
          net::SchemefulSite(url::Origin(url::Origin::Nonce(site_nonce.value()),
                                         tuple_precursor)),
          nullptr, blink::mojom::AncestorChainBit::kCrossSite,
          IsThirdPartyStoragePartitioningEnabled());
    }
    default: {
      // Malformed input case. We saw a separator that we don't understand
      // or one in the wrong order.
      return std::nullopt;
    }
  }
}

// static
std::optional<StorageKey> StorageKey::DeserializeForLocalStorage(
    std::string_view in) {
  // We have to support the local storage specific variant that lacks the
  // trailing slash.
  const url::Origin maybe_origin = url::Origin::Create(GURL(in));
  if (!maybe_origin.opaque()) {
    if (maybe_origin.Serialize() == in) {
      return StorageKey(maybe_origin, net::SchemefulSite(maybe_origin), nullptr,
                        blink::mojom::AncestorChainBit::kSameSite,
                        /*third_party_partitioning_allowed=*/false);
    } else if (maybe_origin.GetURL().spec() == in) {
      // This first party key was passed in with a trailing slash. This is
      // required in Deserialize() but improper for DeserializeForLocalStorage()
      // and must be rejected.
      return std::nullopt;
    }
  }

  // Otherwise we fallback on base deserialization.
  return Deserialize(in);
}

// static
StorageKey StorageKey::CreateFromStringForTesting(const std::string& origin) {
  return CreateFirstParty(url::Origin::Create(GURL(origin)));
}

// static
// Keep consistent with BlinkStorageKey::FromWire().
bool StorageKey::FromWire(
    const url::Origin& origin,
    const net::SchemefulSite& top_level_site,
    const net::SchemefulSite& top_level_site_if_third_party_enabled,
    const std::optional<base::UnguessableToken>& nonce,
    blink::mojom::AncestorChainBit ancestor_chain_bit,
    blink::mojom::AncestorChainBit ancestor_chain_bit_if_third_party_enabled,
    StorageKey& out) {
  // We need to build a different key to prevent overriding `out` if the result
  // isn't valid.
  StorageKey maybe_out;
  maybe_out.origin_ = origin;
  maybe_out.top_level_site_ = top_level_site;
  maybe_out.top_level_site_if_third_party_enabled_ =
      top_level_site_if_third_party_enabled;
  maybe_out.nonce_ = nonce;
  maybe_out.ancestor_chain_bit_ = ancestor_chain_bit;
  maybe_out.ancestor_chain_bit_if_third_party_enabled_ =
      ancestor_chain_bit_if_third_party_enabled;
  if (maybe_out.IsValid()) {
    out = maybe_out;
    return true;
  }
  return false;
}

// static
bool StorageKey::IsThirdPartyStoragePartitioningEnabled() {
  return base::FeatureList::IsEnabled(
      net::features::kThirdPartyStoragePartitioning);
}

// static
StorageKey StorageKey::CreateFirstParty(const url::Origin& origin) {
  return StorageKey(origin, net::SchemefulSite(origin), nullptr,
                    origin.opaque() ? blink::mojom::AncestorChainBit::kCrossSite
                                    : blink::mojom::AncestorChainBit::kSameSite,
                    /*third_party_partitioning_allowed=*/false);
}

// static
StorageKey StorageKey::CreateWithNonce(const url::Origin& origin,
                                       const base::UnguessableToken& nonce) {
  // The AncestorChainBit is not applicable to StorageKeys with a non-empty
  // nonce, so they are initialized to be kCrossSite.
  // Note: The partitioning allowed value is irrelevant with a nonce, `false`
  // was chosen arbitrarily.
  return StorageKey(origin, net::SchemefulSite(origin), &nonce,
                    blink::mojom::AncestorChainBit::kCrossSite,
                    /*third_party_partitioning_allowed=*/false);
}

// static
StorageKey StorageKey::Create(const url::Origin& origin,
                              const net::SchemefulSite& top_level_site,
                              blink::mojom::AncestorChainBit ancestor_chain_bit,
                              bool third_party_partitioning_allowed) {
  return StorageKey(origin, top_level_site, nullptr, ancestor_chain_bit,
                    third_party_partitioning_allowed);
}

// static
StorageKey StorageKey::CreateFromOriginAndIsolationInfo(
    const url::Origin& origin,
    const net::IsolationInfo& isolation_info) {
  // Support for creating a StorageKey from IsolationInfos with special
  // NetworkIsolationPartition is not implemented.
  CHECK_EQ(isolation_info.GetNetworkIsolationPartition(),
           net::NetworkIsolationPartition::kGeneral);
  if (isolation_info.nonce()) {
    // If the nonce is set we can use the simpler construction path.
    return CreateWithNonce(origin, *isolation_info.nonce());
  }

  blink::mojom::AncestorChainBit ancestor_chain_bit =
      blink::mojom::AncestorChainBit::kCrossSite;
  net::SchemefulSite top_level_site =
      net::SchemefulSite(isolation_info.top_frame_origin().value());
  // If the origin or top_level_site is opaque the ancestor chain bit will be
  // CrossSite. Otherwise if the top level site matches the new origin and the
  // site for cookies isn't empty it must be SameSite.
  if (!origin.opaque() && !top_level_site.opaque() &&
      top_level_site.IsSameSiteWith(origin) &&
      !isolation_info.site_for_cookies().IsNull()) {
    ancestor_chain_bit = blink::mojom::AncestorChainBit::kSameSite;
  }
  return Create(origin, top_level_site, ancestor_chain_bit,
                IsThirdPartyStoragePartitioningEnabled());
}

StorageKey StorageKey::WithOrigin(const url::Origin& origin) const {
  net::SchemefulSite top_level_site = top_level_site_;
  net::SchemefulSite top_level_site_if_third_party_enabled =
      top_level_site_if_third_party_enabled_;
  blink::mojom::AncestorChainBit ancestor_chain_bit = ancestor_chain_bit_;
  blink::mojom::AncestorChainBit ancestor_chain_bit_if_third_party_enabled =
      ancestor_chain_bit_if_third_party_enabled_;

  if (nonce_) {
    // If the nonce is set we have to update the top level site to match origin
    // as that's an invariant.
    top_level_site = net::SchemefulSite(origin);
    top_level_site_if_third_party_enabled = top_level_site;
  } else if (!top_level_site_.opaque()) {
    // If `top_level_site_` is opaque then so is
    // `top_level_site_if_third_party_enabled` and we don't need to explicitly
    // check it. The ancestor chain bit also doesn't need to be changed in this
    // case.

    // Only adjust the ancestor chain bit if it's currently kSameSite but the
    // new origin and top level site don't match. Note that the ACB might not
    // necessarily be kSameSite if the TLS and origin do match, so we won't
    // adjust the other way.
    if (ancestor_chain_bit == blink::mojom::AncestorChainBit::kSameSite &&
        !top_level_site_.IsSameSiteWith(origin)) {
      ancestor_chain_bit = blink::mojom::AncestorChainBit::kCrossSite;
    }

    if (ancestor_chain_bit_if_third_party_enabled ==
            blink::mojom::AncestorChainBit::kSameSite &&
        !top_level_site_if_third_party_enabled.IsSameSiteWith(origin)) {
      ancestor_chain_bit_if_third_party_enabled =
          blink::mojom::AncestorChainBit::kCrossSite;
    }
  }

  StorageKey out = *this;
  out.origin_ = origin;
  out.top_level_site_ = top_level_site;
  out.top_level_site_if_third_party_enabled_ =
      top_level_site_if_third_party_enabled;
  out.ancestor_chain_bit_ = ancestor_chain_bit;
  out.ancestor_chain_bit_if_third_party_enabled_ =
      ancestor_chain_bit_if_third_party_enabled;
  DCHECK(out.IsValid());
  return out;
}

StorageKey::StorageKey(const url::Origin& origin,
                       const net::SchemefulSite& top_level_site,
                       const base::UnguessableToken* nonce,
                       blink::mojom::AncestorChainBit ancestor_chain_bit,
                       bool third_party_partitioning_allowed)
    : origin_(origin),
      top_level_site_(third_party_partitioning_allowed
                          ? top_level_site
                          : net::SchemefulSite(origin)),
      top_level_site_if_third_party_enabled_(top_level_site),
      nonce_(base::OptionalFromPtr(nonce)),
      ancestor_chain_bit_(third_party_partitioning_allowed ? ancestor_chain_bit
                          : (nonce || origin.opaque())
                              ? blink::mojom::AncestorChainBit::kCrossSite
                              : blink::mojom::AncestorChainBit::kSameSite),
      ancestor_chain_bit_if_third_party_enabled_(ancestor_chain_bit) {
  DCHECK(IsValid());
}

std::string StorageKey::Serialize() const {
  DCHECK(!origin_.opaque());

  // If the storage key has a nonce, implying the top_level_site is the same as
  // origin and ancestor_chain_bit is kCrossSite, then we need to serialize the
  // key to fit the following scheme:
  //
  // Case 0: <StorageKey `key`.origin> + "/" + "^1" + <StorageKey
  // `key`.nonce.High64Bits> + "^2" + <StorageKey `key`.nonce.Low64Bits>
  //
  // Note that we intentionally do not include the AncestorChainBit in
  // serialization with nonce formats as that information is not applicable
  // (similar to top-level-site).
  if (nonce_.has_value()) {
    return origin_.GetURL().spec() +
           SerializeAttributeSeparator(EncodedAttribute::kNonceHigh) +
           base::NumberToString(nonce_->GetHighForSerialization()) +
           SerializeAttributeSeparator(EncodedAttribute::kNonceLow) +
           base::NumberToString(nonce_->GetLowForSerialization());
  }

  // Else if storage partitioning is enabled we need to serialize the key to fit
  // one of the following schemes:
  //
  // Case 1: If the ancestor_chain_bit is kSameSite or partitioning is disabled:
  //
  // <StorageKey `key`.origin> + "/"
  //
  // Case 2: If the origin matches top_level_site and the ancestor_chain_bit is
  // kCrossSite:
  //
  // <StorageKey `key`.origin> + "/" + "^3" + <StorageKey
  // `key`.ancestor_chain_bit>
  //
  // Case 3: If the origin doesn't match top_level_site (implying
  // ancestor_chain_bit is kCrossSite):
  //
  // <StorageKey `key`.origin> + "/" + "^0" + <StorageKey `key`.top_level_site>
  //
  // Case 4: If the top_level_site is opaque (implying ancestor_chain_bit is
  // kCrossSite):
  //
  // <StorageKey `key`.origin> + "/" + ^4" + <StorageKey
  // `key`.top_level_site.nonce.High64Bits> + "^5" + <StorageKey
  // `key`.top_level_site.nonce.Low64Bits>  + "^6" + <StorageKey
  // `key`.top_level_site.precursor>
  if (IsThirdPartyStoragePartitioningEnabled() &&
      ancestor_chain_bit_ == blink::mojom::AncestorChainBit::kCrossSite) {
    if (top_level_site_.opaque()) {
      // Case 4.
      return base::StrCat({
          origin_.GetURL().spec(),
          SerializeAttributeSeparator(
              EncodedAttribute::kTopLevelSiteOpaqueNonceHigh),
          base::NumberToString(top_level_site_.internal_value()
                                   .GetNonceForSerialization()
                                   ->GetHighForSerialization()),
          SerializeAttributeSeparator(
              EncodedAttribute::kTopLevelSiteOpaqueNonceLow),
          base::NumberToString(top_level_site_.internal_value()
                                   .GetNonceForSerialization()
                                   ->GetLowForSerialization()),
          SerializeAttributeSeparator(
              EncodedAttribute::kTopLevelSiteOpaquePrecursor),
          top_level_site_.internal_value()
              .GetTupleOrPrecursorTupleIfOpaque()
              .Serialize(),
      });
    } else if (top_level_site_.IsSameSiteWith(origin_)) {
      // Case 2.
      return base::StrCat({
          origin_.GetURL().spec(),
          SerializeAttributeSeparator(EncodedAttribute::kAncestorChainBit),
          base::NumberToString(static_cast<int>(ancestor_chain_bit_)),
      });
    } else {
      // Case 3.
      return base::StrCat({
          origin_.GetURL().spec(),
          SerializeAttributeSeparator(EncodedAttribute::kTopLevelSite),
          top_level_site_.Serialize(),
      });
    }
  }

  // Case 1.
  return origin_.GetURL().spec();
}

std::string StorageKey::SerializeForLocalStorage() const {
  DCHECK(!origin_.opaque());

  // If this is a third-party StorageKey we'll use the standard serialization
  // scheme when partitioning is enabled or if there is a nonce.
  if (IsThirdPartyContext()) {
    return Serialize();
  }

  // Otherwise localStorage expects a slightly different scheme, so call that.
  return origin_.Serialize();
}

std::string StorageKey::GetDebugString() const {
  return base::StrCat(
      {"{ origin: ", origin_.GetDebugString(),
       ", top-level site: ", top_level_site_.Serialize(),
       ", nonce: ", nonce_.has_value() ? nonce_->ToString() : "<null>",
       ", ancestor chain bit: ",
       ancestor_chain_bit_ == blink::mojom::AncestorChainBit::kSameSite
           ? "Same-Site"
           : "Cross-Site",
       " }"});
}

std::string StorageKey::GetMemoryDumpString(size_t max_length) const {
  std::string memory_dump_str = origin_.Serialize().substr(0, max_length);

  if (max_length > memory_dump_str.length()) {
    memory_dump_str.append(top_level_site_.Serialize().substr(
        0, max_length - memory_dump_str.length()));
  }

  if (nonce_.has_value() && max_length > memory_dump_str.length()) {
    memory_dump_str.append(
        nonce_->ToString().substr(0, max_length - memory_dump_str.length()));
  }

  if (max_length > memory_dump_str.length()) {
    std::string ancestor_full_string =
        ancestor_chain_bit_ == blink::mojom::AncestorChainBit::kSameSite
            ? "Same-Site"
            : "Cross-Site";
    memory_dump_str.append(
        ancestor_full_string.substr(0, max_length - memory_dump_str.length()));
  }

  std::ranges::replace_if(
      memory_dump_str.begin(), memory_dump_str.end(),
      [](char c) {
        return !absl::ascii_isalnum(static_cast<unsigned char>(c));
      },
      '_');
  return memory_dump_str;
}

const net::SiteForCookies StorageKey::ToNetSiteForCookies() const {
  if (IsThirdPartyContext()) {
    // If any of the ancestor frames are cross-site to `origin_` then the
    // SiteForCookies should be null. The existence of `nonce_` means the same
    // thing.
    return net::SiteForCookies();
  }

  // Otherwise we are in a first party context.
  return net::SiteForCookies(top_level_site_);
}

const net::IsolationInfo StorageKey::ToPartialNetIsolationInfo() const {
  url::Origin top_frame_origin =
      IsFirstPartyContext() ? origin_
                            : url::Origin::Create(top_level_site_.GetURL());
  return net::IsolationInfo::Create(net::IsolationInfo::RequestType::kOther,
                                    top_frame_origin, origin_,
                                    ToNetSiteForCookies(), nonce_);
}

// static
bool StorageKey::ShouldSkipKeyDueToPartitioning(
    const std::string& reg_key_string) {
  // Don't skip anything if storage partitioning is enabled.
  if (IsThirdPartyStoragePartitioningEnabled())
    return false;

  // Determine if there is a valid attribute encoded with a caret
  size_t pos_first_caret = reg_key_string.find_first_of('^');
  if (pos_first_caret != std::string::npos &&
      ValidSeparatorWithData(reg_key_string, pos_first_caret)) {
    std::optional<EncodedAttribute> attribute = DeserializeAttributeSeparator(
        reg_key_string.substr(pos_first_caret, 2));
    // Do skip if partitioning is disabled and we detect a top-level site
    // serialization scheme (opaque or otherwise) or an ancestor chain bit:
    if (attribute.has_value() &&
        (attribute == EncodedAttribute::kTopLevelSite ||
         attribute == EncodedAttribute::kAncestorChainBit ||
         attribute == EncodedAttribute::kTopLevelSiteOpaqueNonceHigh)) {
      return true;
    }
  }
  // If otherwise first-party, nonce, or corrupted, don't skip.
  return false;
}

const std::optional<net::CookiePartitionKey> StorageKey::ToCookiePartitionKey()
    const {
  return net::CookiePartitionKey::FromStorageKeyComponents(
      top_level_site_,
      net::CookiePartitionKey::BoolToAncestorChainBit(IsThirdPartyContext()),
      nonce_);
}

bool StorageKey::MatchesOriginForTrustedStorageDeletion(
    const url::Origin& origin) const {
  // TODO(crbug.com/1382138): Address wss:// and https:// resulting in different
  // SchemefulSites.
  // TODO(crbug.com/1410196): Test that StorageKeys corresponding to anonymous
  // iframes are handled appropriately here.
  return IsFirstPartyContext() ? (origin_ == origin)
                               : (top_level_site_.IsSameSiteWith(origin));
}

bool StorageKey::MatchesRegistrableDomainForTrustedStorageDeletion(
    std::string_view domain) const {
  // TODO(crbug.com/1410196): Test that StorageKeys corresponding to anonymous
  // iframes are handled appropriately here.
  return top_level_site_.registrable_domain_or_host() == domain;
}

bool StorageKey::ExactMatchForTesting(const StorageKey& other) const {
  return *this == other &&
         this->ancestor_chain_bit_if_third_party_enabled_ ==
             other.ancestor_chain_bit_if_third_party_enabled_ &&
         this->top_level_site_if_third_party_enabled_ ==
             other.top_level_site_if_third_party_enabled_;
}

std::ostream& operator<<(std::ostream& ostream, const StorageKey& sk) {
  return ostream << sk.GetDebugString();
}

bool StorageKey::IsValid() const {
  // If the key's origin is opaque ancestor_chain_bit* is always kCrossSite
  // no matter the value of the other members.
  if (origin_.opaque()) {
    if (ancestor_chain_bit_ != blink::mojom::AncestorChainBit::kCrossSite) {
      return false;
    }
    if (ancestor_chain_bit_if_third_party_enabled_ !=
        blink::mojom::AncestorChainBit::kCrossSite) {
      return false;
    }
  }

  // If this key's "normal" members indicate a 3p key, then the
  // *_if_third_party_enabled counterparts must match them.
  if (!origin_.opaque() &&
      (!top_level_site_.IsSameSiteWith(origin_) ||
       ancestor_chain_bit_ != blink::mojom::AncestorChainBit::kSameSite)) {
    if (top_level_site_ != top_level_site_if_third_party_enabled_) {
      return false;
    }
    if (ancestor_chain_bit_ != ancestor_chain_bit_if_third_party_enabled_) {
      return false;
    }
  }

  // If top_level_site* is cross-site to origin, then ancestor_chain_bit* must
  // indicate that. An opaque top_level_site* must have a cross-site
  // ancestor_chain_bit*.
  if (!top_level_site_.IsSameSiteWith(origin_)) {
    if (ancestor_chain_bit_ != blink::mojom::AncestorChainBit::kCrossSite) {
      return false;
    }
  }

  if (!top_level_site_if_third_party_enabled_.IsSameSiteWith(origin_)) {
    if (ancestor_chain_bit_if_third_party_enabled_ !=
        blink::mojom::AncestorChainBit::kCrossSite) {
      return false;
    }
  }

  // If there is a nonce, all other values must indicate same-site to origin.
  if (nonce_) {
    if (nonce_->is_empty()) {
      return false;
    }
    if (!top_level_site_.IsSameSiteWith(origin_)) {
      return false;
    }

    if (!top_level_site_if_third_party_enabled_.IsSameSiteWith(origin_)) {
      return false;
    }

    if (ancestor_chain_bit_ != blink::mojom::AncestorChainBit::kCrossSite) {
      return false;
    }

    if (ancestor_chain_bit_if_third_party_enabled_ !=
        blink::mojom::AncestorChainBit::kCrossSite) {
      return false;
    }
  }

  // If the state is not invalid, it must be valid!
  return true;
}

}  // namespace blink
