///|
/// The externally visible effect of a PSL snapshot change on one hostname.
pub(all) enum ImpactKind {
BecameAccepted
BecameRejected
BoundaryChanged
RuleMetadataChanged
} derive(Eq, Debug)
///|
/// One changed hostname in a snapshot impact analysis.
pub struct DomainImpact {
index_ : Int
input_ : String
kind_ : ImpactKind
before_ : Result[Lookup, DomainError]
after_ : Result[Lookup, DomainError]
}
///|
pub fn DomainImpact::index(self : DomainImpact) -> Int {
self.index_
}
///|
pub fn DomainImpact::input(self : DomainImpact) -> String {
self.input_
}
///|
pub fn DomainImpact::kind(self : DomainImpact) -> ImpactKind {
self.kind_
}
///|
pub fn DomainImpact::before(self : DomainImpact) -> Result[Lookup, DomainError] {
self.before_
}
///|
pub fn DomainImpact::after(self : DomainImpact) -> Result[Lookup, DomainError] {
self.after_
}
///|
/// Ordered hostname changes caused by replacing one snapshot with another.
pub struct SnapshotImpact {
from_revision_ : String
to_revision_ : String
from_sha256_ : String
to_sha256_ : String
scanned_count_ : Int
unchanged_count_ : Int
impacts_ : ReadOnlyArray[DomainImpact]
}
///|
pub fn SnapshotImpact::from_revision(self : SnapshotImpact) -> String {
self.from_revision_
}
///|
pub fn SnapshotImpact::to_revision(self : SnapshotImpact) -> String {
self.to_revision_
}
///|
pub fn SnapshotImpact::from_sha256(self : SnapshotImpact) -> String {
self.from_sha256_
}
///|
pub fn SnapshotImpact::to_sha256(self : SnapshotImpact) -> String {
self.to_sha256_
}
///|
pub fn SnapshotImpact::scanned_count(self : SnapshotImpact) -> Int {
self.scanned_count_
}
///|
pub fn SnapshotImpact::changed_count(self : SnapshotImpact) -> Int {
self.impacts_.length()
}
///|
pub fn SnapshotImpact::unchanged_count(self : SnapshotImpact) -> Int {
self.unchanged_count_
}
///|
pub fn SnapshotImpact::impacts(
self : SnapshotImpact,
) -> ReadOnlyArray[DomainImpact] {
self.impacts_
}
///|
pub fn SnapshotImpact::is_empty(self : SnapshotImpact) -> Bool {
self.impacts_.length() == 0
}
///|
fn classify_impact(
before : Result[Lookup, DomainError],
after : Result[Lookup, DomainError],
) -> ImpactKind? {
match (before, after) {
(Err(_), Ok(_)) => Some(BecameAccepted)
(Ok(_), Err(_)) => Some(BecameRejected)
(Err(_), Err(_)) => None
(Ok(left), Ok(right)) =>
if left.public_suffix() != right.public_suffix() ||
left.registrable_domain() != right.registrable_domain() {
Some(BoundaryChanged)
} else if left.matched_rule() != right.matched_rule() ||
left.rule_kind() != right.rule_kind() ||
left.rule_section() != right.rule_section() {
Some(RuleMetadataChanged)
} else {
None
}
}
}
///|
/// Compare the lookup outcome for every hostname under the same policy.
///
/// Unchanged rows are counted but omitted. Changed rows retain caller order,
/// duplicate inputs, their original index, and both lookup outcomes.
pub fn Snapshot::analyze_impact(
self : Snapshot,
newer : Snapshot,
domains : Array[String],
options : LookupOptions,
) -> SnapshotImpact {
self.compare_to(newer).analyze_impact(domains, options)
}
///|
fn analyze_impact_with_lists(
older : Snapshot,
newer : Snapshot,
before_list : SuffixList,
after_list : SuffixList,
domains : Array[String],
options : LookupOptions,
) -> SnapshotImpact {
let impacts : Array[DomainImpact] = []
let mut unchanged_count = 0
for index, domain in domains {
let before = before_list.lookup_with_options(domain, options)
let after = after_list.lookup_with_options(domain, options)
match classify_impact(before, after) {
Some(kind) =>
impacts.push({
index_: index,
input_: domain,
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_: domains.length(),
unchanged_count_: unchanged_count,
impacts_: ReadOnlyArray::from_array(impacts),
}
}
///|
fn impact_kind_name(kind : ImpactKind) -> String {
match kind {
BecameAccepted => "became_accepted"
BecameRejected => "became_rejected"
BoundaryChanged => "boundary_changed"
RuleMetadataChanged => "rule_metadata_changed"
}
}
///|
fn append_lookup_fields(
fields : Array[String],
outcome : Result[Lookup, DomainError],
) -> Unit {
match outcome {
Ok(result) => {
fields.push("ok")
fields.push(result.normalized_domain())
fields.push(result.public_suffix())
fields.push(result.registrable_domain().unwrap_or(""))
fields.push(result.matched_rule())
fields.push(rule_kind_name(result.rule_kind()))
fields.push(result.rule_section().map(section_name).unwrap_or(""))
fields.push("")
}
Err(error) =>
for field in ["error", "", "", "", "", "", "", error.message()] {
fields.push(field)
}
}
}
///|
fn domain_impacts_to_csv(impacts : ReadOnlyArray[DomainImpact]) -> String {
let output = StringBuilder()
output.write_string(
"\"index\",\"input\",\"impact\",\"before_status\",\"before_normalized_domain\",\"before_public_suffix\",\"before_registrable_domain\",\"before_matched_rule\",\"before_rule_kind\",\"before_rule_section\",\"before_error\",\"after_status\",\"after_normalized_domain\",\"after_public_suffix\",\"after_registrable_domain\",\"after_matched_rule\",\"after_rule_kind\",\"after_rule_section\",\"after_error\"\n",
)
for impact in impacts {
let fields = [
impact.index_.to_string(),
impact.input_,
impact_kind_name(impact.kind_),
]
append_lookup_fields(fields, impact.before_)
append_lookup_fields(fields, impact.after_)
write_csv_row(output, fields)
}
output.to_string()
}
///|
/// Export only changed rows as deterministic, fully quoted CSV.
pub fn SnapshotImpact::to_csv(self : SnapshotImpact) -> String {
domain_impacts_to_csv(self.impacts_)
}