///|
/// One Cookie domain-scope decision to re-evaluate across PSL snapshots.
/// Hosts and Domain attributes must already be ASCII/A-label DNS names.
pub struct CookieScopeInput {
request_host_ : String
domain_attribute_ : String?
}
///|
pub fn CookieScopeInput::new(
request_host : String,
domain_attribute : String?,
) -> CookieScopeInput {
{ request_host_: request_host, domain_attribute_: domain_attribute, }
}
///|
pub fn CookieScopeInput::request_host(self : CookieScopeInput) -> String {
self.request_host_
}
///|
pub fn CookieScopeInput::domain_attribute(self : CookieScopeInput) -> String? {
self.domain_attribute_
}
///|
/// Check ASCII DNS syntax without consulting a PSL or applying lookup policy.
/// A syntactically valid Domain attribute may still be rejected later because
/// it is a public suffix or does not domain-match the request host.
pub fn CookieScopeInput::validate_syntax(
self : CookieScopeInput,
) -> Result[Unit, CookieScopeError] {
match canonical_request_host(self.request_host_) {
Ok(_) => ()
Err(error) => return Err(error)
}
match self.domain_attribute_ {
None => ()
Some(value) =>
match canonical_domain_attribute(value) {
Ok(_) => ()
Err(error) => return Err(error)
}
}
Ok(())
}
///|
/// A behavioral change in Cookie domain acceptance or stored scope.
pub(all) enum CookieScopeImpactKind {
CookieBecameAccepted
CookieBecameRejected
CookieScopeChanged
}
///|
pub struct CookieScopeImpact {
index_ : Int
input_ : CookieScopeInput
kind_ : CookieScopeImpactKind
before_ : Result[CookieScope, CookieScopeError]
after_ : Result[CookieScope, CookieScopeError]
}
///|
pub fn CookieScopeImpact::index(self : CookieScopeImpact) -> Int {
self.index_
}
///|
pub fn CookieScopeImpact::input(self : CookieScopeImpact) -> CookieScopeInput {
self.input_
}
///|
pub fn CookieScopeImpact::kind(
self : CookieScopeImpact,
) -> CookieScopeImpactKind {
self.kind_
}
///|
pub fn CookieScopeImpact::before(
self : CookieScopeImpact,
) -> Result[CookieScope, CookieScopeError] {
self.before_
}
///|
pub fn CookieScopeImpact::after(
self : CookieScopeImpact,
) -> Result[CookieScope, CookieScopeError] {
self.after_
}
///|
/// Ordered Cookie-scope changes caused by replacing a pinned PSL snapshot.
pub struct CookieScopeImpactReport {
from_revision_ : String
to_revision_ : String
from_sha256_ : String
to_sha256_ : String
scanned_count_ : Int
unchanged_count_ : Int
impacts_ : ReadOnlyArray[CookieScopeImpact]
}
///|
pub fn CookieScopeImpactReport::from_revision(
self : CookieScopeImpactReport,
) -> String {
self.from_revision_
}
///|
pub fn CookieScopeImpactReport::to_revision(
self : CookieScopeImpactReport,
) -> String {
self.to_revision_
}
///|
pub fn CookieScopeImpactReport::from_sha256(
self : CookieScopeImpactReport,
) -> String {
self.from_sha256_
}
///|
pub fn CookieScopeImpactReport::to_sha256(
self : CookieScopeImpactReport,
) -> String {
self.to_sha256_
}
///|
pub fn CookieScopeImpactReport::scanned_count(
self : CookieScopeImpactReport,
) -> Int {
self.scanned_count_
}
///|
pub fn CookieScopeImpactReport::changed_count(
self : CookieScopeImpactReport,
) -> Int {
self.impacts_.length()
}
///|
pub fn CookieScopeImpactReport::unchanged_count(
self : CookieScopeImpactReport,
) -> Int {
self.unchanged_count_
}
///|
pub fn CookieScopeImpactReport::impacts(
self : CookieScopeImpactReport,
) -> ReadOnlyArray[CookieScopeImpact] {
self.impacts_
}
///|
pub fn CookieScopeImpactReport::is_empty(
self : CookieScopeImpactReport,
) -> Bool {
self.impacts_.length() == 0
}
///|
fn classify_cookie_scope_impact(
before : Result[CookieScope, CookieScopeError],
after : Result[CookieScope, CookieScopeError],
) -> CookieScopeImpactKind? {
match (before, after) {
(Err(_), Ok(_)) => Some(CookieBecameAccepted)
(Ok(_), Err(_)) => Some(CookieBecameRejected)
(Err(_), Err(_)) => None
(Ok(old_scope), Ok(new_scope)) =>
if old_scope != new_scope {
Some(CookieScopeChanged)
} else {
None
}
}
}
///|
/// Re-evaluate an ordered inventory of Cookie Domain decisions under two
/// verified snapshots using the same lookup policy.
///
/// Changes to Cookie acceptance or the stored domain/host-only flag are
/// reported. Two rejections count as unchanged even if their error text differs.
/// Invalid inputs are retained as row-level outcomes and do not stop the scan.
pub fn Snapshot::analyze_cookie_scope_impact(
self : Snapshot,
newer : Snapshot,
inputs : Array[CookieScopeInput],
options : LookupOptions,
) -> CookieScopeImpactReport {
self.compare_to(newer).analyze_cookie_scope_impact(inputs, options)
}
///|
fn analyze_cookie_scope_impact_with_lists(
older : Snapshot,
newer : Snapshot,
before_list : SuffixList,
after_list : SuffixList,
inputs : Array[CookieScopeInput],
options : LookupOptions,
) -> CookieScopeImpactReport {
let impacts : Array[CookieScopeImpact] = []
let mut unchanged_count = 0
for index, input in inputs {
let before = before_list.resolve_cookie_scope_with_options(
input.request_host_,
input.domain_attribute_,
options,
)
let after = after_list.resolve_cookie_scope_with_options(
input.request_host_,
input.domain_attribute_,
options,
)
match classify_cookie_scope_impact(before, after) {
Some(kind) =>
impacts.push({
index_: index,
input_: input,
kind_: kind,
before_: before,
after_: after,
})
None => unchanged_count = unchanged_count + 1
}
}
{
from_revision_: older.source_revision_,
to_revision_: newer.source_revision_,
from_sha256_: older.sha256_,
to_sha256_: newer.sha256_,
scanned_count_: inputs.length(),
unchanged_count_: unchanged_count,
impacts_: ReadOnlyArray::from_array(impacts),
}
}
///|
fn cookie_scope_impact_kind_name(kind : CookieScopeImpactKind) -> String {
match kind {
CookieBecameAccepted => "became_accepted"
CookieBecameRejected => "became_rejected"
CookieScopeChanged => "scope_changed"
}
}
///|
fn append_cookie_scope_fields(
fields : Array[String],
outcome : Result[CookieScope, CookieScopeError],
) -> Unit {
match outcome {
Ok(scope) => {
fields.push("ok")
fields.push(scope.domain())
fields.push(scope.is_host_only().to_string())
fields.push("")
}
Err(error) => {
fields.push("error")
fields.push("")
fields.push("")
fields.push(error.message())
}
}
}
///|
/// Export changed rows to deterministic, fully quoted CSV. A separate
/// presence field distinguishes a missing Domain attribute from an empty one.
pub fn CookieScopeImpactReport::to_csv(
self : CookieScopeImpactReport,
) -> String {
let output = StringBuilder()
output.write_string(
"\"index\",\"request_host\",\"domain_attribute_present\",\"domain_attribute\",\"impact\",\"before_status\",\"before_domain\",\"before_host_only\",\"before_error\",\"after_status\",\"after_domain\",\"after_host_only\",\"after_error\"\n",
)
for impact in self.impacts_ {
let attribute = impact.input_.domain_attribute_
let fields = [
impact.index_.to_string(),
impact.input_.request_host_,
(attribute is Some(_)).to_string(),
attribute.unwrap_or(""),
cookie_scope_impact_kind_name(impact.kind_),
]
append_cookie_scope_fields(fields, impact.before_)
append_cookie_scope_fields(fields, impact.after_)
write_csv_row(output, fields)
}
output.to_string()
}